From df9fcf9850dc9aabe121cfe1e6cf7d18444d34b6 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Thu, 22 Jun 2023 06:09:00 +0200 Subject: [PATCH 001/270] Make ethernet_info work with esp-idf framework (#4976) --- .../components/ethernet_info/ethernet_info_text_sensor.cpp | 4 ++-- esphome/components/ethernet_info/ethernet_info_text_sensor.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index e69872c290..f841875396 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -1,7 +1,7 @@ #include "ethernet_info_text_sensor.h" #include "esphome/core/log.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP } // namespace ethernet_info } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index aad8f362b5..2d46fe18eb 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/ethernet/ethernet_component.h" -#ifdef USE_ESP32_FRAMEWORK_ARDUINO +#ifdef USE_ESP32 namespace esphome { namespace ethernet_info { @@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS } // namespace ethernet_info } // namespace esphome -#endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif // USE_ESP32 From a90d266017fd10e1c927865fbed7b3c7f987d589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 22 Jun 2023 21:18:29 +0200 Subject: [PATCH 002/270] display: fix white screen on binary displays (#4991) --- esphome/components/display/display_buffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 672c6a22b0..791f7350e6 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -16,7 +16,7 @@ namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 255); +const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_ON(255, 255, 255, 255); void Rect::expand(int16_t horizontal, int16_t vertical) { From dec6f044996657864491383e10b442944cbb2298 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 23 Jun 2023 07:34:55 +1200 Subject: [PATCH 003/270] Bump version to 2023.6.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f49cff3b61..e06862072a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.0" +__version__ = "2023.6.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 595ac84779cc0401912289b8db054523f252125c Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 22 Jun 2023 18:03:31 -0700 Subject: [PATCH 004/270] remove unused static declarations (#4993) --- esphome/core/time.cpp | 20 ++++++++++---------- esphome/core/time.h | 4 ---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index c03506fd2a..bc5bfa173e 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -2,6 +2,16 @@ namespace esphome { +static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } + +static uint8_t days_in_month(uint8_t month, uint16_t year) { + static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint8_t days = DAYS_IN_MONTH[month]; + if (month == 2 && is_leap_year(year)) + return 29; + return days; +} + size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); return ::strftime(buffer, buffer_len, format, &c_tm); @@ -158,14 +168,4 @@ template bool increment_time_value(T ¤t, uint16_t begin, uint1 return false; } -static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } - -static uint8_t days_in_month(uint8_t month, uint16_t year) { - static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days = DAYS_IN_MONTH[month]; - if (month == 2 && is_leap_year(year)) - return 29; - return days; -} - } // namespace esphome diff --git a/esphome/core/time.h b/esphome/core/time.h index e1bdc8c839..e16e449f0b 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -8,10 +8,6 @@ namespace esphome { template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); -static bool is_leap_year(uint32_t year); - -static uint8_t days_in_month(uint8_t month, uint16_t year); - /// A more user-friendly version of struct tm from time.h struct ESPTime { /** seconds after the minute [0-60] From fbfb4e2a73320630a2518b6c1e8b2ed5e07e8d9e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:42:37 +1200 Subject: [PATCH 005/270] Fix rp2040 pio tool download (#4994) --- esphome/components/rp2040/__init__.py | 24 ++++------ esphome/components/rp2040/build_pio.py.script | 47 +++++++++++++++++++ esphome/components/rp2040_pio/__init__.py | 40 ++++++++++++++++ .../components/rp2040_pio_led_strip/light.py | 7 +-- 4 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 esphome/components/rp2040/build_pio.py.script create mode 100644 esphome/components/rp2040_pio/__init__.py diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index ad66ce6d18..030d586626 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -16,8 +16,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, ) from esphome.core import CORE, coroutine_with_priority, EsphomeError -from esphome.helpers import mkdir_p, write_file -import esphome.platformio_api as api +from esphome.helpers import mkdir_p, write_file, copy_file_if_changed from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -193,25 +192,20 @@ def generate_pio_files() -> bool: pio_path = CORE.relative_build_path(f"src/pio/{key}.pio") mkdir_p(os.path.dirname(pio_path)) write_file(pio_path, data) - _LOGGER.info("Assembling PIO assembly code") - retval = api.run_platformio_cli( - "pkg", - "exec", - "--package", - "earlephilhower/tool-pioasm-rp2040-earlephilhower", - "--", - "pioasm", - pio_path, - pio_path + ".h", - ) includes.append(f"pio/{key}.pio.h") - if retval != 0: - raise EsphomeError("PIO assembly failed") write_file( CORE.relative_build_path("src/pio_includes.h"), "#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]), ) + + dir = os.path.dirname(__file__) + build_pio_file = os.path.join(dir, "build_pio.py.script") + copy_file_if_changed( + build_pio_file, + CORE.relative_build_path("build_pio.py"), + ) + return True diff --git a/esphome/components/rp2040/build_pio.py.script b/esphome/components/rp2040/build_pio.py.script new file mode 100644 index 0000000000..c3e0767ed6 --- /dev/null +++ b/esphome/components/rp2040/build_pio.py.script @@ -0,0 +1,47 @@ +""" +Custom pioasm compiler script for platformio. +(c) 2022 by P.Z. + +Sourced 2023/06/23 from https://gist.github.com/hexeguitar/f4533bc697c956ac1245b6843e2ef438 + +Modified by jesserockz 2023/06/23 +""" + +from os.path import join +import glob +import sys + +import subprocess + +# pylint: disable=E0602 +Import("env") # noqa + +from SCons.Script import ARGUMENTS + + +platform = env.PioPlatform() +PROJ_SRC = env["PROJECT_SRC_DIR"] +PIO_FILES = glob.glob(join(PROJ_SRC, "**", "*.pio"), recursive=True) + +verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) + + +if PIO_FILES: + if verbose: + print("==============================================") + print("PIO ASSEMBLY COMPILER") + try: + PIOASM_DIR = platform.get_package_dir("tool-pioasm-rp2040-earlephilhower") + except: + print("tool-pioasm-rp2040-earlephilhower not supported on your system!") + sys.exit() + + PIOASM_EXE = join(PIOASM_DIR, "pioasm") + if verbose: + print("PIO files found:") + for filename in PIO_FILES: + if verbose: + print(f" {filename}") + subprocess.run([PIOASM_EXE, "-o", "c-sdk", filename, f"{filename}.h"]) + if verbose: + print("==============================================") diff --git a/esphome/components/rp2040_pio/__init__.py b/esphome/components/rp2040_pio/__init__.py new file mode 100644 index 0000000000..af884d5ac2 --- /dev/null +++ b/esphome/components/rp2040_pio/__init__.py @@ -0,0 +1,40 @@ +import platform + +import esphome.codegen as cg + + +DEPENDENCIES = ["rp2040"] + + +PIOASM_REPO_VERSION = "1.5.0-b" +PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}" +PIOASM_VERSION = "pioasm-2e6142b.230216" +PIOASM_DOWNLOADS = { + "linux": { + "aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz", + "armv7l": f"arm-linux-gnueabihf.{PIOASM_VERSION}.tar.gz", + "x86_64": f"x86_64-linux-gnu.{PIOASM_VERSION}.tar.gz", + }, + "windows": { + "amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip", + }, + "darwin": { + "x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz", + "arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz", + }, +} + + +async def to_code(config): + # cg.add_platformio_option( + # "platform_packages", + # [ + # "earlephilhower/tool-pioasm-rp2040-earlephilhower", + # ], + # ) + file = PIOASM_DOWNLOADS[platform.system().lower()][platform.machine().lower()] + cg.add_platformio_option( + "platform_packages", + [f"earlephilhower/tool-pioasm-rp2040-earlephilhower@{PIOASM_REPO_BASE}/{file}"], + ) + cg.add_platformio_option("extra_scripts", ["pre:build_pio.py"]) diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index a2ba72318f..6c51b57e97 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -127,6 +127,7 @@ def time_to_cycles(time_us): CONF_PIO = "pio" +AUTO_LOAD = ["rp2040_pio"] CODEOWNERS = ["@Papa-DMan"] DEPENDENCIES = ["rp2040"] @@ -265,9 +266,3 @@ async def to_code(config): time_to_cycles(config[CONF_BIT1_LOW]), ), ) - cg.add_platformio_option( - "platform_packages", - [ - "earlephilhower/tool-pioasm-rp2040-earlephilhower", - ], - ) From 39948db59af9e2467ecab13822124586da5c7388 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:17:43 +1200 Subject: [PATCH 006/270] Bump version to 2023.6.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e06862072a..48e62b9b86 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.1" +__version__ = "2023.6.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 604d4eec792b277e1a979aeaa34f2c6ef629d53d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 26 Jun 2023 10:27:03 +1200 Subject: [PATCH 007/270] Update webserver to 56d73b5 (#5007) --- esphome/components/web_server/server_index.h | 1179 +++++++++--------- 1 file changed, 590 insertions(+), 589 deletions(-) diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h index 4e6e136f8c..2dbb839c5e 100644 --- a/esphome/components/web_server/server_index.h +++ b/esphome/components/web_server/server_index.h @@ -6,596 +6,597 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xd9, 0x76, 0xe3, 0xc6, 0x92, 0xe0, 0xf3, - 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x28, 0x58, 0x5d, 0x05, 0x5c, 0x81, 0x10, 0x49, 0x95, 0xaa, 0xca, 0xa0, 0x40, 0x5e, - 0xd5, 0x62, 0x57, 0xd9, 0xb5, 0xb9, 0xa4, 0xb2, 0xaf, 0x2d, 0xeb, 0x4a, 0x10, 0x99, 0x14, 0xe1, 0x02, 0x01, 0x1a, - 0x48, 0x6a, 0x31, 0x85, 0x3e, 0xfd, 0xd4, 0x4f, 0x7d, 0xce, 0x6c, 0xfd, 0xd0, 0x0f, 0xd3, 0xa7, 0xfb, 0x61, 0x3e, - 0x62, 0x9e, 0xfb, 0x53, 0xee, 0x0f, 0x4c, 0x7f, 0xc2, 0x44, 0x44, 0x2e, 0x48, 0x80, 0xa4, 0x24, 0xbb, 0x7d, 0xe7, - 0x78, 0x11, 0x90, 0x6b, 0x44, 0x64, 0x64, 0x6c, 0x19, 0x09, 0xee, 0xde, 0x1b, 0x65, 0x43, 0x7e, 0x35, 0x63, 0xd6, - 0x84, 0x4f, 0x93, 0xfe, 0xae, 0xfc, 0x3f, 0x8b, 0x46, 0xfd, 0xdd, 0x24, 0x4e, 0x3f, 0x59, 0x39, 0x4b, 0xc2, 0x78, - 0x98, 0xa5, 0xd6, 0x24, 0x67, 0xe3, 0x70, 0x14, 0xf1, 0x28, 0x88, 0xa7, 0xd1, 0x19, 0xb3, 0xb6, 0xfa, 0xbb, 0x53, - 0xc6, 0x23, 0x6b, 0x38, 0x89, 0xf2, 0x82, 0xf1, 0xf0, 0xe3, 0xc1, 0x17, 0xad, 0x27, 0xfd, 0xdd, 0x62, 0x98, 0xc7, - 0x33, 0x6e, 0xe1, 0x90, 0xe1, 0x34, 0x1b, 0xcd, 0x13, 0xd6, 0x3f, 0x8f, 0x72, 0xeb, 0x05, 0x0b, 0xdf, 0x9d, 0xfe, - 0xc4, 0x86, 0xdc, 0x1f, 0xb1, 0x71, 0x9c, 0xb2, 0xf7, 0x79, 0x36, 0x63, 0x39, 0xbf, 0xf2, 0xf6, 0x57, 0x57, 0xc4, - 0xac, 0xf0, 0x9e, 0xe9, 0xaa, 0x33, 0xc6, 0xdf, 0x5d, 0xa4, 0xaa, 0xcf, 0x73, 0x26, 0x26, 0xc9, 0xf2, 0xc2, 0x8b, - 0xd7, 0xb4, 0xd9, 0xbf, 0x9a, 0x9e, 0x66, 0x49, 0xe1, 0x7d, 0xd2, 0xf5, 0xb3, 0x3c, 0xe3, 0x19, 0x82, 0xe5, 0x4f, - 0xa2, 0xc2, 0x68, 0xe9, 0xbd, 0x5b, 0xd1, 0x64, 0x26, 0x2b, 0x5f, 0x15, 0x2f, 0xd2, 0xf9, 0x94, 0xe5, 0xd1, 0x69, - 0xc2, 0xbc, 0x9c, 0x85, 0x0e, 0xf3, 0xb8, 0x17, 0xbb, 0x61, 0x9f, 0x5b, 0x71, 0x6a, 0xb1, 0xc1, 0x0b, 0x46, 0x25, - 0x0b, 0xa6, 0x5b, 0x05, 0xf7, 0xda, 0x1e, 0x90, 0x6b, 0x1c, 0x9f, 0xcd, 0xf5, 0xfb, 0x45, 0x1e, 0x73, 0xf5, 0x7c, - 0x1e, 0x25, 0x73, 0x16, 0xc4, 0xa5, 0x1b, 0xb0, 0x43, 0x7e, 0x14, 0xc6, 0xde, 0x27, 0x1a, 0x14, 0x86, 0x5c, 0x8c, - 0xb3, 0xdc, 0x41, 0x5a, 0xc5, 0x38, 0x36, 0xbf, 0xbe, 0x76, 0x78, 0xb8, 0x28, 0x5d, 0xf7, 0x13, 0xf3, 0x87, 0x51, - 0x92, 0x38, 0x38, 0xf1, 0xfd, 0xfb, 0x39, 0xce, 0x18, 0x7b, 0xfc, 0x30, 0x3e, 0x72, 0x7b, 0xf1, 0xd8, 0x89, 0x99, - 0x5b, 0xf5, 0xcb, 0xc6, 0x56, 0xcc, 0x1c, 0xee, 0xba, 0xef, 0xd6, 0xf7, 0xc9, 0x19, 0x9f, 0xe7, 0x00, 0x7b, 0xe9, - 0xbd, 0x53, 0x33, 0xef, 0x63, 0xfd, 0x33, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x11, 0x5e, 0xc4, 0xe9, 0x28, - 0xbb, 0xf0, 0xf7, 0x27, 0x11, 0xfc, 0xf9, 0x90, 0x65, 0xfc, 0xfe, 0x7d, 0xe7, 0x3c, 0x8b, 0x47, 0x56, 0x3b, 0x0c, - 0xcd, 0xca, 0xab, 0x67, 0xfb, 0xfb, 0xd7, 0xd7, 0x8d, 0x02, 0x3f, 0x8d, 0x78, 0x7c, 0xce, 0x44, 0x67, 0x00, 0xc0, - 0x86, 0xbf, 0x33, 0xce, 0x46, 0xfb, 0xfc, 0x2a, 0x81, 0x52, 0xc6, 0x78, 0x61, 0x03, 0x8e, 0xcf, 0xb3, 0x21, 0x90, - 0x2d, 0x35, 0x08, 0x0f, 0x4d, 0x73, 0x36, 0x4b, 0xa2, 0x21, 0xc3, 0x7a, 0x18, 0xa9, 0xea, 0x51, 0x35, 0xf2, 0xbe, - 0x0b, 0xc5, 0xf2, 0x3a, 0xae, 0x97, 0xb1, 0x30, 0x65, 0x17, 0xd6, 0x9b, 0x68, 0xd6, 0x1b, 0x26, 0x51, 0x51, 0x58, - 0x29, 0x5b, 0x10, 0x0a, 0xf9, 0x7c, 0x08, 0x0c, 0x42, 0x08, 0x2e, 0x80, 0x4c, 0x7c, 0x12, 0x17, 0xfe, 0xf1, 0xc6, - 0xb0, 0x28, 0x3e, 0xb0, 0x62, 0x9e, 0xf0, 0x8d, 0x10, 0xd6, 0x82, 0xdf, 0x0b, 0xc3, 0xef, 0x5c, 0x3e, 0xc9, 0xb3, - 0x0b, 0xeb, 0x45, 0x9e, 0x43, 0x73, 0x1b, 0xa6, 0x14, 0x0d, 0xac, 0x18, 0xc6, 0xca, 0xb8, 0xa5, 0x07, 0xc3, 0x05, - 0xf4, 0xad, 0x8f, 0x05, 0xb3, 0x4e, 0xe6, 0x69, 0x11, 0x8d, 0x19, 0x34, 0x3d, 0xb1, 0xb2, 0xdc, 0x3a, 0x81, 0x41, - 0x4f, 0x60, 0xc9, 0x0a, 0x0e, 0xbb, 0xc6, 0xb7, 0xdd, 0x1e, 0xcd, 0x05, 0x85, 0x07, 0xec, 0x92, 0x87, 0xac, 0x04, - 0xc6, 0xb4, 0x0a, 0x8d, 0x86, 0xe3, 0x2e, 0x12, 0x28, 0x60, 0x61, 0xc6, 0x90, 0x65, 0x1d, 0xb3, 0xb1, 0x5e, 0x9c, - 0x2f, 0xee, 0xdf, 0xd7, 0xb4, 0x06, 0x9a, 0x38, 0xd0, 0xb6, 0x68, 0xb4, 0xf5, 0x04, 0xe2, 0x35, 0x12, 0xb9, 0x1e, - 0xf3, 0x25, 0xf9, 0xf6, 0xaf, 0xd2, 0x61, 0x7d, 0x6c, 0xa8, 0x2c, 0x79, 0xb6, 0xcf, 0xf3, 0x38, 0x3d, 0x03, 0x20, - 0xe4, 0x4c, 0x66, 0x93, 0xb2, 0x14, 0x8b, 0xff, 0x9e, 0x85, 0x2c, 0xec, 0xe3, 0xe8, 0x29, 0x73, 0xec, 0x82, 0x7a, - 0xd8, 0x61, 0x88, 0xa4, 0x07, 0x06, 0x63, 0x03, 0x16, 0xb0, 0x4d, 0xdb, 0xf6, 0xbe, 0x73, 0xbd, 0x2b, 0xe4, 0x20, - 0xdf, 0xf7, 0x89, 0x7d, 0x45, 0xe7, 0x38, 0xec, 0x20, 0xd0, 0x7e, 0xc2, 0xd2, 0x33, 0x3e, 0x19, 0xb0, 0xc3, 0xf6, - 0x51, 0xc0, 0x01, 0xaa, 0xd1, 0x7c, 0xc8, 0x1c, 0xe4, 0x47, 0x2f, 0xc7, 0xed, 0xb3, 0xe9, 0xc0, 0x14, 0xb8, 0x30, - 0xf7, 0x08, 0xc7, 0xda, 0xd2, 0xb8, 0x8a, 0x45, 0x15, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x53, 0x96, 0x1b, 0x70, - 0xe8, 0x66, 0xbd, 0xda, 0x0a, 0xce, 0x61, 0x85, 0xa0, 0x9f, 0x35, 0x9e, 0xa7, 0x43, 0x1e, 0x83, 0xe0, 0xb2, 0x37, - 0x01, 0x5c, 0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0x64, 0x87, 0xf9, 0x66, 0xe7, 0xc8, 0x43, - 0x28, 0x35, 0xf1, 0x25, 0xe2, 0x31, 0x20, 0x58, 0x7a, 0x1f, 0x99, 0xde, 0x9e, 0x5f, 0x0c, 0x98, 0xbf, 0xcc, 0xc7, - 0x21, 0xf7, 0xa7, 0xd1, 0x0c, 0xb1, 0x61, 0xc4, 0x03, 0x51, 0x3a, 0x44, 0xe8, 0x6a, 0xeb, 0x82, 0x14, 0xf3, 0x2b, - 0x16, 0x70, 0x81, 0x20, 0xb0, 0x67, 0x5f, 0x44, 0xc3, 0x09, 0x6c, 0xf1, 0x8a, 0x70, 0x23, 0xb5, 0x1d, 0x86, 0x39, - 0x8b, 0x38, 0x7b, 0x91, 0x30, 0x7c, 0xc3, 0x15, 0x80, 0x9e, 0xb6, 0xeb, 0xe5, 0x6a, 0xdf, 0x25, 0x31, 0x7f, 0x9b, - 0xc1, 0x3c, 0x3d, 0xc1, 0x24, 0xc0, 0xc5, 0xf9, 0xfd, 0xfb, 0x31, 0xb2, 0xc8, 0x1e, 0x87, 0xd5, 0x3a, 0x9d, 0x73, - 0x58, 0xb7, 0x14, 0x5b, 0xd8, 0x40, 0x6d, 0x2f, 0xf6, 0x39, 0x10, 0xf1, 0x59, 0x96, 0x72, 0x18, 0x0e, 0xe0, 0xd5, - 0x1c, 0xe4, 0x47, 0xb3, 0x19, 0x4b, 0x47, 0xcf, 0x26, 0x71, 0x32, 0x02, 0x6a, 0x94, 0x80, 0x6f, 0xc2, 0x42, 0xc0, - 0x13, 0x90, 0x09, 0x6e, 0xc6, 0x88, 0x96, 0x0f, 0x19, 0x99, 0x87, 0xb6, 0xdd, 0x43, 0x09, 0x24, 0xb1, 0x40, 0x19, - 0x44, 0x0b, 0xf7, 0x01, 0x44, 0x7f, 0xe1, 0xf2, 0xcd, 0x30, 0xd6, 0xcb, 0x28, 0x09, 0xfc, 0x1e, 0x25, 0x0d, 0xd0, - 0x9f, 0x81, 0x0c, 0xec, 0xa1, 0xe0, 0xfa, 0x4a, 0x4a, 0x9d, 0x88, 0x29, 0x0c, 0x81, 0x00, 0x43, 0x94, 0x20, 0x92, - 0x06, 0xef, 0xb3, 0xe4, 0x6a, 0x1c, 0x27, 0xc9, 0xfe, 0x7c, 0x36, 0xcb, 0x72, 0xee, 0x7d, 0x1d, 0x2e, 0x78, 0x56, - 0xe1, 0x4a, 0x9b, 0xbc, 0xb8, 0x88, 0x39, 0x12, 0xd4, 0x5d, 0x0c, 0x23, 0x58, 0xea, 0xa7, 0x59, 0x96, 0xb0, 0x28, - 0x05, 0x34, 0xd8, 0xc0, 0xb6, 0x83, 0x74, 0x9e, 0x24, 0xbd, 0x53, 0x18, 0xf6, 0x53, 0x8f, 0xaa, 0x85, 0xc4, 0x0f, - 0xe8, 0x79, 0x2f, 0xcf, 0xa3, 0x2b, 0x68, 0x88, 0x6d, 0x80, 0x17, 0x61, 0xb5, 0xbe, 0xda, 0x7f, 0xf7, 0xd6, 0x17, - 0x8c, 0x1f, 0x8f, 0xaf, 0x00, 0xd0, 0xb2, 0x92, 0x9a, 0xe3, 0x3c, 0x9b, 0x36, 0xa6, 0x46, 0x3a, 0xc4, 0x21, 0xeb, - 0xad, 0x01, 0x21, 0xa6, 0x91, 0x61, 0x95, 0x98, 0x09, 0xc1, 0x5b, 0xe2, 0x67, 0x59, 0x89, 0x7b, 0x60, 0x80, 0x0f, - 0x81, 0x28, 0x86, 0x29, 0x6f, 0x86, 0x96, 0xe7, 0x57, 0x8b, 0x38, 0x24, 0x38, 0x67, 0xa8, 0x7f, 0x11, 0xc6, 0x61, - 0x04, 0xb3, 0x2f, 0xc4, 0x80, 0xa5, 0x82, 0x38, 0x2e, 0x4b, 0x6f, 0xa2, 0x99, 0x18, 0x25, 0x1e, 0x0a, 0x14, 0x0e, - 0xdb, 0xe8, 0xfa, 0x9a, 0xc1, 0x8b, 0xeb, 0x7d, 0x13, 0x2e, 0x22, 0x85, 0x0f, 0x6a, 0x28, 0xdc, 0x5f, 0x81, 0x90, - 0x13, 0xa8, 0xc9, 0xce, 0x41, 0x0f, 0x02, 0x9c, 0x5f, 0x83, 0xfa, 0x1b, 0x27, 0x08, 0xc5, 0xbd, 0x8e, 0x07, 0x1a, - 0xf4, 0xd9, 0x24, 0x4a, 0xcf, 0xd8, 0x28, 0x98, 0xb0, 0x52, 0x4a, 0xde, 0x3d, 0x0b, 0xd6, 0x18, 0xd8, 0xa9, 0xb0, - 0x5e, 0x1e, 0xbc, 0x79, 0x2d, 0x57, 0xae, 0x26, 0x8c, 0x61, 0x91, 0xe6, 0xa0, 0x56, 0x41, 0x6c, 0x4b, 0x71, 0xfc, - 0x82, 0x2b, 0xe9, 0x2d, 0x4a, 0xe2, 0xe2, 0xe3, 0x0c, 0x4c, 0x0c, 0xf6, 0x1e, 0x86, 0x81, 0xe9, 0x43, 0x98, 0x8a, - 0xca, 0x61, 0x3e, 0x51, 0x31, 0xd2, 0x45, 0xd0, 0x59, 0x60, 0x2a, 0x5e, 0x33, 0xc7, 0x2d, 0x81, 0x55, 0x79, 0x3c, - 0xb4, 0xa2, 0xd1, 0xe8, 0x55, 0x1a, 0xf3, 0x38, 0x4a, 0xe2, 0x5f, 0x88, 0x92, 0x0b, 0xe4, 0x31, 0xde, 0x93, 0x8b, - 0x00, 0xb8, 0x53, 0x8f, 0xc4, 0x55, 0x42, 0xf6, 0x1e, 0x11, 0x43, 0x48, 0xcb, 0x24, 0x3c, 0x3c, 0x92, 0xe0, 0x25, - 0xfe, 0x6c, 0x5e, 0x4c, 0x90, 0xb0, 0x72, 0x60, 0x14, 0xe4, 0xd9, 0x69, 0xc1, 0xf2, 0x73, 0x36, 0xd2, 0x1c, 0x50, - 0x00, 0x56, 0xd4, 0x1c, 0x8c, 0x17, 0x9a, 0xd1, 0x51, 0x3a, 0x94, 0xc1, 0x50, 0x3d, 0x53, 0xcc, 0x32, 0xc9, 0xcc, - 0xda, 0xc2, 0xd1, 0x52, 0xc0, 0x11, 0x46, 0x85, 0x94, 0x04, 0x79, 0xa8, 0x30, 0x9c, 0x80, 0x14, 0x02, 0xad, 0x60, - 0x6e, 0x73, 0xa5, 0xc9, 0x5e, 0xcc, 0x49, 0x25, 0xe4, 0xd0, 0x11, 0x36, 0x32, 0x41, 0x9a, 0xbb, 0xb0, 0xab, 0x40, - 0xca, 0x4b, 0x70, 0x85, 0x14, 0x51, 0x66, 0x0e, 0x32, 0x40, 0xf8, 0x8d, 0xd0, 0x85, 0x3e, 0xb6, 0x20, 0x36, 0xf0, - 0xf5, 0xca, 0x03, 0x61, 0x25, 0xde, 0x15, 0x22, 0xde, 0x1a, 0xb0, 0x71, 0x62, 0xe4, 0x27, 0xef, 0x1e, 0xf7, 0xd3, - 0x6c, 0x6f, 0x38, 0x64, 0x45, 0x91, 0x01, 0x6c, 0xf7, 0xa8, 0xfd, 0x3a, 0x43, 0x0b, 0x28, 0xe9, 0x6a, 0x59, 0x67, - 0x17, 0xa4, 0xc1, 0x4d, 0xb5, 0xa2, 0x74, 0x7a, 0x60, 0x1f, 0x1f, 0x83, 0xcc, 0xf6, 0x24, 0x19, 0x80, 0xea, 0xcb, - 0x86, 0x9f, 0xb0, 0x67, 0xea, 0x94, 0x59, 0x69, 0x5f, 0x3a, 0x75, 0x90, 0x3c, 0x18, 0xd6, 0x2d, 0x8d, 0x05, 0x5d, - 0x39, 0x34, 0xae, 0x86, 0x54, 0x90, 0x8b, 0x33, 0x52, 0xd9, 0xc6, 0x32, 0x82, 0xd5, 0x56, 0x7a, 0x44, 0x7a, 0x85, - 0x4d, 0x41, 0x80, 0x1e, 0xb2, 0xa3, 0x9e, 0xac, 0x0f, 0x73, 0x41, 0xb9, 0x9c, 0xfd, 0x3c, 0x67, 0x05, 0x17, 0xac, - 0x0b, 0xe3, 0x82, 0xb9, 0x0a, 0x22, 0xb6, 0x69, 0x1d, 0xd6, 0x6c, 0xc7, 0x55, 0xb0, 0xbd, 0x9b, 0xa1, 0x1e, 0x2b, - 0x90, 0x93, 0x6f, 0x66, 0x27, 0x84, 0x95, 0xb9, 0xd7, 0xd7, 0xdf, 0xa8, 0x41, 0xaa, 0xa5, 0xd4, 0x36, 0x50, 0x63, - 0x4d, 0x6c, 0xd5, 0x64, 0x64, 0xbb, 0x52, 0xa1, 0xde, 0xeb, 0xf4, 0x6a, 0x7c, 0x00, 0x7b, 0xae, 0xad, 0x59, 0xba, - 0x32, 0xb6, 0xdf, 0x2b, 0x9a, 0xbe, 0x13, 0x23, 0x93, 0x35, 0xca, 0x6e, 0xe7, 0x1e, 0xb5, 0xe3, 0xa1, 0xed, 0x52, - 0x5d, 0x25, 0x18, 0xe6, 0x75, 0xc1, 0xd0, 0x84, 0x7a, 0xa6, 0xbb, 0xd8, 0x9a, 0xa9, 0x58, 0xa8, 0xd6, 0x5a, 0x39, - 0x10, 0x3c, 0x3c, 0x04, 0xe3, 0x64, 0xa5, 0x7f, 0xf0, 0x36, 0x9a, 0x32, 0xa4, 0xa8, 0xb7, 0xae, 0x81, 0x74, 0x20, - 0xa0, 0xc9, 0x51, 0x53, 0xbd, 0x71, 0x57, 0x58, 0x4d, 0xf5, 0xfd, 0x15, 0x83, 0x15, 0x01, 0xf6, 0x75, 0xb9, 0x62, - 0x89, 0x48, 0x6f, 0x0a, 0x2e, 0xd1, 0xf4, 0x11, 0x65, 0x62, 0x4d, 0x48, 0xc1, 0x03, 0xf2, 0xb0, 0xfc, 0x8d, 0x85, - 0x93, 0xad, 0x98, 0xc2, 0x91, 0xa3, 0x4c, 0x01, 0x3a, 0x93, 0x12, 0x00, 0x71, 0x49, 0x7f, 0x6b, 0x1b, 0x0b, 0xc9, - 0xb6, 0x8f, 0x7c, 0xe0, 0x8f, 0x93, 0x88, 0x3b, 0x9d, 0xad, 0xb6, 0x0b, 0x7c, 0x08, 0x42, 0x1c, 0x74, 0x04, 0x98, - 0xf7, 0x15, 0x2a, 0x8c, 0xbc, 0x05, 0x97, 0xfb, 0x60, 0x14, 0x4d, 0xe2, 0x31, 0x77, 0x12, 0x54, 0x22, 0x6e, 0xc9, - 0x12, 0x50, 0x32, 0x7a, 0x5f, 0x81, 0x94, 0xe0, 0x42, 0xba, 0x88, 0x6a, 0x2d, 0xd0, 0x14, 0xa4, 0x24, 0xa5, 0x48, - 0x0b, 0x2a, 0x08, 0x0c, 0xa1, 0xd2, 0x53, 0x1c, 0x05, 0xfa, 0x2d, 0x1e, 0x88, 0x41, 0x83, 0x25, 0x8b, 0x32, 0x1e, - 0xc4, 0xcb, 0x85, 0xa0, 0x86, 0x7d, 0x9e, 0xbd, 0xce, 0x2e, 0x58, 0xfe, 0x2c, 0x42, 0xd8, 0x03, 0xd1, 0xbd, 0x04, - 0x49, 0x4f, 0x02, 0x9d, 0xf5, 0x14, 0xaf, 0x9c, 0x13, 0xd2, 0xb0, 0x10, 0xd3, 0x18, 0x15, 0x21, 0x68, 0x39, 0xa2, - 0x7d, 0x8a, 0x5b, 0x8a, 0xf6, 0x1e, 0xaa, 0x12, 0xa6, 0x79, 0x6b, 0xef, 0x75, 0x9d, 0xb7, 0x60, 0x84, 0x99, 0xe2, - 0xd6, 0xfa, 0x8e, 0x75, 0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x7d, 0x5d, 0x19, 0xe9, - 0xa0, 0x4c, 0xb5, 0x34, 0x47, 0xcb, 0x49, 0x6c, 0x09, 0xb7, 0x04, 0x65, 0x84, 0x86, 0x57, 0x9e, 0x25, 0x89, 0xa1, - 0x8b, 0xbc, 0xb8, 0xe7, 0x34, 0xd4, 0x11, 0x40, 0x31, 0xad, 0x69, 0xa4, 0x01, 0x0f, 0x74, 0x05, 0x2a, 0x25, 0xa5, - 0x8d, 0xbc, 0xaa, 0x89, 0x80, 0x38, 0x1d, 0xb1, 0x5c, 0x38, 0x68, 0x52, 0x87, 0xc2, 0x84, 0x29, 0x30, 0x34, 0x1b, - 0x81, 0x84, 0x57, 0x08, 0x80, 0x79, 0xe2, 0x4f, 0xb2, 0x82, 0xeb, 0x3a, 0x13, 0xfa, 0xf8, 0xfa, 0x3a, 0x16, 0xfe, - 0x22, 0x32, 0x40, 0xce, 0xa6, 0xd9, 0x39, 0x5b, 0x01, 0x75, 0x4f, 0x0d, 0x66, 0x82, 0x6c, 0x0c, 0x03, 0x4a, 0x14, - 0x54, 0xcb, 0x2c, 0x89, 0x87, 0x4c, 0x6b, 0xa9, 0xa9, 0x0f, 0x06, 0x1d, 0xbb, 0x04, 0x19, 0xc1, 0xdc, 0x7e, 0xbf, - 0xdf, 0xf6, 0x3a, 0x6e, 0x29, 0x08, 0xbe, 0x58, 0xa2, 0xe8, 0x0d, 0xfa, 0x51, 0x9a, 0xe0, 0xab, 0x64, 0x01, 0x77, - 0x0d, 0xa5, 0xc8, 0x85, 0x9f, 0xe4, 0x49, 0x41, 0xec, 0x7a, 0x23, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xdb, 0x76, 0xe3, 0x46, 0x92, 0xe0, 0xf3, + 0x9e, 0xb3, 0x7f, 0xb0, 0x2f, 0x28, 0x58, 0x53, 0x05, 0xb4, 0x40, 0x88, 0xa4, 0x4a, 0x55, 0x65, 0x50, 0x20, 0x5b, + 0x75, 0xb1, 0xab, 0xec, 0xba, 0xb9, 0xa4, 0xb2, 0xdb, 0x96, 0xd5, 0x12, 0x44, 0x26, 0x45, 0xb8, 0x40, 0x80, 0x06, + 0x92, 0xba, 0x98, 0xc2, 0x9c, 0x79, 0x9a, 0xa7, 0x39, 0x67, 0x6f, 0xf3, 0x30, 0x0f, 0x3b, 0x67, 0xe6, 0x61, 0x3f, + 0x62, 0x9f, 0xe7, 0x53, 0xfa, 0x07, 0x76, 0x3e, 0x61, 0x23, 0x22, 0x2f, 0x48, 0x80, 0xa4, 0x24, 0x7b, 0xdc, 0x7b, + 0xdc, 0xd5, 0x02, 0xf2, 0x1a, 0x11, 0x19, 0x19, 0xb7, 0x8c, 0x04, 0x77, 0xef, 0x8d, 0xb2, 0x21, 0xbf, 0x9a, 0x31, + 0x6b, 0xc2, 0xa7, 0x49, 0x7f, 0x57, 0xfe, 0x3f, 0x8b, 0x46, 0xfd, 0xdd, 0x24, 0x4e, 0x3f, 0x59, 0x39, 0x4b, 0xc2, + 0x78, 0x98, 0xa5, 0xd6, 0x24, 0x67, 0xe3, 0x70, 0x14, 0xf1, 0x28, 0x88, 0xa7, 0xd1, 0x19, 0xb3, 0xb6, 0xfa, 0xbb, + 0x53, 0xc6, 0x23, 0x6b, 0x38, 0x89, 0xf2, 0x82, 0xf1, 0xf0, 0xe3, 0xc1, 0x17, 0xad, 0x27, 0xfd, 0xdd, 0x62, 0x98, + 0xc7, 0x33, 0x6e, 0xe1, 0x90, 0xe1, 0x34, 0x1b, 0xcd, 0x13, 0xd6, 0x3f, 0x8f, 0x72, 0xeb, 0x05, 0x0b, 0xdf, 0x9d, + 0xfe, 0xc4, 0x86, 0xdc, 0x1f, 0xb1, 0x71, 0x9c, 0xb2, 0xf7, 0x79, 0x36, 0x63, 0x39, 0xbf, 0xf2, 0xf6, 0x57, 0x57, + 0xc4, 0xac, 0xf0, 0x9e, 0xe9, 0xaa, 0x33, 0xc6, 0xdf, 0x5d, 0xa4, 0xaa, 0xcf, 0x73, 0x26, 0x26, 0xc9, 0xf2, 0xc2, + 0x8b, 0xd7, 0xb4, 0xd9, 0xbf, 0x9a, 0x9e, 0x66, 0x49, 0xe1, 0x7d, 0xd2, 0xf5, 0xb3, 0x3c, 0xe3, 0x19, 0x82, 0xe5, + 0x4f, 0xa2, 0xc2, 0x68, 0xe9, 0xbd, 0x5b, 0xd1, 0x64, 0x26, 0x2b, 0x5f, 0x15, 0x2f, 0xd2, 0xf9, 0x94, 0xe5, 0xd1, + 0x69, 0xc2, 0xbc, 0x9c, 0x85, 0x0e, 0xf7, 0x98, 0x17, 0xbb, 0x61, 0x9f, 0x59, 0x71, 0x6a, 0xf1, 0xc1, 0x0b, 0x46, + 0x25, 0x0b, 0xa6, 0x5b, 0x05, 0xf7, 0xda, 0x1e, 0x90, 0x6b, 0x1c, 0x9f, 0xcd, 0xf5, 0xfb, 0x45, 0x1e, 0x73, 0xf5, + 0x7c, 0x1e, 0x25, 0x73, 0x16, 0xc4, 0xa5, 0x1b, 0xf0, 0x43, 0x76, 0x14, 0xc6, 0xde, 0x27, 0x1a, 0x14, 0x86, 0x5c, + 0x8c, 0xb3, 0xdc, 0x41, 0x5a, 0xc5, 0x38, 0x36, 0xbb, 0xbe, 0x76, 0x58, 0xb8, 0x28, 0x5d, 0xf7, 0x13, 0xf3, 0x87, + 0x51, 0x92, 0x38, 0x38, 0xf1, 0xfd, 0xfb, 0x39, 0xce, 0x18, 0x7b, 0xec, 0x30, 0x3e, 0x72, 0x7b, 0xf1, 0xd8, 0x89, + 0x99, 0x5b, 0xf5, 0xcb, 0xc6, 0x56, 0xcc, 0x1c, 0xe6, 0xba, 0xef, 0xd6, 0xf7, 0xc9, 0x19, 0x9f, 0xe7, 0x00, 0x7b, + 0xe9, 0xbd, 0x53, 0x33, 0xef, 0x63, 0xfd, 0x33, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x11, 0x5e, 0xc4, 0xe9, + 0x28, 0xbb, 0xf0, 0xf7, 0x27, 0x11, 0xfc, 0xf9, 0x90, 0x65, 0xfc, 0xfe, 0x7d, 0xe7, 0x3c, 0x8b, 0x47, 0x56, 0x3b, + 0x0c, 0xcd, 0xca, 0xab, 0x67, 0xfb, 0xfb, 0xd7, 0xd7, 0x8d, 0x02, 0x3f, 0x8d, 0x78, 0x7c, 0xce, 0x44, 0x67, 0x00, + 0xc0, 0x86, 0xbf, 0x33, 0xce, 0x46, 0xfb, 0xfc, 0x2a, 0x81, 0x52, 0xc6, 0x78, 0x61, 0x03, 0x8e, 0xcf, 0xb3, 0x21, + 0x90, 0x2d, 0x35, 0x08, 0x0f, 0x4d, 0x73, 0x36, 0x4b, 0xa2, 0x21, 0xc3, 0x7a, 0x18, 0xa9, 0xea, 0x51, 0x35, 0xf2, + 0xbe, 0x0b, 0xc5, 0xf2, 0x3a, 0xae, 0x97, 0xb1, 0x30, 0x65, 0x17, 0xd6, 0x9b, 0x68, 0xd6, 0x1b, 0x26, 0x51, 0x51, + 0x58, 0x29, 0x5b, 0x10, 0x0a, 0xf9, 0x7c, 0x08, 0x0c, 0x42, 0x08, 0x2e, 0x80, 0x4c, 0x7c, 0x12, 0x17, 0xfe, 0xf1, + 0xc6, 0xb0, 0x28, 0x3e, 0xb0, 0x62, 0x9e, 0xf0, 0x8d, 0x10, 0xd6, 0x82, 0xdd, 0x0b, 0xc3, 0xef, 0x5c, 0x3e, 0xc9, + 0xb3, 0x0b, 0xeb, 0x45, 0x9e, 0x43, 0x73, 0x1b, 0xa6, 0x14, 0x0d, 0xac, 0x18, 0xc6, 0xca, 0xb8, 0xa5, 0x07, 0xc3, + 0x05, 0xf4, 0xad, 0x8f, 0x05, 0xb3, 0x4e, 0xe6, 0x69, 0x11, 0x8d, 0x19, 0x34, 0x3d, 0xb1, 0xb2, 0xdc, 0x3a, 0x81, + 0x41, 0x4f, 0x60, 0xc9, 0x0a, 0x0e, 0xbb, 0xc6, 0xb7, 0xdd, 0x1e, 0xcd, 0x05, 0x85, 0x07, 0xec, 0x92, 0x87, 0xbc, + 0x04, 0xc6, 0xb4, 0x0a, 0x8d, 0x86, 0xe3, 0x2e, 0x12, 0x28, 0xe0, 0x61, 0xc6, 0x90, 0x65, 0x1d, 0xb3, 0xb1, 0x5e, + 0x9c, 0x2f, 0xee, 0xdf, 0xd7, 0xb4, 0x46, 0xc2, 0x43, 0xdb, 0xa2, 0xd1, 0xd6, 0xe3, 0x84, 0x78, 0x8d, 0x44, 0xae, + 0xc7, 0x7d, 0x49, 0xbe, 0xfd, 0xab, 0x74, 0x58, 0x1f, 0x1b, 0x2a, 0x4b, 0x9e, 0xed, 0xf3, 0x3c, 0x4e, 0xcf, 0x00, + 0x08, 0xc5, 0x06, 0x46, 0x93, 0xb2, 0x14, 0x8b, 0xff, 0x9e, 0x85, 0x3c, 0xec, 0xe3, 0xe8, 0x29, 0x73, 0xec, 0x82, + 0x7a, 0xd8, 0x00, 0x08, 0x90, 0x1e, 0x18, 0x8c, 0x0f, 0x78, 0xc0, 0x37, 0x6d, 0xdb, 0xfb, 0xce, 0xf5, 0xae, 0x90, + 0x83, 0x7c, 0xdf, 0x27, 0xf6, 0x15, 0x9d, 0xe3, 0xb0, 0x83, 0x40, 0xfb, 0x09, 0x4b, 0xcf, 0xf8, 0x64, 0xc0, 0x0f, + 0xdb, 0x47, 0x01, 0x03, 0xa8, 0x46, 0xf3, 0x21, 0x73, 0x90, 0x1f, 0xbd, 0x1c, 0xb7, 0xcf, 0xa6, 0x03, 0x53, 0xe0, + 0xc2, 0xdc, 0x23, 0x1c, 0x6b, 0x4b, 0xe3, 0x2a, 0xd8, 0x14, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x53, 0x96, 0x1b, + 0x70, 0xe8, 0x66, 0xbd, 0xda, 0x0a, 0xce, 0x61, 0x85, 0xa0, 0x9f, 0x35, 0x9e, 0xa7, 0x43, 0x1e, 0x83, 0xe0, 0xb2, + 0x37, 0x01, 0x5c, 0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0xe4, 0x87, 0xf9, 0x66, 0xe7, 0xc8, + 0x43, 0x28, 0x35, 0xf1, 0x25, 0xe2, 0x31, 0x20, 0x58, 0x7a, 0x1f, 0x99, 0xde, 0x9e, 0x5f, 0x0c, 0xb8, 0xbf, 0xcc, + 0xc7, 0x21, 0xf3, 0xa7, 0xd1, 0x0c, 0xb1, 0xe1, 0xc4, 0x03, 0x51, 0x3a, 0x44, 0xe8, 0x6a, 0xeb, 0x82, 0x14, 0xf3, + 0x2b, 0x16, 0x70, 0x81, 0x20, 0xb0, 0x67, 0x5f, 0x44, 0xc3, 0x09, 0x6c, 0xf1, 0x8a, 0x70, 0x23, 0xb5, 0x1d, 0x86, + 0x39, 0x8b, 0x38, 0x7b, 0x91, 0x30, 0x7c, 0xc3, 0x15, 0x80, 0x9e, 0xb6, 0xeb, 0xe5, 0x6a, 0xdf, 0x25, 0x31, 0x7f, + 0x9b, 0xc1, 0x3c, 0x3d, 0xc1, 0x24, 0xc0, 0xc5, 0xf9, 0xfd, 0xfb, 0x31, 0xb2, 0xc8, 0x1e, 0x87, 0xd5, 0x3a, 0x9d, + 0x73, 0x58, 0xb7, 0x14, 0x5b, 0xd8, 0x40, 0x6d, 0x2f, 0xf6, 0x39, 0x10, 0xf1, 0x59, 0x96, 0x72, 0x18, 0x0e, 0xe0, + 0xd5, 0x1c, 0xe4, 0x47, 0xb3, 0x19, 0x4b, 0x47, 0xcf, 0x26, 0x71, 0x32, 0x02, 0x6a, 0x94, 0x80, 0x6f, 0xc2, 0x42, + 0xc0, 0x13, 0x90, 0x09, 0x6e, 0xc6, 0x88, 0x96, 0x0f, 0x19, 0x99, 0x85, 0xb6, 0xdd, 0x43, 0x09, 0x24, 0xb1, 0x40, + 0x19, 0x44, 0x0b, 0xf7, 0x01, 0x44, 0x7f, 0xe1, 0xb2, 0xcd, 0x30, 0xd6, 0xcb, 0x28, 0x09, 0xfc, 0x1e, 0x25, 0x0d, + 0xd0, 0x1f, 0x08, 0xc1, 0x7b, 0x28, 0xb8, 0xbe, 0x92, 0x52, 0x27, 0x62, 0x0a, 0x43, 0x20, 0xc0, 0x10, 0x25, 0x88, + 0xa4, 0xc1, 0xfb, 0x2c, 0xb9, 0x1a, 0xc7, 0x49, 0xb2, 0x3f, 0x9f, 0xcd, 0xb2, 0x9c, 0x7b, 0x5f, 0x87, 0x0b, 0x9e, + 0x55, 0xb8, 0xd2, 0x26, 0x2f, 0x2e, 0x62, 0x8e, 0x04, 0x75, 0x17, 0xc3, 0x08, 0x96, 0xfa, 0x69, 0x96, 0x25, 0x2c, + 0x4a, 0x01, 0x0d, 0x3e, 0xb0, 0xed, 0x20, 0x9d, 0x27, 0x49, 0xef, 0x14, 0x86, 0xfd, 0xd4, 0xa3, 0x6a, 0x21, 0xf1, + 0x03, 0x7a, 0xde, 0xcb, 0xf3, 0xe8, 0x0a, 0x1a, 0x62, 0x1b, 0x60, 0x2f, 0x58, 0xad, 0xaf, 0xf6, 0xdf, 0xbd, 0xf5, + 0x05, 0xe3, 0xc7, 0xe3, 0x2b, 0x00, 0xb4, 0xac, 0xa4, 0xe6, 0x38, 0xcf, 0xa6, 0x8d, 0xa9, 0x91, 0x0e, 0x71, 0xc8, + 0x7b, 0x6b, 0x40, 0x88, 0x69, 0x64, 0x58, 0x25, 0x6e, 0x42, 0xf0, 0x96, 0xf8, 0x59, 0x56, 0xe2, 0x1e, 0x18, 0xe0, + 0x43, 0x20, 0x8a, 0x61, 0xca, 0x5b, 0xa0, 0xcd, 0xaf, 0x16, 0x71, 0x48, 0x70, 0xce, 0x50, 0xff, 0x22, 0x8c, 0xc3, + 0x08, 0x66, 0x5f, 0x88, 0x01, 0x4b, 0x05, 0x71, 0x5c, 0x96, 0xde, 0x44, 0x33, 0x31, 0x4a, 0x3c, 0x14, 0x28, 0x2c, + 0x0c, 0x41, 0xc1, 0x70, 0x78, 0x71, 0xbd, 0x6f, 0xc2, 0x45, 0xa4, 0xf0, 0x41, 0x0d, 0x85, 0xfb, 0x2b, 0x10, 0x72, + 0x02, 0x35, 0xd9, 0x39, 0xe8, 0x41, 0x80, 0xf3, 0x6b, 0x50, 0x7f, 0xe3, 0x04, 0xa1, 0xb8, 0xd7, 0xf1, 0x40, 0x83, + 0x3e, 0x9b, 0x44, 0xe9, 0x19, 0x1b, 0x05, 0x13, 0x56, 0x4a, 0xc9, 0xbb, 0x67, 0xc1, 0x1a, 0x03, 0x3b, 0x15, 0xd6, + 0xcb, 0x83, 0x37, 0xaf, 0xe5, 0xca, 0xd5, 0x84, 0x31, 0x2c, 0xd2, 0x1c, 0xd4, 0x2a, 0x88, 0x6d, 0x29, 0x8e, 0x5f, + 0x70, 0x25, 0xbd, 0x45, 0x49, 0x5c, 0x7c, 0x9c, 0x81, 0x89, 0xc1, 0xde, 0xc3, 0x30, 0x30, 0x7d, 0x08, 0x53, 0x51, + 0x39, 0xcc, 0x27, 0x2a, 0x46, 0xba, 0x08, 0x3a, 0x0b, 0x4c, 0xc5, 0x6b, 0xe6, 0xb8, 0x25, 0xb0, 0x2a, 0x8f, 0x87, + 0x56, 0x34, 0x1a, 0xbd, 0x4a, 0x63, 0x1e, 0x47, 0x49, 0xfc, 0x0b, 0x51, 0x72, 0x81, 0x3c, 0xc6, 0x7a, 0x72, 0x11, + 0x00, 0x77, 0xea, 0x91, 0xb8, 0x4a, 0xc8, 0xde, 0x23, 0x62, 0x08, 0x69, 0x99, 0x84, 0x87, 0x47, 0x12, 0xbc, 0xc4, + 0x9f, 0xcd, 0x8b, 0x09, 0x12, 0x56, 0x0e, 0x8c, 0x82, 0x3c, 0x3b, 0x2d, 0x58, 0x7e, 0xce, 0x46, 0x9a, 0x03, 0x0a, + 0xc0, 0x8a, 0x9a, 0x83, 0xf1, 0x42, 0x33, 0x3a, 0x4a, 0x87, 0x72, 0x18, 0xaa, 0x67, 0x8a, 0x59, 0x26, 0x99, 0x59, + 0x5b, 0x38, 0x5a, 0x0a, 0x38, 0xc2, 0xa8, 0x90, 0x92, 0x20, 0x0f, 0x15, 0x86, 0x13, 0x90, 0x42, 0xcc, 0xad, 0x6d, + 0x73, 0xa5, 0xc9, 0x5e, 0xcc, 0x49, 0x25, 0xe4, 0xd0, 0x11, 0x36, 0x32, 0x41, 0x9a, 0xbb, 0xb0, 0xab, 0x40, 0xca, + 0x4b, 0x70, 0x85, 0x14, 0x51, 0x66, 0x0e, 0x32, 0x40, 0xf8, 0x0d, 0xe9, 0x42, 0x50, 0x26, 0xd0, 0x82, 0x21, 0x1b, + 0xf8, 0x7a, 0xe5, 0x81, 0xb0, 0x12, 0xef, 0x0a, 0x11, 0x6f, 0x0d, 0xd8, 0xa4, 0x8b, 0x00, 0x30, 0xef, 0x1e, 0xf3, + 0xd3, 0x6c, 0x6f, 0x38, 0x64, 0x45, 0x91, 0x01, 0x6c, 0xf7, 0xa8, 0xfd, 0x3a, 0x43, 0x0b, 0x28, 0xe9, 0x6a, 0x59, + 0x67, 0x17, 0xa4, 0xc1, 0x4d, 0xb5, 0xa2, 0x74, 0x7a, 0x60, 0x1f, 0x1f, 0x83, 0xcc, 0xf6, 0x24, 0x19, 0x80, 0xea, + 0xcb, 0x86, 0x9f, 0xb0, 0x67, 0xea, 0x94, 0x59, 0x69, 0x5f, 0x3a, 0x75, 0x90, 0x3c, 0x18, 0xd6, 0x2d, 0x8d, 0x05, + 0x5d, 0x39, 0x34, 0xae, 0x86, 0x54, 0x90, 0x8b, 0x33, 0x52, 0xd9, 0xc6, 0x32, 0x82, 0xd5, 0x56, 0x7a, 0x44, 0x7a, + 0x85, 0x4d, 0x41, 0x80, 0x1e, 0xf2, 0xa3, 0x9e, 0xac, 0x0f, 0x73, 0x41, 0xb9, 0x9c, 0xfd, 0x3c, 0x67, 0x05, 0x17, + 0xac, 0x0b, 0xe3, 0x82, 0xb9, 0x0a, 0x22, 0xb6, 0x69, 0x1d, 0xd6, 0x6c, 0xc7, 0x55, 0xb0, 0xbd, 0x9b, 0xa1, 0x1e, + 0x2b, 0x90, 0x93, 0x6f, 0x66, 0x27, 0xb2, 0x27, 0xdc, 0xeb, 0xeb, 0x6f, 0xd4, 0x20, 0xd5, 0x52, 0x6a, 0x1b, 0xa8, + 0xb1, 0x26, 0xb6, 0x6a, 0x32, 0xb2, 0x5d, 0xa9, 0x50, 0xef, 0x75, 0x7a, 0x35, 0x3e, 0x80, 0x3d, 0xd7, 0xd6, 0x2c, + 0x5d, 0x19, 0xdb, 0xef, 0x15, 0x4d, 0xdf, 0x89, 0x91, 0xc9, 0x1a, 0xe5, 0xb7, 0x73, 0x8f, 0xda, 0xf1, 0xd0, 0x76, + 0xa9, 0xae, 0x12, 0x0c, 0xf3, 0xba, 0x60, 0x68, 0x42, 0x3d, 0xd3, 0x5d, 0x6c, 0xcd, 0x54, 0x3c, 0x54, 0x6b, 0xad, + 0x1c, 0x08, 0x16, 0x1e, 0x82, 0x71, 0xb2, 0xd2, 0x3f, 0x78, 0x1b, 0x4d, 0x19, 0x52, 0xd4, 0x5b, 0xd7, 0x40, 0x3a, + 0x10, 0xd0, 0xe4, 0xa8, 0xa9, 0xde, 0x98, 0x2b, 0xac, 0xa6, 0xfa, 0xfe, 0x8a, 0xc1, 0x8a, 0x00, 0xfb, 0xba, 0x5c, + 0xb1, 0x44, 0xa4, 0x37, 0x05, 0x97, 0x68, 0xfa, 0x88, 0x32, 0xb1, 0x26, 0xa4, 0xe0, 0x01, 0x79, 0x58, 0xfe, 0xc6, + 0xc2, 0xa9, 0x56, 0x0a, 0x47, 0x86, 0x32, 0x05, 0xe8, 0x4c, 0x4a, 0x00, 0xc4, 0x25, 0xfd, 0xad, 0x6d, 0x2c, 0x24, + 0xdb, 0x3e, 0xf2, 0x81, 0x3f, 0x4e, 0x22, 0xee, 0x74, 0xb6, 0xda, 0x2e, 0xf0, 0x21, 0x08, 0x71, 0xd0, 0x11, 0x60, + 0xde, 0x57, 0xa8, 0x70, 0xf2, 0x16, 0x5c, 0xe6, 0x83, 0x51, 0x34, 0x89, 0xc7, 0xdc, 0x49, 0x50, 0x89, 0xb8, 0x25, + 0x4b, 0x40, 0xc9, 0xe8, 0x7d, 0x05, 0xca, 0x82, 0x09, 0xe9, 0x22, 0xaa, 0x95, 0x40, 0x63, 0x0a, 0x52, 0x92, 0x52, + 0xa4, 0x05, 0x15, 0x04, 0x86, 0x50, 0xe9, 0x29, 0x8e, 0x02, 0xfd, 0x16, 0x0f, 0xc4, 0xa0, 0xc1, 0x92, 0x45, 0x19, + 0x0f, 0xe2, 0xe5, 0x42, 0x50, 0xc3, 0x3e, 0xcf, 0x5e, 0x67, 0x17, 0x2c, 0x7f, 0x16, 0x21, 0xec, 0x81, 0xe8, 0x5e, + 0x82, 0xa4, 0x27, 0x81, 0xce, 0x7b, 0x8a, 0x57, 0xce, 0x09, 0x69, 0x58, 0x88, 0x69, 0x8c, 0x8a, 0x10, 0xec, 0x16, + 0xa2, 0x7d, 0x8a, 0x5b, 0x8a, 0xf6, 0x1e, 0xaa, 0x12, 0xae, 0x79, 0x6b, 0xef, 0x75, 0x9d, 0xb7, 0x60, 0x84, 0x99, + 0xe2, 0xd6, 0xfa, 0x8e, 0x75, 0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x7d, 0x5d, 0x19, + 0xe9, 0xa0, 0x4c, 0xb5, 0x34, 0x47, 0x08, 0xc4, 0x96, 0x70, 0x4b, 0x50, 0x46, 0x68, 0x78, 0xe5, 0x59, 0x92, 0x18, + 0xba, 0xc8, 0x8b, 0x7b, 0x4e, 0x43, 0x1d, 0x01, 0x14, 0xd3, 0x9a, 0x46, 0x1a, 0xb0, 0x40, 0x57, 0xa0, 0x52, 0x52, + 0xda, 0xc8, 0xab, 0xd6, 0x46, 0x40, 0x9c, 0x8e, 0x58, 0x2e, 0x1c, 0x34, 0xa9, 0x43, 0x61, 0xc2, 0x14, 0x18, 0x9a, + 0x8d, 0x40, 0xc2, 0x2b, 0x04, 0xc0, 0x3c, 0xf1, 0x27, 0x59, 0xc1, 0x75, 0x9d, 0x09, 0x7d, 0x7c, 0x7d, 0x1d, 0x0b, + 0x7f, 0x11, 0x19, 0x20, 0x67, 0xd3, 0xec, 0x9c, 0xad, 0x80, 0xba, 0xa7, 0x06, 0x33, 0x41, 0x36, 0x86, 0x01, 0x25, + 0x0a, 0xaa, 0x65, 0x96, 0xc4, 0x60, 0xe9, 0xeb, 0x06, 0x3e, 0x18, 0x74, 0xec, 0x12, 0x65, 0x84, 0xdb, 0xef, 0xf7, + 0xdb, 0x5e, 0xc7, 0x2d, 0x05, 0xc1, 0x17, 0x4b, 0x14, 0xbd, 0x41, 0x3f, 0x4a, 0x13, 0x7c, 0x95, 0x2c, 0x60, 0xae, + 0xa1, 0x14, 0x39, 0xe9, 0x26, 0xe6, 0x49, 0x41, 0xec, 0x7a, 0x23, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, 0xdb, 0xf6, 0x83, 0x26, 0x9b, 0x66, 0x27, 0xb5, 0xc3, 0xd4, 0xc2, 0xc8, 0x35, 0x2f, 0xb4, 0x07, 0x6c, 0x2e, 0x0f, - 0x5a, 0x89, 0x54, 0x0d, 0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x42, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, - 0x1f, 0x99, 0x04, 0x73, 0x15, 0x09, 0xf6, 0xa5, 0x40, 0xe0, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x06, 0xcb, 0x73, 0x1a, - 0x0d, 0x3f, 0x69, 0x70, 0x2b, 0xb2, 0x37, 0xd9, 0xc0, 0x69, 0x94, 0x84, 0x86, 0xb8, 0x32, 0xf1, 0x56, 0x12, 0xba, - 0xb6, 0x51, 0xc0, 0x21, 0x5b, 0x62, 0xfb, 0xe6, 0x42, 0x37, 0xb9, 0x5d, 0xb2, 0x87, 0xf2, 0x9f, 0x34, 0x97, 0xdc, - 0xc0, 0x72, 0x5c, 0x49, 0x03, 0xae, 0x18, 0x0f, 0x96, 0xa6, 0x01, 0x09, 0xf0, 0x5d, 0x39, 0x8a, 0x8b, 0xf5, 0x24, - 0xf8, 0x5d, 0xc1, 0x7c, 0x6e, 0xcc, 0x74, 0x2b, 0xa4, 0x5a, 0xc2, 0x49, 0x33, 0x58, 0x83, 0x26, 0x8d, 0x07, 0x25, - 0x6a, 0xbe, 0x46, 0x43, 0x85, 0x38, 0xfe, 0x4c, 0x54, 0xa1, 0x09, 0x86, 0x60, 0xe4, 0x5e, 0x21, 0x19, 0x2e, 0x5b, - 0x16, 0x2d, 0x52, 0xa6, 0xc6, 0xa4, 0x52, 0x35, 0xcb, 0x65, 0x60, 0x60, 0xd1, 0x6e, 0xf5, 0xa5, 0x25, 0xae, 0x44, - 0x6e, 0x1a, 0x6a, 0x61, 0x52, 0x28, 0x6f, 0xc2, 0xc9, 0xd1, 0xef, 0x52, 0xd6, 0xbb, 0x89, 0x4f, 0xae, 0xf0, 0xc9, - 0x7d, 0xc3, 0x87, 0x32, 0x79, 0xbb, 0x18, 0x14, 0xc1, 0xd7, 0xb5, 0x4a, 0xb4, 0x4f, 0x7d, 0x14, 0xcc, 0xae, 0x16, - 0xba, 0x20, 0x50, 0x24, 0x9b, 0xa4, 0x03, 0xc9, 0x6f, 0x28, 0x36, 0x2a, 0xcf, 0x28, 0x73, 0xc5, 0x06, 0xa9, 0x79, - 0xa5, 0x99, 0x97, 0xba, 0x0d, 0xfb, 0xbd, 0x2c, 0x25, 0x9d, 0xb8, 0xa0, 0x4c, 0xec, 0xdd, 0x44, 0x1b, 0x2f, 0x0d, - 0x33, 0x61, 0xfd, 0x0a, 0x63, 0xa7, 0x46, 0xa1, 0x54, 0x8a, 0x40, 0x1c, 0x1b, 0x5f, 0x2b, 0xcb, 0x20, 0xf3, 0x57, - 0xd8, 0x53, 0x00, 0x4a, 0x02, 0x8b, 0xaf, 0xa9, 0xe4, 0x45, 0x61, 0x9d, 0x8e, 0xf7, 0x88, 0x8e, 0x95, 0x08, 0xad, - 0x89, 0x7c, 0xad, 0xcf, 0x62, 0xbf, 0xe6, 0x12, 0x9a, 0x94, 0xcc, 0x07, 0x79, 0x60, 0xab, 0x40, 0x44, 0xa5, 0xdb, - 0x92, 0x41, 0x42, 0x0e, 0xe9, 0x32, 0xd1, 0x6b, 0x23, 0x19, 0xb4, 0x4e, 0x85, 0x44, 0x4b, 0x8f, 0xc2, 0xc8, 0x41, - 0xc7, 0x9d, 0xd6, 0x62, 0x89, 0x90, 0x4d, 0x7b, 0x93, 0x58, 0x11, 0x9d, 0xd3, 0x1c, 0x4d, 0x38, 0x53, 0xa7, 0x3b, - 0x0e, 0xa0, 0x03, 0x62, 0x7f, 0x89, 0xf5, 0x56, 0x9a, 0x9d, 0xae, 0x5f, 0x39, 0x7c, 0xd7, 0xd7, 0x13, 0xe4, 0x07, - 0x61, 0xf0, 0xc2, 0x9a, 0x0d, 0x94, 0xec, 0xdd, 0x7b, 0x8d, 0xad, 0xc8, 0xfe, 0xac, 0x4a, 0x2a, 0x4f, 0xa1, 0xc6, - 0xb9, 0xf5, 0x75, 0x62, 0x66, 0x68, 0x51, 0x55, 0xec, 0x1b, 0x52, 0x7d, 0x5f, 0x29, 0xec, 0x0a, 0xe5, 0x7d, 0x39, - 0x74, 0xec, 0xba, 0x6e, 0x90, 0x93, 0xf3, 0x72, 0x6f, 0x95, 0x0b, 0x79, 0xff, 0xbe, 0xe9, 0x33, 0x9d, 0xeb, 0xe1, - 0x9f, 0x39, 0xa8, 0x9c, 0x8b, 0xab, 0x94, 0x2c, 0x98, 0x67, 0x4a, 0x1d, 0x2d, 0x39, 0xa0, 0xed, 0x1e, 0x7a, 0xda, - 0xd1, 0x45, 0x14, 0x73, 0x4b, 0x8f, 0x22, 0x3c, 0x6d, 0x94, 0x4f, 0xd2, 0xe8, 0x00, 0xbc, 0xd0, 0x84, 0x24, 0x27, - 0xdc, 0xb4, 0x45, 0x8b, 0xe1, 0x84, 0x61, 0x08, 0x5c, 0xd9, 0x13, 0xa6, 0xec, 0xb9, 0x87, 0x78, 0x8b, 0x81, 0xd9, - 0x6a, 0xd8, 0xcb, 0x66, 0xf7, 0x9a, 0xf9, 0x0f, 0x6b, 0x04, 0xb2, 0x6d, 0xaa, 0xea, 0xca, 0xc6, 0xbb, 0x14, 0x91, - 0x18, 0x61, 0x5b, 0x35, 0xb6, 0xb4, 0xf5, 0x7b, 0x0d, 0xf7, 0xba, 0x72, 0xcc, 0x6b, 0x4a, 0xb5, 0xa1, 0x87, 0x95, - 0x9b, 0xc3, 0x4c, 0x47, 0x5e, 0xac, 0xa0, 0xdb, 0x13, 0x41, 0x21, 0x70, 0x22, 0xb4, 0x3d, 0xa8, 0xb8, 0x81, 0x48, - 0xc9, 0x95, 0x56, 0xcd, 0xe6, 0xc9, 0x48, 0x02, 0x0b, 0x2e, 0x2c, 0x97, 0x7c, 0x74, 0x11, 0x27, 0x49, 0x55, 0xfa, - 0xbb, 0x0a, 0x78, 0x31, 0xec, 0x6d, 0xa2, 0x5d, 0x60, 0x34, 0x57, 0x20, 0xb8, 0xda, 0x08, 0xfb, 0xe8, 0xb8, 0xd5, - 0xba, 0x8b, 0x88, 0x23, 0x37, 0xa3, 0x11, 0x50, 0x8f, 0x11, 0x56, 0xcd, 0xda, 0x7b, 0x2f, 0x30, 0xa4, 0x66, 0xe0, - 0x83, 0xea, 0x8c, 0x8a, 0x7f, 0x95, 0x3d, 0xf5, 0x2b, 0xd1, 0xbb, 0x55, 0x75, 0x35, 0x03, 0x2a, 0x2a, 0xf0, 0x61, - 0x86, 0x58, 0xda, 0x2a, 0x10, 0x90, 0xeb, 0x61, 0x51, 0x0a, 0x98, 0xa4, 0xc1, 0x82, 0x52, 0x60, 0xad, 0x95, 0xdd, - 0xeb, 0xdb, 0x82, 0x39, 0x14, 0x0a, 0x17, 0xfd, 0x9f, 0x65, 0xd3, 0x19, 0x5a, 0x66, 0x0d, 0xa6, 0x86, 0x06, 0x1f, - 0x1b, 0xf5, 0xe5, 0x8a, 0xb2, 0x5a, 0x1f, 0xda, 0x91, 0x35, 0x7e, 0xd2, 0x8e, 0x32, 0x38, 0x54, 0x73, 0x5d, 0x54, - 0xb7, 0x9b, 0x9b, 0x22, 0x66, 0x15, 0x8f, 0xfb, 0xa4, 0xb7, 0xb5, 0x35, 0xe9, 0x69, 0x1a, 0x90, 0x4c, 0x92, 0x0c, - 0x6f, 0x32, 0x40, 0x59, 0x11, 0x67, 0x51, 0x36, 0xc8, 0xb7, 0x28, 0x4b, 0x5c, 0xbf, 0x1f, 0x7a, 0x7b, 0x35, 0xcf, - 0xda, 0xdb, 0x5b, 0xef, 0x22, 0x57, 0x75, 0xd2, 0x83, 0x3c, 0x3c, 0x82, 0xa2, 0x25, 0x9b, 0x32, 0x5c, 0x4c, 0xb3, - 0x11, 0x0b, 0x6c, 0xe8, 0x9e, 0xda, 0xa5, 0xdc, 0x34, 0x11, 0x6c, 0x8e, 0x88, 0x39, 0x8b, 0x0f, 0xf5, 0x48, 0x6a, - 0xb0, 0x07, 0x2c, 0xa0, 0xcd, 0x85, 0xaf, 0xc2, 0xb3, 0x24, 0x3b, 0x8d, 0x92, 0x03, 0xa1, 0xc0, 0x6b, 0x2d, 0xbf, - 0x05, 0x97, 0x91, 0x2c, 0x56, 0x43, 0x49, 0x7d, 0x35, 0xf8, 0x2a, 0xb8, 0xbd, 0x47, 0xe5, 0xad, 0xd8, 0x1d, 0xbf, - 0xed, 0x77, 0x6c, 0x15, 0x11, 0xfb, 0xc9, 0x9c, 0x0e, 0x34, 0x4e, 0x01, 0x94, 0x39, 0x00, 0x4d, 0x56, 0x78, 0x43, - 0x16, 0xfe, 0x34, 0xf8, 0x49, 0xb9, 0xd4, 0x19, 0xb8, 0x10, 0xe0, 0xe4, 0x27, 0x31, 0x6f, 0xe1, 0x79, 0xa4, 0xed, - 0x2d, 0x44, 0x05, 0xc6, 0x15, 0x29, 0x2e, 0x5d, 0x2a, 0x6f, 0xd0, 0x3b, 0x0e, 0x4f, 0xa0, 0xd9, 0xc6, 0xc6, 0xc2, - 0x79, 0x13, 0xf1, 0x89, 0x9f, 0x47, 0xe9, 0x28, 0x9b, 0x3a, 0xee, 0xa6, 0x6d, 0xbb, 0x7e, 0x41, 0x9e, 0xc8, 0xe7, - 0x6e, 0xb9, 0x71, 0x02, 0x7e, 0x40, 0x68, 0x0f, 0xec, 0xcd, 0x63, 0xef, 0x80, 0x85, 0x27, 0xbb, 0x1b, 0x8b, 0x11, - 0x2b, 0xfb, 0x27, 0xde, 0xa5, 0x8e, 0xb9, 0x7b, 0xef, 0x51, 0xca, 0x40, 0xaf, 0xb0, 0x7f, 0x29, 0xc1, 0x00, 0x76, - 0xa3, 0xf8, 0x3b, 0x48, 0xb9, 0x8f, 0x74, 0x20, 0x22, 0xe3, 0xb4, 0xd7, 0xd7, 0x76, 0x46, 0x11, 0x03, 0xfb, 0x9e, - 0x76, 0x56, 0xef, 0xdf, 0xaf, 0xd4, 0x7c, 0x55, 0xea, 0xcd, 0x59, 0x58, 0xf3, 0xd4, 0xbd, 0x97, 0x74, 0xb4, 0x52, - 0xdf, 0xc8, 0x73, 0x46, 0x4a, 0x73, 0xd9, 0x4e, 0x70, 0x8c, 0x2d, 0xbe, 0x7a, 0x5b, 0x1f, 0x8a, 0x28, 0x85, 0x1f, - 0x83, 0xf5, 0x12, 0x81, 0xfa, 0x06, 0x07, 0xc7, 0x3b, 0x08, 0xb7, 0x76, 0x9d, 0x41, 0xe0, 0xdc, 0x6b, 0xb5, 0xae, - 0x7f, 0xdc, 0x3a, 0xfc, 0x73, 0xd4, 0xfa, 0x65, 0xaf, 0xf5, 0xc3, 0x91, 0x7b, 0xed, 0xfc, 0xb8, 0x35, 0x38, 0x94, - 0x6f, 0x87, 0x7f, 0xee, 0xff, 0x58, 0x1c, 0xfd, 0x41, 0x14, 0x6e, 0xb8, 0xee, 0xd6, 0x99, 0x37, 0x63, 0xe1, 0x56, - 0xab, 0xd5, 0x87, 0xa7, 0x33, 0x78, 0xc2, 0xbf, 0x17, 0xf0, 0xe7, 0xfa, 0xd0, 0xfa, 0x4f, 0x3f, 0xa6, 0xff, 0xf9, - 0xc7, 0xfc, 0x08, 0xc7, 0x3c, 0xfc, 0xf3, 0x8f, 0x85, 0xfd, 0xa0, 0x1f, 0x6e, 0x1d, 0x6d, 0xba, 0x8e, 0xae, 0xf9, - 0x43, 0x58, 0x3d, 0x42, 0xab, 0xc3, 0x3f, 0xcb, 0x37, 0xfb, 0xc1, 0xc9, 0x6e, 0x3f, 0x3c, 0xba, 0x76, 0xec, 0xeb, - 0x07, 0xee, 0xb5, 0xeb, 0x5e, 0x6f, 0xe0, 0x3c, 0xe7, 0x30, 0xfa, 0x03, 0xf8, 0x3b, 0x86, 0xbf, 0x36, 0xfc, 0x9d, - 0xc2, 0xdf, 0x3f, 0x43, 0x37, 0x11, 0x7f, 0xbb, 0xa6, 0x58, 0xc8, 0x35, 0x1e, 0x58, 0x44, 0xb0, 0x0a, 0xee, 0xc6, - 0x56, 0xec, 0x6d, 0x10, 0xd1, 0x60, 0x1f, 0xfa, 0xbe, 0x8f, 0x61, 0x52, 0x67, 0x71, 0xbc, 0x01, 0x8b, 0x8e, 0x9c, - 0xb3, 0x11, 0x30, 0x4f, 0x44, 0x0e, 0x8a, 0x80, 0x8b, 0xb3, 0xd5, 0x02, 0x0f, 0x57, 0xbd, 0x61, 0xb8, 0xc1, 0x1c, - 0x30, 0x0a, 0xde, 0x32, 0x7c, 0xe8, 0xba, 0xde, 0x0b, 0x79, 0x66, 0x88, 0xfb, 0x5c, 0xb0, 0x56, 0x9a, 0x09, 0x93, - 0xc6, 0x76, 0xbd, 0xd9, 0x8a, 0x4a, 0xd8, 0xd6, 0xe9, 0x19, 0xd4, 0x9d, 0x8a, 0x83, 0xb6, 0xef, 0x58, 0xf4, 0x09, - 0xb7, 0xe4, 0x1b, 0xe3, 0x10, 0x78, 0xc9, 0x92, 0x6f, 0x1a, 0x8d, 0x86, 0x8d, 0x28, 0xdc, 0xb1, 0xa7, 0x0c, 0x66, - 0x58, 0x32, 0x11, 0x39, 0x29, 0x4d, 0x61, 0xd9, 0xc2, 0xe4, 0xef, 0xa3, 0x9c, 0x6f, 0x54, 0x86, 0x6d, 0x58, 0xb3, - 0x64, 0x9b, 0x96, 0xfe, 0x1d, 0xa6, 0x40, 0xd3, 0x92, 0xce, 0x3f, 0xcc, 0xf1, 0xc3, 0x94, 0xd0, 0x7a, 0xeb, 0x70, - 0xf0, 0xd0, 0x0b, 0x90, 0x3b, 0xa2, 0x9f, 0xf3, 0x1e, 0xd5, 0x18, 0xfc, 0x2b, 0xc3, 0x0c, 0x9e, 0x98, 0x0f, 0x43, - 0x34, 0x8b, 0x52, 0x07, 0xb7, 0x52, 0x14, 0xf7, 0xaf, 0x70, 0x67, 0xa4, 0xa5, 0xb7, 0x1f, 0xaa, 0x1d, 0x73, 0x90, - 0x33, 0xf6, 0x5d, 0x94, 0x7c, 0x62, 0xb9, 0x73, 0xe9, 0x75, 0xba, 0x9f, 0x53, 0x67, 0x0f, 0x6d, 0xb3, 0x0f, 0xd5, - 0x31, 0x9a, 0x32, 0x0b, 0xd4, 0x11, 0x61, 0xab, 0xe3, 0xe5, 0x18, 0xd5, 0x42, 0x12, 0x14, 0x5e, 0x16, 0x76, 0x89, - 0xc3, 0xed, 0xdd, 0xe2, 0xfc, 0xac, 0x6f, 0x07, 0xb6, 0x0d, 0x16, 0xff, 0x01, 0x85, 0xad, 0x84, 0x61, 0x01, 0x06, - 0xd9, 0x6e, 0xdc, 0xe3, 0x9b, 0x9b, 0x55, 0xc0, 0x09, 0x0f, 0xd2, 0xa9, 0x7b, 0xe2, 0x45, 0xde, 0x24, 0x84, 0x01, - 0x87, 0xd0, 0x0c, 0xbb, 0xf4, 0x86, 0xbb, 0xb1, 0x9c, 0x06, 0x63, 0x21, 0x7e, 0x12, 0x15, 0xfc, 0x15, 0xc6, 0x23, - 0xc2, 0x21, 0x1a, 0xfb, 0x3e, 0xbb, 0x64, 0x43, 0x65, 0x67, 0x00, 0xa1, 0x22, 0xb7, 0xe7, 0x0e, 0x43, 0xa3, 0x19, - 0xcc, 0x1d, 0x86, 0x07, 0x03, 0x1b, 0xf6, 0x12, 0xec, 0xca, 0x30, 0x3a, 0xec, 0x1c, 0x0d, 0xd2, 0x70, 0xc6, 0x02, - 0x4d, 0x5b, 0x59, 0x74, 0x56, 0x2b, 0xea, 0x1e, 0x0d, 0x9c, 0x29, 0x18, 0xe9, 0x60, 0x8b, 0x3b, 0xf8, 0x86, 0x11, - 0x8a, 0x22, 0xfc, 0xc0, 0xce, 0x5e, 0x5c, 0xce, 0x1c, 0x7b, 0x77, 0xcb, 0xde, 0xc4, 0x52, 0xcf, 0x06, 0xf6, 0x82, - 0xb9, 0xc3, 0x0b, 0xd7, 0xec, 0xbc, 0x7d, 0x84, 0xa0, 0x62, 0x21, 0x4e, 0x7e, 0x31, 0xb0, 0xfb, 0x62, 0xea, 0x36, - 0x0c, 0x9a, 0xca, 0xe5, 0xc7, 0x15, 0x3d, 0x20, 0x54, 0x55, 0x57, 0x05, 0x1d, 0x94, 0x75, 0x03, 0x67, 0x62, 0x22, - 0xd1, 0xc2, 0xc9, 0x24, 0x15, 0xc0, 0xe1, 0xc1, 0x66, 0x30, 0xa9, 0xd1, 0x6d, 0xfb, 0x68, 0x70, 0x11, 0x3c, 0xb0, - 0x1f, 0xa8, 0x97, 0x31, 0x20, 0xc3, 0xc4, 0xf4, 0x63, 0x90, 0x76, 0xf8, 0xf7, 0x9c, 0x01, 0x92, 0x17, 0x54, 0x34, - 0x93, 0x45, 0x67, 0x58, 0x74, 0x10, 0x20, 0xa8, 0x5e, 0xa1, 0xad, 0x3f, 0xb1, 0x26, 0xa3, 0x90, 0x60, 0xbf, 0x7f, - 0x1f, 0x96, 0x66, 0xb3, 0x73, 0x84, 0xe7, 0x0d, 0x39, 0x2f, 0xbe, 0x8b, 0x39, 0xa8, 0x84, 0xad, 0xbe, 0xed, 0x0e, - 0x6c, 0x0b, 0x97, 0xb6, 0x97, 0x6d, 0x86, 0x82, 0xc2, 0xf1, 0xe6, 0x01, 0x0b, 0x26, 0xfd, 0xb0, 0x3d, 0x70, 0x72, - 0x19, 0x6e, 0xc4, 0x73, 0x4b, 0x21, 0xc1, 0xdb, 0xde, 0x04, 0x04, 0x3a, 0x72, 0xee, 0x86, 0xbd, 0xa9, 0x0a, 0xa1, - 0xe8, 0x78, 0x73, 0xe4, 0x06, 0x31, 0xfc, 0x71, 0x5a, 0xc8, 0x34, 0x13, 0xdd, 0x57, 0x6b, 0x66, 0x37, 0x18, 0x29, - 0x8b, 0x3c, 0x09, 0xb3, 0x4d, 0x07, 0x23, 0xb4, 0x20, 0x69, 0x77, 0x07, 0x00, 0xc3, 0xa6, 0xa3, 0x38, 0x6d, 0x4b, - 0xb1, 0x9a, 0xb2, 0xcf, 0x0f, 0xf5, 0x72, 0x0c, 0xd9, 0x60, 0xc8, 0xfc, 0x4a, 0xfb, 0x00, 0x58, 0x41, 0xe2, 0xe5, - 0x47, 0xea, 0xcc, 0xeb, 0x65, 0xed, 0x7c, 0x6b, 0xa1, 0x44, 0x11, 0xf3, 0x0c, 0x09, 0xc5, 0x4b, 0xed, 0x86, 0x09, - 0x73, 0x7b, 0x86, 0xc4, 0xd0, 0x2c, 0x1f, 0xb6, 0x81, 0xe9, 0x55, 0x80, 0x3d, 0x35, 0xb7, 0x45, 0x12, 0x56, 0xcd, - 0xbd, 0x43, 0x60, 0xed, 0x23, 0xe0, 0x21, 0xda, 0x46, 0x3d, 0x15, 0xcd, 0x67, 0x49, 0xf8, 0xb2, 0x71, 0x5c, 0x1c, - 0xe1, 0x89, 0xd0, 0xbe, 0x3f, 0x9c, 0xe7, 0x20, 0x0f, 0xf8, 0x5b, 0xb0, 0x0c, 0x42, 0xd9, 0x14, 0x1d, 0x3d, 0x3c, - 0x02, 0xf6, 0x08, 0xf1, 0x46, 0xd8, 0xdc, 0xa8, 0x46, 0x8b, 0x92, 0x8c, 0x17, 0x3a, 0x18, 0xee, 0x71, 0xe9, 0xda, - 0xa3, 0x60, 0x90, 0x27, 0xc6, 0x0e, 0x9e, 0xf9, 0xfb, 0x43, 0xac, 0xc6, 0x09, 0x0a, 0xb7, 0xa4, 0xdd, 0x56, 0x89, - 0xbf, 0x7d, 0x3f, 0x05, 0x09, 0x8e, 0x75, 0xe0, 0x67, 0xdd, 0xbf, 0x9f, 0x48, 0xa4, 0x76, 0xd3, 0x1e, 0x9d, 0x44, - 0x60, 0x3c, 0x38, 0xf7, 0x53, 0xa8, 0x46, 0x12, 0x51, 0x51, 0x8e, 0x16, 0xa8, 0x79, 0xaa, 0x56, 0xc1, 0x77, 0x68, - 0x46, 0xe0, 0x39, 0x86, 0xad, 0xc9, 0x4f, 0xd5, 0x8d, 0x45, 0x2c, 0xdf, 0x75, 0xe9, 0x68, 0x0b, 0x0f, 0x20, 0x05, - 0xa3, 0x09, 0x86, 0x71, 0x29, 0x28, 0x59, 0xf1, 0xdf, 0xb1, 0x11, 0x2b, 0x9f, 0x1c, 0x66, 0x9b, 0x9b, 0x47, 0xe2, - 0xdc, 0x82, 0x18, 0x87, 0x1b, 0xd1, 0xd5, 0xb8, 0x02, 0xa0, 0x3e, 0x9d, 0x13, 0xd7, 0x03, 0xd3, 0x8a, 0x35, 0x5d, - 0x8a, 0x7d, 0x72, 0x98, 0x01, 0x28, 0xb8, 0xe5, 0x1c, 0xfa, 0x83, 0x3f, 0x1e, 0x81, 0x7b, 0xec, 0xff, 0xc1, 0xdd, - 0x52, 0x82, 0xa6, 0x27, 0xcf, 0x14, 0x17, 0x74, 0xc6, 0xda, 0xf1, 0x28, 0x36, 0x1a, 0x14, 0x5e, 0x0a, 0x18, 0x80, - 0x36, 0x07, 0x99, 0x50, 0x71, 0x10, 0x72, 0x54, 0x60, 0xfb, 0xb8, 0xf9, 0x39, 0xee, 0xec, 0xe7, 0x60, 0xe1, 0x0d, - 0xf4, 0xdb, 0x6b, 0x78, 0xfb, 0xa3, 0x7e, 0xfb, 0x89, 0x05, 0xbf, 0x94, 0x32, 0x74, 0x5f, 0x9b, 0xe2, 0x91, 0x9a, - 0xa2, 0x14, 0x4b, 0x64, 0xd0, 0x90, 0xb9, 0xf9, 0x52, 0xcc, 0x86, 0xbb, 0x25, 0x10, 0x43, 0x89, 0xae, 0xdc, 0xe7, - 0xd1, 0x19, 0x12, 0xd7, 0x35, 0x49, 0x61, 0xe4, 0x12, 0x98, 0x08, 0x57, 0x7c, 0x4b, 0xcc, 0xd9, 0x6f, 0x83, 0x0d, - 0x5e, 0xcb, 0x3b, 0x40, 0xfb, 0x8e, 0x4d, 0x67, 0xfc, 0x6a, 0x9f, 0x14, 0x7d, 0x20, 0xd3, 0x06, 0xc4, 0xd9, 0x79, - 0xbb, 0x17, 0xef, 0xf2, 0x5e, 0x0c, 0x52, 0x3d, 0x57, 0x2c, 0x86, 0x7b, 0xd5, 0x7b, 0x8f, 0x51, 0x4a, 0x93, 0x99, - 0xbc, 0x1a, 0x7a, 0x5d, 0x89, 0xde, 0xe6, 0x26, 0x20, 0xd8, 0x33, 0xba, 0x72, 0xd1, 0xb5, 0x2c, 0x05, 0x4d, 0x00, - 0xa2, 0x27, 0x75, 0x96, 0x23, 0x8e, 0xc3, 0x6c, 0x36, 0x28, 0x1e, 0x31, 0x77, 0xe5, 0xa8, 0x38, 0x26, 0x76, 0x97, - 0x09, 0x3b, 0x80, 0x19, 0x71, 0x79, 0xab, 0x23, 0xa2, 0xc3, 0xa2, 0xbf, 0x8e, 0x6f, 0x1f, 0x7b, 0x6c, 0xb3, 0xe3, - 0x82, 0x06, 0xa9, 0x8d, 0xf5, 0xb8, 0x1a, 0x0b, 0xea, 0xc3, 0x63, 0x4d, 0xa5, 0xb2, 0xd8, 0xdc, 0x2c, 0xeb, 0x47, - 0xb5, 0x6a, 0x07, 0xd7, 0x4e, 0x53, 0x2e, 0x9b, 0xd9, 0x20, 0x1c, 0x88, 0x98, 0x40, 0x81, 0x96, 0x56, 0x56, 0x0c, - 0x30, 0xa4, 0x2c, 0x47, 0xf9, 0x14, 0x32, 0x2f, 0x2e, 0x4b, 0x9d, 0xfa, 0xf2, 0x4c, 0x06, 0x1d, 0xf1, 0xd4, 0x93, - 0x8c, 0x15, 0x50, 0xb0, 0x5e, 0xea, 0x25, 0xb4, 0x44, 0x80, 0xf9, 0x0b, 0x95, 0x43, 0x23, 0x2c, 0x90, 0x28, 0x34, - 0xcc, 0x12, 0x65, 0x7c, 0x16, 0x61, 0x0c, 0xda, 0xfe, 0x59, 0x2d, 0xf6, 0x55, 0x28, 0xa3, 0xa3, 0x38, 0xcc, 0x8f, - 0x02, 0xaa, 0x9f, 0x4b, 0x09, 0x36, 0x09, 0x3f, 0x02, 0x1b, 0x55, 0x8e, 0x27, 0x09, 0xc2, 0xe7, 0x71, 0xce, 0xc8, - 0x53, 0xd8, 0x90, 0x30, 0x4b, 0xd3, 0x36, 0x52, 0xed, 0x22, 0x33, 0x08, 0xe5, 0xc2, 0xfc, 0x13, 0xe3, 0xec, 0x22, - 0x0b, 0x97, 0x5a, 0x83, 0xf9, 0xf1, 0xce, 0x04, 0x28, 0xbb, 0xbe, 0xce, 0x84, 0x8f, 0x1b, 0x91, 0xbd, 0xa1, 0x2b, - 0x26, 0x03, 0x85, 0x54, 0xe0, 0x44, 0x64, 0xf1, 0xd0, 0x19, 0x0a, 0x8d, 0x70, 0x40, 0xa7, 0xc8, 0xb9, 0x6b, 0x6c, - 0xfa, 0x7c, 0xa0, 0x7d, 0xa3, 0x34, 0x74, 0x12, 0x10, 0x02, 0x02, 0x77, 0xc3, 0x9a, 0x4a, 0x07, 0x69, 0x90, 0x50, - 0x29, 0xfa, 0x39, 0x80, 0x7f, 0x18, 0x49, 0x0a, 0x80, 0xfd, 0x50, 0x8d, 0x14, 0x51, 0x96, 0x05, 0x2e, 0x00, 0xcd, - 0xb5, 0x8f, 0x2b, 0xe1, 0x0b, 0x03, 0x15, 0xa6, 0xa7, 0x59, 0x79, 0x29, 0x94, 0xc8, 0xd3, 0x15, 0x29, 0x6b, 0x24, - 0x93, 0xcf, 0xd1, 0xe1, 0x53, 0xde, 0xf5, 0x5b, 0x89, 0x87, 0x2e, 0x78, 0x0e, 0xcb, 0xaa, 0x9e, 0xdf, 0x84, 0x9c, - 0x9c, 0x6b, 0xd0, 0x15, 0x52, 0xe8, 0x2f, 0x39, 0xc9, 0x7b, 0x6f, 0xfc, 0xaa, 0x96, 0x1a, 0x43, 0xd9, 0xc7, 0x55, - 0xcd, 0xb0, 0xbc, 0x9c, 0x55, 0x61, 0x0a, 0x02, 0x6e, 0xc1, 0x92, 0x60, 0x21, 0x35, 0x04, 0x58, 0xd8, 0x1e, 0x69, - 0xa5, 0x20, 0x2f, 0x75, 0x78, 0xe7, 0x39, 0x58, 0x01, 0xc6, 0xa1, 0x96, 0x4a, 0xa6, 0x91, 0xc4, 0x97, 0x4a, 0x14, - 0x98, 0x72, 0x7f, 0x08, 0x7e, 0x6a, 0xf3, 0xa4, 0xeb, 0xd2, 0xf5, 0xe3, 0x29, 0xa6, 0xf6, 0x10, 0xe8, 0xb1, 0x77, - 0x0f, 0x4c, 0x89, 0xba, 0x0e, 0x2b, 0x88, 0x43, 0xb3, 0x9a, 0x66, 0x01, 0x33, 0xa6, 0x0d, 0x5a, 0xb2, 0x0d, 0xb6, - 0x5c, 0x0e, 0xf6, 0x91, 0xd8, 0x9e, 0xd5, 0x0a, 0x08, 0x5d, 0x83, 0x06, 0x86, 0xdc, 0xa5, 0x42, 0x0b, 0xf3, 0x5e, - 0x97, 0x8a, 0x70, 0x7f, 0x0e, 0xb8, 0xb4, 0x82, 0x33, 0x2f, 0xa3, 0x81, 0xf7, 0xe3, 0xd3, 0x04, 0x13, 0x5f, 0x10, - 0x2b, 0xb0, 0x83, 0x83, 0x4e, 0xb3, 0x29, 0x70, 0x2a, 0x2e, 0x52, 0x06, 0xcb, 0x8a, 0x52, 0x1b, 0xfe, 0x48, 0x91, - 0xad, 0xbb, 0x3c, 0xd2, 0x5d, 0x88, 0x05, 0xb0, 0xd3, 0x2f, 0x18, 0xf9, 0x96, 0xf5, 0x32, 0x60, 0x70, 0xae, 0x35, - 0x0e, 0x02, 0xbf, 0xb9, 0x99, 0x1c, 0x95, 0x29, 0xb1, 0x5d, 0x93, 0xd5, 0x05, 0xe4, 0x98, 0x04, 0xd8, 0xc0, 0x1d, - 0x84, 0xa5, 0xb2, 0xc7, 0x8b, 0x72, 0x8a, 0xcb, 0xa5, 0x2c, 0xe4, 0xe6, 0x79, 0x35, 0xcd, 0xe7, 0x56, 0x9a, 0x4d, - 0xc7, 0x5b, 0xf1, 0x45, 0xc1, 0x3f, 0x70, 0x62, 0x69, 0xd5, 0x53, 0x6a, 0x85, 0x47, 0x99, 0x5b, 0xb2, 0x4e, 0x49, - 0xad, 0xae, 0x1b, 0xa8, 0x46, 0x78, 0x9a, 0x86, 0x8d, 0x40, 0x88, 0x09, 0x2e, 0x7e, 0xdb, 0x64, 0x62, 0xda, 0x5b, - 0x42, 0xea, 0x08, 0xbb, 0x87, 0x72, 0x82, 0xbb, 0x9a, 0x67, 0x5f, 0x86, 0xb3, 0xf5, 0xcc, 0xbd, 0x67, 0x30, 0xf7, - 0xd3, 0x90, 0x1b, 0x8c, 0x1e, 0xcb, 0x84, 0x1f, 0x19, 0xfb, 0xc8, 0x55, 0xd5, 0xb3, 0xb3, 0xb0, 0x12, 0x59, 0xe2, - 0xc9, 0x38, 0xea, 0x30, 0x4e, 0x45, 0x6b, 0x82, 0xec, 0xfa, 0xba, 0x30, 0xf7, 0x02, 0x05, 0x4d, 0x3d, 0x5e, 0x8f, - 0xd3, 0x56, 0xec, 0x6c, 0x44, 0x22, 0xf7, 0xde, 0xd4, 0x22, 0x91, 0x15, 0x9f, 0xe3, 0x48, 0x6b, 0x0e, 0x72, 0x9f, - 0x9d, 0x2d, 0x6f, 0x52, 0xa1, 0x5b, 0x34, 0xda, 0xc6, 0x1e, 0xd5, 0x07, 0x92, 0x7a, 0x46, 0x05, 0x56, 0x35, 0xf6, - 0xfd, 0xfb, 0x1d, 0x91, 0x6e, 0xa9, 0x14, 0x1b, 0x2c, 0x2d, 0x8c, 0x66, 0x8c, 0x82, 0x41, 0x49, 0x91, 0x81, 0x1a, - 0xe5, 0x6b, 0x04, 0xc3, 0x1e, 0x35, 0x00, 0xc5, 0xb9, 0xba, 0xfa, 0x69, 0x29, 0xd9, 0x42, 0x40, 0xe2, 0x2e, 0x18, - 0x88, 0x35, 0xc1, 0xcc, 0xc8, 0x27, 0x1f, 0x81, 0xf3, 0x06, 0x0c, 0x1d, 0x03, 0xf0, 0x0b, 0xc4, 0xa6, 0x07, 0x13, - 0xdb, 0x26, 0xa2, 0xe8, 0xb3, 0x81, 0x97, 0x00, 0xec, 0xac, 0x0a, 0x8d, 0x7e, 0xa8, 0x52, 0xc0, 0x90, 0x0d, 0xdc, - 0x80, 0x55, 0x61, 0xb9, 0xbd, 0x97, 0xe0, 0x36, 0xc0, 0xeb, 0x0b, 0xd9, 0x7c, 0x03, 0xf3, 0x04, 0xab, 0xb3, 0x0b, - 0xbf, 0xb2, 0xac, 0xc5, 0xb9, 0xd3, 0x41, 0xa3, 0x5e, 0x51, 0x42, 0xd4, 0xee, 0x63, 0xed, 0x4b, 0x8c, 0xb0, 0x88, - 0xf7, 0x37, 0xf8, 0xae, 0xc7, 0x2d, 0xf7, 0x34, 0x5a, 0x84, 0xe9, 0x32, 0x69, 0x0c, 0x4a, 0xd6, 0xfd, 0x64, 0xc4, - 0xbd, 0xdc, 0x17, 0xb1, 0xe0, 0x0a, 0x47, 0x56, 0x85, 0x14, 0x1b, 0x48, 0xd2, 0xd3, 0x1e, 0x1d, 0xb0, 0x6f, 0x34, - 0x7b, 0x01, 0x65, 0x3e, 0x56, 0xa4, 0x92, 0x90, 0xd2, 0xec, 0x86, 0x48, 0x12, 0xd6, 0x8a, 0x3c, 0x75, 0xde, 0x77, - 0xb4, 0xcf, 0xad, 0x24, 0x82, 0x11, 0x9c, 0x84, 0xe9, 0x58, 0x79, 0xd0, 0x14, 0xe0, 0x2a, 0x3a, 0x62, 0xfa, 0x26, - 0x20, 0xbf, 0x19, 0xc8, 0xed, 0xa5, 0xe4, 0xda, 0x5c, 0xc3, 0xf0, 0x0c, 0x09, 0x56, 0x45, 0x22, 0xf0, 0x88, 0x1a, - 0x70, 0xcc, 0x57, 0x79, 0x1e, 0x60, 0xc2, 0xd7, 0xf6, 0x26, 0x00, 0x94, 0x93, 0xab, 0xe2, 0x2c, 0x05, 0xba, 0x01, - 0xcb, 0xd5, 0x71, 0x6a, 0x54, 0x24, 0x2e, 0x6e, 0x4c, 0x57, 0xb7, 0xf4, 0xa7, 0x68, 0x39, 0x93, 0x21, 0xa6, 0x83, - 0x20, 0x20, 0x53, 0x9f, 0x32, 0x47, 0xc8, 0x5c, 0x61, 0x7d, 0xce, 0x9c, 0xda, 0xd4, 0x3d, 0x46, 0xdd, 0x3c, 0x49, - 0x2d, 0x5e, 0xa7, 0x4d, 0x29, 0x11, 0x93, 0x12, 0xf3, 0x54, 0xa4, 0x62, 0x33, 0x25, 0xee, 0xdc, 0xfa, 0x46, 0x0b, - 0x69, 0xa3, 0x9d, 0x8a, 0x1c, 0x6c, 0x56, 0xc9, 0x7b, 0x02, 0xe3, 0xa5, 0x20, 0x7c, 0x89, 0x8c, 0xb5, 0x98, 0x33, - 0xc7, 0x44, 0xb0, 0x7a, 0x31, 0x15, 0xf9, 0x07, 0x47, 0xa7, 0xd9, 0x1b, 0xf4, 0x20, 0xf5, 0x06, 0x12, 0xb3, 0x26, - 0xbe, 0x0b, 0x69, 0xa8, 0x23, 0x04, 0x2a, 0xa3, 0x5a, 0xa6, 0xe3, 0xc4, 0x2a, 0x7c, 0x23, 0xf8, 0xea, 0xbd, 0x3e, - 0xce, 0x37, 0x9e, 0x1b, 0xab, 0x11, 0xc4, 0xe0, 0x2d, 0xe4, 0x47, 0x9e, 0x14, 0xe1, 0x40, 0xb8, 0x7c, 0x73, 0xb3, - 0x97, 0xef, 0xf2, 0x2a, 0x44, 0x52, 0xc1, 0x18, 0x63, 0x46, 0x31, 0xee, 0x89, 0x9a, 0x5a, 0xcc, 0x61, 0x60, 0xd9, - 0x3a, 0xcc, 0xf1, 0x00, 0x00, 0x5a, 0x9a, 0xd2, 0xab, 0xa6, 0x42, 0xe5, 0x79, 0x2e, 0xe1, 0x53, 0x1d, 0xa2, 0xaa, - 0xc6, 0xef, 0x57, 0x67, 0xa0, 0x10, 0xdc, 0xf7, 0x3a, 0x1e, 0x1e, 0x42, 0xc0, 0x2a, 0x0a, 0x59, 0xa0, 0x37, 0x68, - 0xaf, 0x4a, 0x84, 0x62, 0xe6, 0x64, 0x3d, 0x66, 0x38, 0xa9, 0x60, 0x0b, 0x95, 0xb0, 0x54, 0x5a, 0xe0, 0x57, 0x1b, - 0xa1, 0x79, 0xca, 0xb8, 0xf7, 0xa6, 0xc2, 0x19, 0xf4, 0x07, 0xf3, 0x96, 0x19, 0xf5, 0xfd, 0xd2, 0x89, 0x4c, 0x05, - 0x26, 0x6e, 0x66, 0xa9, 0xfd, 0x7e, 0x59, 0xa5, 0xfd, 0xbc, 0x42, 0xee, 0x73, 0xd2, 0x7c, 0x9d, 0x3b, 0x68, 0x3e, - 0x19, 0xee, 0x57, 0xca, 0x0f, 0x2d, 0x8c, 0x9a, 0xf2, 0xcb, 0xeb, 0xca, 0xaf, 0xf0, 0x54, 0x78, 0xab, 0xdf, 0x45, - 0xa1, 0x8b, 0xfa, 0x1c, 0x0c, 0x21, 0xfd, 0x08, 0xae, 0xa1, 0xc1, 0x83, 0x22, 0x59, 0x2c, 0xd6, 0x2e, 0x88, 0xeb, - 0x63, 0x4e, 0xb5, 0x43, 0x19, 0x63, 0xc4, 0xd3, 0x92, 0x83, 0x24, 0x83, 0x83, 0xf1, 0x1b, 0x18, 0x10, 0x93, 0x92, - 0x90, 0x0e, 0xa1, 0xb3, 0x32, 0x13, 0x51, 0xb9, 0x8b, 0xb7, 0x1b, 0x97, 0x35, 0x85, 0x22, 0xec, 0x04, 0x33, 0x95, - 0x52, 0x41, 0x20, 0x4d, 0xbe, 0x7b, 0x9d, 0x5a, 0x30, 0xb4, 0x70, 0x4d, 0x05, 0xe4, 0xb5, 0x5d, 0x0f, 0x9a, 0x7c, - 0xa4, 0x18, 0xfa, 0x2a, 0x35, 0xe2, 0x65, 0x06, 0x5f, 0xc3, 0xe6, 0xaf, 0x89, 0x92, 0x3c, 0x64, 0x22, 0xf6, 0x0a, - 0x3e, 0x11, 0xb2, 0x29, 0xd8, 0x99, 0x40, 0x3f, 0xb4, 0x2b, 0x7b, 0xe9, 0x6e, 0x51, 0xb9, 0xb4, 0x68, 0x6c, 0x25, - 0x6a, 0xd6, 0xfc, 0x30, 0xde, 0x4c, 0x61, 0x3f, 0x7b, 0x94, 0x40, 0x40, 0x9a, 0xca, 0x49, 0xaa, 0x79, 0x0f, 0xd3, - 0x23, 0x00, 0x09, 0x76, 0x3f, 0x81, 0x85, 0x7e, 0x53, 0x62, 0x82, 0x45, 0xd5, 0xd8, 0x6d, 0x06, 0x5a, 0x73, 0x46, - 0x9a, 0x6f, 0x86, 0x5a, 0x7b, 0x53, 0x59, 0xcf, 0x98, 0x1d, 0x60, 0xdb, 0xee, 0x66, 0x71, 0x98, 0x6e, 0x76, 0x8e, - 0x0c, 0xc1, 0x85, 0xc7, 0xff, 0x49, 0x89, 0x69, 0x20, 0xb9, 0xd4, 0x8d, 0x9f, 0x50, 0x87, 0xe1, 0xff, 0x16, 0xa4, - 0x80, 0x07, 0xb5, 0xd5, 0x58, 0x72, 0xee, 0x15, 0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, - 0x89, 0x62, 0x9e, 0x13, 0x00, 0xa3, 0xd8, 0xfc, 0x39, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4c, 0xed, 0xf6, 0x7d, 0x3f, - 0xca, 0xcf, 0xe8, 0x48, 0x45, 0x65, 0x73, 0x12, 0xf3, 0x6f, 0x0b, 0x30, 0xcd, 0x89, 0x0f, 0xf5, 0x5c, 0x47, 0xa1, - 0x00, 0x5f, 0xd9, 0x50, 0x6a, 0xb6, 0xd7, 0xbf, 0x75, 0xb6, 0x87, 0x92, 0x28, 0x82, 0x05, 0x1a, 0x74, 0x59, 0x83, - 0x2f, 0x60, 0x19, 0xdc, 0x91, 0x7e, 0x0a, 0xbe, 0x9f, 0xd6, 0xc1, 0x67, 0xec, 0x7f, 0x01, 0x68, 0x55, 0x60, 0x40, - 0xb9, 0xd3, 0x34, 0xac, 0x84, 0xb8, 0x44, 0x85, 0x59, 0xc5, 0xf9, 0xe3, 0x3a, 0xaf, 0x9b, 0x96, 0x25, 0x06, 0xe5, - 0x67, 0xae, 0xe1, 0xc6, 0xf7, 0x1a, 0xf9, 0xe3, 0x7b, 0x2f, 0x41, 0xb7, 0x13, 0x69, 0xef, 0xdf, 0xcf, 0xef, 0x91, - 0x85, 0x86, 0xf7, 0xc2, 0x66, 0xd0, 0x16, 0xe9, 0x92, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, - 0x30, 0x03, 0x0c, 0xc1, 0x63, 0xa7, 0x32, 0xf9, 0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, - 0xe4, 0xc5, 0x3a, 0xd9, 0x5e, 0x9c, 0xc3, 0xf3, 0xeb, 0xb8, 0x00, 0xea, 0x00, 0xfa, 0x15, 0x95, 0xc5, 0x06, 0x72, - 0x71, 0x53, 0xd6, 0x7a, 0x45, 0xa3, 0xd1, 0x8d, 0x5d, 0x58, 0x5d, 0x81, 0x4f, 0xa2, 0x74, 0x94, 0x88, 0x49, 0xcc, - 0xa4, 0xca, 0x15, 0xb9, 0x36, 0xba, 0x97, 0xb6, 0x68, 0x5e, 0x0a, 0x09, 0x5e, 0x11, 0xb8, 0x21, 0xf4, 0x95, 0xbe, - 0x5c, 0x6d, 0xa0, 0xe0, 0x51, 0x7b, 0x73, 0x11, 0x4c, 0x4c, 0x3c, 0x66, 0x48, 0x4d, 0xbf, 0x0e, 0xa7, 0x56, 0x16, - 0x4b, 0x0e, 0xbf, 0xce, 0x19, 0x6b, 0x28, 0x00, 0xe2, 0x93, 0x47, 0xeb, 0xdd, 0xa4, 0x37, 0x4a, 0x3b, 0x28, 0x8d, - 0x10, 0xdf, 0x55, 0xf8, 0xba, 0x0b, 0xc5, 0x57, 0xae, 0xba, 0xf7, 0x75, 0xcc, 0x8c, 0x0b, 0x46, 0x2f, 0xf9, 0x34, - 0x69, 0x5c, 0xbb, 0xa1, 0xbb, 0x3a, 0xdf, 0x7b, 0x5f, 0xca, 0xbc, 0x85, 0x63, 0x60, 0x93, 0x63, 0xe6, 0xbc, 0xf4, - 0xde, 0x1a, 0x27, 0xca, 0x3f, 0x98, 0x47, 0xbc, 0x72, 0x98, 0x55, 0x27, 0xc9, 0x3f, 0x0c, 0x7e, 0x08, 0xd6, 0xb7, - 0x34, 0x4e, 0x90, 0xbb, 0xea, 0x04, 0x99, 0x28, 0xb7, 0xa1, 0x37, 0xdc, 0xde, 0x5d, 0x05, 0x82, 0x38, 0x15, 0xd3, - 0x47, 0xe5, 0xb8, 0x7e, 0xb4, 0x40, 0xa5, 0x22, 0xe2, 0x73, 0x95, 0xbb, 0xb2, 0x36, 0x35, 0xd4, 0xe3, 0x3a, 0x99, - 0x85, 0xa6, 0x59, 0x91, 0x4b, 0xd9, 0xf4, 0x18, 0x99, 0x66, 0xa7, 0xda, 0xfc, 0xee, 0xda, 0x43, 0x3a, 0x86, 0xe6, - 0x62, 0xad, 0x16, 0xdc, 0xef, 0x2a, 0x0a, 0xef, 0x7a, 0xb1, 0x91, 0xca, 0x50, 0xb3, 0x1e, 0x45, 0x1f, 0xc7, 0x6d, - 0xe6, 0xf2, 0x28, 0xfb, 0xb3, 0x06, 0x80, 0xe9, 0x08, 0x8b, 0xee, 0xa6, 0x67, 0xec, 0x09, 0xf4, 0xf4, 0x44, 0x06, - 0x89, 0xde, 0xe8, 0x7c, 0xd5, 0x2a, 0xb1, 0x74, 0x05, 0x81, 0xdd, 0x1b, 0x32, 0x56, 0x25, 0xed, 0x96, 0xeb, 0x97, - 0xf3, 0x7c, 0x9e, 0xf3, 0xa5, 0x3c, 0x9f, 0x9a, 0x45, 0x77, 0xaf, 0xed, 0xde, 0x9c, 0x1a, 0x2a, 0xe6, 0x5a, 0xdd, - 0xe4, 0x37, 0x4c, 0xd7, 0xc1, 0x50, 0x8b, 0x20, 0xb3, 0xda, 0x55, 0x2f, 0xca, 0x72, 0xa3, 0x9e, 0xc9, 0xb1, 0x21, - 0x7c, 0x53, 0xe9, 0x0e, 0xd1, 0x0d, 0x53, 0x35, 0xd3, 0xf7, 0x8d, 0x6d, 0x21, 0xdb, 0xbc, 0xbc, 0x1a, 0xe5, 0x40, - 0x69, 0xb9, 0xbf, 0x4c, 0x18, 0xbe, 0xbf, 0xbe, 0xfe, 0x5e, 0xc8, 0xa9, 0xaa, 0xa3, 0xb7, 0x78, 0xad, 0x7b, 0x06, - 0x1b, 0xa5, 0x72, 0x22, 0x2e, 0xd8, 0xea, 0xc1, 0x9b, 0xbb, 0x57, 0xc0, 0x72, 0x01, 0xd8, 0x5d, 0x30, 0xa7, 0x31, - 0x54, 0xb5, 0x81, 0xbf, 0x5c, 0x3d, 0xd8, 0xaa, 0x3d, 0xfc, 0xe5, 0xe0, 0xcb, 0xe0, 0xc6, 0xc6, 0xc6, 0x36, 0xde, - 0xae, 0x25, 0x82, 0xbc, 0xc1, 0x03, 0x7d, 0xbc, 0xfa, 0x28, 0x68, 0xb9, 0x4a, 0x6c, 0x0f, 0x1c, 0x0a, 0x5b, 0x83, - 0x7c, 0x93, 0x32, 0x69, 0x38, 0x2f, 0x78, 0x36, 0x95, 0x33, 0x14, 0xf2, 0x9a, 0x8f, 0x83, 0xb6, 0x23, 0xfc, 0x1b, - 0x38, 0xb5, 0xe3, 0xe5, 0xc5, 0x27, 0xe8, 0x03, 0x9e, 0xae, 0x94, 0xa6, 0x22, 0x4e, 0x29, 0xb7, 0xe8, 0x72, 0x9d, - 0x07, 0x23, 0xc5, 0xc5, 0x04, 0x95, 0x8e, 0xbb, 0xb8, 0x71, 0x36, 0x72, 0xfa, 0x4b, 0xbc, 0xba, 0x48, 0x97, 0x8f, - 0x44, 0xb6, 0x6a, 0xe9, 0xfd, 0xac, 0x4f, 0xb7, 0xed, 0x29, 0xe3, 0x93, 0x6c, 0x44, 0x07, 0x33, 0x3e, 0x4e, 0x84, - 0xd7, 0x27, 0x46, 0xfa, 0x6e, 0x11, 0x98, 0x6e, 0x8e, 0x4d, 0x7e, 0x38, 0x5e, 0x6f, 0x36, 0x6b, 0xdc, 0xc1, 0x3b, - 0xe7, 0x93, 0xb3, 0x28, 0x31, 0xa2, 0xb2, 0xd0, 0xf0, 0x80, 0x56, 0x88, 0x9b, 0xf7, 0x4c, 0x60, 0x5c, 0x76, 0x45, - 0x52, 0xdb, 0x0d, 0x04, 0x2e, 0xf6, 0x38, 0x66, 0xc9, 0xc8, 0xf6, 0xa0, 0x3c, 0xd0, 0x17, 0xa3, 0xe9, 0x16, 0x30, - 0x2d, 0xaf, 0x9d, 0x5d, 0xa4, 0xb6, 0x57, 0x4d, 0x15, 0xc0, 0x2c, 0x59, 0x1e, 0x9f, 0x21, 0xeb, 0x7e, 0x0d, 0x5d, - 0xc4, 0x80, 0xb1, 0x71, 0x65, 0xce, 0x5d, 0xac, 0x5a, 0x11, 0xdf, 0x68, 0x22, 0x4d, 0xea, 0x43, 0xea, 0x7b, 0x14, - 0xd6, 0xea, 0x2a, 0x07, 0x09, 0xdc, 0x23, 0xef, 0x8e, 0xb8, 0xf4, 0xf4, 0x99, 0xc5, 0xb8, 0x4a, 0xdf, 0x52, 0xd7, - 0xe2, 0x9a, 0x61, 0xaf, 0x78, 0x00, 0xf6, 0x07, 0xc6, 0x2d, 0x62, 0x11, 0x6f, 0xe7, 0xb5, 0x14, 0xd6, 0xc6, 0x1c, - 0x68, 0x6e, 0xb8, 0xc1, 0xcf, 0xac, 0x5a, 0x33, 0x30, 0xc3, 0x8c, 0x33, 0x92, 0x0f, 0xc6, 0xbd, 0xaa, 0xb1, 0x23, - 0x57, 0x01, 0x44, 0xdf, 0x82, 0x2e, 0xc9, 0xe1, 0x95, 0x2c, 0x57, 0x9d, 0x21, 0xbf, 0x82, 0x75, 0xd6, 0x8b, 0x13, - 0x30, 0x93, 0xa6, 0xbc, 0xc4, 0xc4, 0x14, 0x71, 0xb9, 0x59, 0xc6, 0x3c, 0x4d, 0x9f, 0x45, 0x3b, 0x38, 0xb9, 0x91, - 0xc0, 0x11, 0xfb, 0xc6, 0x32, 0x34, 0x13, 0x36, 0x62, 0x22, 0x8d, 0x4a, 0x29, 0xe1, 0x03, 0xb9, 0xd4, 0x92, 0xbf, - 0xcc, 0xe5, 0xd5, 0x97, 0xdb, 0x04, 0x07, 0xe4, 0x35, 0xb0, 0x1c, 0x1a, 0xc7, 0x2d, 0x03, 0x89, 0x58, 0x0c, 0x88, - 0x51, 0xab, 0x72, 0x39, 0x19, 0xd5, 0xc9, 0x7c, 0x85, 0x5c, 0xa8, 0xc8, 0x83, 0x5b, 0x02, 0x25, 0x7f, 0x8e, 0xa9, - 0x83, 0x59, 0xa9, 0xdd, 0xb4, 0xd8, 0x24, 0x79, 0xcf, 0x0c, 0x48, 0xae, 0xbe, 0x86, 0x87, 0xc6, 0x2f, 0x5e, 0x99, - 0x53, 0xc2, 0x17, 0x65, 0x2c, 0x2d, 0x8d, 0xb9, 0xf4, 0xdf, 0xca, 0xfb, 0xb4, 0x12, 0xb0, 0x57, 0x20, 0xa6, 0x0c, - 0x5c, 0x62, 0xe3, 0x82, 0xa4, 0xbc, 0x96, 0xa7, 0xec, 0xbe, 0x86, 0xf2, 0x5d, 0x32, 0xe9, 0x2a, 0x95, 0xb5, 0xc6, - 0xaa, 0xfb, 0x79, 0xce, 0xf2, 0xab, 0x7d, 0x86, 0xb9, 0xc9, 0x68, 0x90, 0x2d, 0x99, 0xd9, 0x94, 0x5f, 0xed, 0xdd, - 0xf8, 0x95, 0x87, 0x92, 0x0e, 0xd5, 0x2a, 0xdd, 0xbc, 0x74, 0xc3, 0x31, 0x6e, 0xdc, 0x70, 0x04, 0xb0, 0x31, 0xec, - 0x54, 0x91, 0x5a, 0xe7, 0xbf, 0x2f, 0x87, 0x9f, 0x68, 0xaf, 0x1d, 0xe9, 0x5d, 0x77, 0xb4, 0x32, 0x3d, 0xfd, 0x06, - 0x54, 0x8d, 0x2c, 0xa1, 0x9b, 0x50, 0xc5, 0x64, 0x24, 0x4a, 0x4c, 0x57, 0x29, 0x8f, 0xfa, 0x1a, 0x71, 0x0e, 0xe2, - 0x86, 0xf2, 0x17, 0xff, 0x14, 0x5e, 0x9d, 0x04, 0x68, 0x44, 0x2d, 0xc6, 0x59, 0xca, 0x5b, 0xe3, 0x68, 0x1a, 0x27, - 0x57, 0xc1, 0x3c, 0x6e, 0x4d, 0xb3, 0x34, 0x2b, 0x66, 0xc0, 0x95, 0x5e, 0x71, 0x05, 0x36, 0xfc, 0xb4, 0x35, 0x8f, - 0xbd, 0x97, 0x2c, 0x39, 0x67, 0x3c, 0x1e, 0x46, 0x9e, 0xbd, 0x97, 0x83, 0x78, 0xb0, 0xde, 0x46, 0x79, 0x9e, 0x5d, - 0xd8, 0xde, 0x87, 0xec, 0x14, 0x98, 0xd6, 0x7b, 0x77, 0x79, 0x75, 0xc6, 0x52, 0xef, 0xe3, 0xe9, 0x3c, 0xe5, 0x73, - 0xaf, 0x88, 0xd2, 0xa2, 0x55, 0xb0, 0x3c, 0x1e, 0x83, 0x9a, 0x48, 0xb2, 0xbc, 0x85, 0xf9, 0xcf, 0x53, 0x16, 0x24, - 0xf1, 0xd9, 0x84, 0x5b, 0xa3, 0x28, 0xff, 0xd4, 0x6b, 0xb5, 0x66, 0x79, 0x3c, 0x8d, 0xf2, 0xab, 0x16, 0xb5, 0x08, - 0x3e, 0x6b, 0x6f, 0x47, 0x9f, 0x8f, 0x1f, 0xf6, 0x78, 0x0e, 0x7d, 0x63, 0xa4, 0x62, 0x00, 0xc2, 0xc7, 0xda, 0xde, - 0x69, 0x4f, 0x8b, 0x7b, 0xe2, 0x44, 0x29, 0x4a, 0x79, 0x79, 0xe2, 0x5d, 0x31, 0x80, 0xdb, 0x3f, 0xe5, 0xa9, 0x07, - 0xbe, 0x1c, 0xcf, 0xd2, 0xc5, 0x70, 0x9e, 0x17, 0x30, 0xc0, 0x2c, 0x8b, 0x53, 0xce, 0xf2, 0xde, 0x69, 0x96, 0x03, - 0xd9, 0x5a, 0x79, 0x34, 0x8a, 0xe7, 0x45, 0xf0, 0x70, 0x76, 0xd9, 0x43, 0x5b, 0xe1, 0x2c, 0xcf, 0xe6, 0xe9, 0x48, - 0xce, 0x15, 0xa7, 0xb0, 0x31, 0x62, 0x6e, 0x56, 0xd0, 0x97, 0x50, 0x00, 0xbe, 0x94, 0x45, 0x79, 0xeb, 0x0c, 0x3b, - 0xa3, 0xa1, 0xdf, 0x1e, 0xb1, 0x33, 0x2f, 0x3f, 0x3b, 0x8d, 0x9c, 0x4e, 0xf7, 0xb1, 0xa7, 0xfe, 0xf3, 0x77, 0x5c, - 0x30, 0xdc, 0x57, 0x16, 0x77, 0xda, 0xed, 0xbf, 0x71, 0x7b, 0x8d, 0x59, 0x08, 0xa0, 0xa0, 0x33, 0xbb, 0xb4, 0x8a, - 0x2c, 0x81, 0xf5, 0x59, 0xd5, 0xb3, 0x37, 0x03, 0xbf, 0x29, 0x4e, 0xcf, 0x82, 0xee, 0xec, 0xb2, 0x44, 0xec, 0x02, - 0x91, 0x90, 0x29, 0x91, 0x94, 0x6f, 0x8b, 0xdf, 0x0a, 0xf1, 0x93, 0xd5, 0x10, 0x77, 0x15, 0xc4, 0x15, 0xd5, 0x5b, - 0x23, 0xd8, 0x07, 0x44, 0xfe, 0x4e, 0x21, 0x00, 0x99, 0x80, 0x13, 0x98, 0x2b, 0x38, 0xe8, 0xe5, 0x37, 0x83, 0xd1, - 0x5d, 0x0d, 0xc6, 0x93, 0xdb, 0xc0, 0xc8, 0xd3, 0xd1, 0xa2, 0xbe, 0xae, 0x1d, 0x70, 0x4e, 0x7b, 0x13, 0x86, 0xfc, - 0x14, 0x74, 0xf1, 0xf9, 0x22, 0x1e, 0xf1, 0x89, 0x78, 0x24, 0x76, 0xbe, 0x10, 0x75, 0x3b, 0xed, 0xb6, 0x78, 0x2f, - 0x40, 0xa1, 0x05, 0x1d, 0x1f, 0x1b, 0x00, 0x13, 0x7d, 0xb1, 0xee, 0x23, 0x36, 0xdf, 0xdd, 0xfa, 0xa5, 0x1a, 0x8f, - 0xa9, 0xbc, 0x41, 0xa1, 0x22, 0xd4, 0x37, 0x5b, 0x30, 0xe3, 0x2d, 0xef, 0x77, 0xf4, 0x41, 0xd5, 0xe0, 0x3b, 0x46, - 0x5a, 0x2f, 0xe0, 0x9e, 0x99, 0x0b, 0xd4, 0x4b, 0xfb, 0x18, 0x92, 0x6a, 0xb5, 0x5c, 0xd0, 0x1b, 0x0c, 0x43, 0x48, - 0x74, 0x20, 0xe8, 0xe4, 0x83, 0x82, 0xbe, 0xa9, 0x91, 0xb9, 0x41, 0xe1, 0x64, 0x2e, 0x6c, 0xf9, 0x4c, 0xcb, 0x75, - 0x50, 0xd2, 0xe0, 0x65, 0x7f, 0xc1, 0x64, 0x03, 0x90, 0xde, 0x95, 0xa4, 0xe5, 0xd5, 0xd1, 0x93, 0x72, 0xf9, 0xb2, - 0x21, 0x51, 0x0e, 0x7c, 0x7d, 0x3e, 0x41, 0xbf, 0x5b, 0x7f, 0x28, 0xc6, 0x48, 0xa9, 0xd9, 0xb2, 0xdd, 0x01, 0xd3, - 0x59, 0x59, 0x98, 0x7d, 0xc6, 0x4a, 0x1c, 0xe5, 0x2b, 0xb0, 0xa4, 0x31, 0xf4, 0xfa, 0x73, 0x28, 0xdc, 0x34, 0xe5, - 0xa4, 0x6d, 0xdc, 0x74, 0xfd, 0x1f, 0x56, 0x3c, 0xa6, 0x6c, 0x67, 0x15, 0x1b, 0x07, 0xd7, 0xe5, 0x78, 0x28, 0xae, - 0x1d, 0x16, 0x98, 0x2d, 0xfe, 0xdb, 0x3d, 0x09, 0x47, 0xa3, 0x55, 0x64, 0xf3, 0x7c, 0x48, 0xa1, 0xc1, 0xe5, 0x10, - 0x83, 0x4d, 0x1a, 0xde, 0xf6, 0x98, 0x56, 0x2c, 0xe8, 0x77, 0xd7, 0xbe, 0xaa, 0xc0, 0xe9, 0xd4, 0x45, 0x5c, 0x6a, - 0x90, 0x61, 0x15, 0x05, 0x36, 0xea, 0xca, 0x11, 0x25, 0xd8, 0xd1, 0x85, 0x4f, 0x7f, 0x9e, 0xc6, 0x20, 0x5a, 0x8f, - 0xe3, 0x11, 0x5d, 0x74, 0x89, 0x47, 0x74, 0xf2, 0xd1, 0xa2, 0x4c, 0x27, 0x0c, 0xa5, 0x43, 0x81, 0x24, 0x38, 0x3e, - 0xcb, 0xcc, 0x19, 0xbb, 0x65, 0xe3, 0xe9, 0x85, 0xa1, 0x9b, 0x47, 0xd9, 0x34, 0x8a, 0xd3, 0x00, 0x3f, 0x48, 0xe2, - 0xe9, 0x11, 0x03, 0xec, 0xe2, 0xc1, 0x5f, 0x45, 0xfb, 0x8e, 0xeb, 0xff, 0x04, 0x82, 0x8b, 0xfa, 0x97, 0xd2, 0xf1, - 0xd3, 0x70, 0xa9, 0x73, 0xe5, 0x7a, 0x29, 0x08, 0x3b, 0xae, 0x8c, 0x64, 0x46, 0x81, 0x95, 0x5d, 0x4e, 0x7f, 0x06, - 0xad, 0x4e, 0xa0, 0xae, 0xfe, 0x9b, 0x2b, 0x60, 0x5c, 0x0c, 0xa8, 0x56, 0x85, 0x4a, 0xe4, 0x1b, 0xcc, 0x21, 0xf9, - 0xf3, 0xfa, 0x5a, 0x7f, 0x3c, 0xa0, 0x71, 0x81, 0x56, 0xa4, 0xdf, 0xc8, 0x4b, 0x98, 0x84, 0x85, 0x7e, 0x16, 0x98, - 0x56, 0xef, 0x1a, 0x5b, 0x4f, 0x6e, 0x25, 0x8c, 0x39, 0x9d, 0xa5, 0x4e, 0x0d, 0x0d, 0x3a, 0xbe, 0x58, 0x33, 0x95, - 0x5b, 0x46, 0xc4, 0xdc, 0x4f, 0x49, 0xe6, 0xd4, 0xaf, 0x3f, 0xe1, 0x55, 0xa7, 0x7a, 0xd6, 0x96, 0x62, 0xef, 0xe1, - 0xc9, 0xae, 0x10, 0x52, 0x16, 0xb1, 0x6e, 0x68, 0x83, 0xd4, 0xb0, 0xad, 0x3f, 0x0e, 0x81, 0xce, 0x9f, 0x42, 0x7b, - 0x63, 0xe1, 0xa8, 0xbb, 0x00, 0x39, 0xcc, 0xb5, 0x27, 0x14, 0x35, 0x7d, 0x44, 0xc0, 0xee, 0x6f, 0x2c, 0x78, 0xb9, - 0xbb, 0x25, 0x7a, 0xf7, 0x4f, 0xca, 0x82, 0x74, 0xaa, 0x19, 0xfb, 0xab, 0xa6, 0x10, 0x75, 0x30, 0x2c, 0x65, 0x1c, - 0xe3, 0xb8, 0xb9, 0xb6, 0x13, 0x45, 0x90, 0x5b, 0x32, 0x6e, 0x81, 0x19, 0x56, 0x51, 0x0e, 0x62, 0x44, 0xe7, 0xd0, - 0x14, 0x22, 0x6d, 0xa4, 0xb7, 0x0c, 0xc5, 0x09, 0x42, 0x30, 0xd8, 0x58, 0xc4, 0x65, 0xb8, 0xb1, 0x60, 0xe9, 0x30, - 0x1b, 0xb1, 0x8f, 0x1f, 0x5e, 0xe1, 0x35, 0x89, 0x2c, 0x45, 0x79, 0x9a, 0xb9, 0xe5, 0x09, 0x18, 0x58, 0x08, 0x69, - 0xae, 0xbe, 0x52, 0x03, 0xc0, 0x88, 0x58, 0x91, 0x45, 0xa3, 0x22, 0x28, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x10, 0x1c, - 0x59, 0x2c, 0x00, 0x13, 0x94, 0x7a, 0x31, 0xc0, 0x4f, 0xb4, 0xee, 0xc3, 0x40, 0xbb, 0x5b, 0xa2, 0x11, 0xe0, 0x9a, - 0x23, 0x1a, 0x15, 0xaa, 0x98, 0x55, 0x64, 0xa2, 0x3b, 0x8a, 0xcf, 0x35, 0x39, 0x29, 0xc5, 0xba, 0xbf, 0x9b, 0x44, - 0xa7, 0x2c, 0x81, 0x21, 0x81, 0xaf, 0xda, 0x30, 0x92, 0x78, 0xb5, 0x76, 0xe3, 0x74, 0x36, 0x97, 0x5f, 0x0b, 0x83, - 0x89, 0x3b, 0x78, 0x80, 0x8b, 0x97, 0x19, 0x06, 0xea, 0x44, 0x32, 0x90, 0x03, 0x00, 0x88, 0x74, 0x18, 0x82, 0xd0, - 0x55, 0xac, 0x02, 0xa5, 0xf1, 0x68, 0xb9, 0x0c, 0xf6, 0xf7, 0x0c, 0x4b, 0x53, 0x78, 0x9e, 0xc6, 0x29, 0x3e, 0x16, - 0xf8, 0x18, 0x5d, 0xe2, 0x63, 0x06, 0x8f, 0x1a, 0xf7, 0xbc, 0xb4, 0xff, 0xaa, 0xab, 0x92, 0xc9, 0x15, 0xb0, 0x34, - 0x01, 0xb2, 0xeb, 0x6b, 0x50, 0x5b, 0x9a, 0x04, 0xbb, 0x5b, 0x40, 0x2c, 0xe4, 0x1e, 0xf1, 0xed, 0x18, 0x66, 0x92, - 0x91, 0x15, 0xb3, 0x96, 0x28, 0xb7, 0xc8, 0x38, 0x08, 0xc1, 0x77, 0xcc, 0x9d, 0x86, 0x0d, 0xe4, 0xc9, 0x2c, 0x99, - 0x67, 0xf8, 0xe2, 0xda, 0x96, 0xf8, 0xb8, 0x87, 0x20, 0x0a, 0x3d, 0x22, 0x86, 0xba, 0x8c, 0xcb, 0xcf, 0xf6, 0xc4, - 0xa1, 0x8d, 0xb3, 0x80, 0x19, 0x8a, 0xca, 0x8c, 0x47, 0x71, 0x22, 0x1a, 0xaf, 0xc0, 0xa7, 0x91, 0xee, 0x48, 0xe8, - 0xec, 0x6e, 0x55, 0xb0, 0x01, 0xf0, 0x4a, 0x22, 0x88, 0x54, 0x4e, 0x5b, 0x94, 0x53, 0x0a, 0x80, 0xdc, 0xe6, 0xd5, - 0x27, 0x9d, 0x80, 0x29, 0xc0, 0x88, 0x1e, 0x1d, 0xd3, 0x6c, 0x83, 0x21, 0x12, 0x0b, 0x67, 0x6c, 0x6c, 0x5d, 0xfb, - 0x2f, 0xff, 0xfc, 0x0f, 0xb6, 0x27, 0x40, 0xcc, 0xc6, 0x63, 0x90, 0x72, 0xd6, 0xba, 0x86, 0xff, 0xeb, 0x1f, 0xff, - 0xef, 0xff, 0xf9, 0xaf, 0xba, 0x6d, 0x0a, 0x4d, 0x4f, 0x02, 0x71, 0xb4, 0xa0, 0x49, 0x4a, 0x29, 0x9e, 0xf6, 0x38, - 0x4a, 0x57, 0x80, 0x74, 0x08, 0x54, 0x9a, 0x31, 0x36, 0xf2, 0x6c, 0x0b, 0x34, 0x81, 0x78, 0x3e, 0x4e, 0xd8, 0x39, - 0x93, 0x1f, 0x96, 0xd1, 0x83, 0xe8, 0xca, 0x21, 0x58, 0x30, 0x5c, 0xde, 0x79, 0x95, 0xdb, 0x40, 0xd1, 0x52, 0x52, - 0xbc, 0x4e, 0x30, 0xcf, 0x36, 0x06, 0x6d, 0xce, 0xd1, 0xae, 0x0f, 0xeb, 0x81, 0x4a, 0xb5, 0x6d, 0x01, 0x2f, 0x99, - 0xbd, 0xab, 0x20, 0x5e, 0x82, 0xeb, 0x34, 0xc7, 0xa6, 0x29, 0x2b, 0x8a, 0x55, 0x60, 0x01, 0x4d, 0x3c, 0xbb, 0x6a, - 0x62, 0xd7, 0x3a, 0x00, 0x00, 0xdd, 0x9d, 0x1d, 0x31, 0x2d, 0x54, 0xb0, 0xf1, 0x18, 0x36, 0x38, 0xea, 0xb6, 0x84, - 0xe3, 0xb1, 0x45, 0xd8, 0xb7, 0xdf, 0x82, 0x2c, 0xb1, 0xc1, 0x3f, 0x74, 0xf5, 0x01, 0x34, 0x4d, 0xaf, 0x84, 0x9d, - 0x31, 0x87, 0xe8, 0x6c, 0x0c, 0xa3, 0x9f, 0x0c, 0xa4, 0xb2, 0xe1, 0xa7, 0x55, 0x8c, 0xb1, 0x96, 0x11, 0xfe, 0xfd, - 0x5f, 0xfe, 0xf1, 0xbf, 0xc1, 0xd8, 0xd4, 0x6f, 0x3d, 0x17, 0x40, 0xab, 0xff, 0x09, 0xad, 0xe6, 0xe9, 0x2d, 0xed, - 0xfe, 0xf2, 0xf7, 0xff, 0x1d, 0x9a, 0xd1, 0x45, 0x29, 0xe0, 0x13, 0x82, 0x68, 0x88, 0xb6, 0xe9, 0xaf, 0x02, 0xa9, - 0x36, 0xc8, 0xda, 0x99, 0xfe, 0x09, 0xc1, 0x2e, 0x78, 0x36, 0xbb, 0x11, 0x1c, 0x84, 0x7a, 0x98, 0x64, 0x05, 0xd3, - 0xf0, 0x08, 0x7d, 0xf2, 0xeb, 0x00, 0xa2, 0xb9, 0x66, 0xb0, 0x6b, 0x0b, 0x4b, 0x8f, 0x23, 0x56, 0x68, 0xd5, 0x38, - 0x8d, 0x05, 0x2c, 0x18, 0x27, 0x74, 0x28, 0xdc, 0x03, 0x4b, 0x26, 0x9e, 0xe0, 0x81, 0x04, 0x9c, 0x5b, 0xff, 0xf8, - 0xda, 0xea, 0xc1, 0x34, 0xc3, 0x89, 0xb1, 0x44, 0x84, 0x4b, 0x8d, 0x00, 0x7f, 0x41, 0x08, 0x1f, 0xeb, 0xe7, 0xe8, - 0x52, 0x3f, 0xa3, 0xa0, 0x16, 0x13, 0x80, 0xbe, 0x9d, 0xa2, 0x31, 0x66, 0xce, 0x20, 0xb2, 0x33, 0x2a, 0xf7, 0xde, - 0x48, 0xf2, 0x11, 0xc2, 0xf8, 0x18, 0x73, 0x61, 0xf1, 0xe6, 0xd3, 0x3c, 0x67, 0xc7, 0x49, 0x76, 0x81, 0x31, 0x43, - 0x24, 0xd2, 0xba, 0xfa, 0xf2, 0xdf, 0xfe, 0xd5, 0xf7, 0xff, 0xed, 0x5f, 0xd7, 0x34, 0x98, 0xc0, 0x9e, 0x00, 0x23, - 0x9f, 0x87, 0x9a, 0xce, 0x0d, 0xb4, 0x56, 0x0f, 0x8a, 0x78, 0xae, 0xae, 0x91, 0x88, 0x63, 0xa9, 0xc4, 0x5b, 0x3e, - 0x12, 0xda, 0x9a, 0x29, 0x6e, 0x9f, 0x05, 0x21, 0x5b, 0x33, 0x0d, 0x56, 0xdd, 0x32, 0xcf, 0x89, 0x1b, 0xdc, 0x40, - 0x97, 0x5f, 0x89, 0xf1, 0x6a, 0x30, 0x6e, 0x85, 0xc0, 0x03, 0x6d, 0x26, 0xf4, 0xdd, 0x33, 0xa1, 0xad, 0x02, 0xb1, - 0x0c, 0x52, 0x77, 0xd5, 0x00, 0xf2, 0xac, 0x03, 0x9a, 0x80, 0x9a, 0xc4, 0x95, 0xad, 0x40, 0xe6, 0xd6, 0x69, 0xde, - 0x7f, 0x83, 0x97, 0x1d, 0x2d, 0xec, 0x8d, 0x96, 0x42, 0x41, 0x86, 0x0d, 0x27, 0xc3, 0x46, 0x6a, 0x54, 0xd3, 0xa6, - 0x40, 0xc7, 0x2f, 0x5b, 0x6d, 0x3b, 0x1c, 0x63, 0xf7, 0x9a, 0xf6, 0xe7, 0x52, 0xfb, 0xc7, 0xd2, 0xde, 0x97, 0xda, - 0x1f, 0x3f, 0x69, 0xd3, 0xd0, 0xfe, 0xf1, 0x5a, 0xed, 0x8f, 0x94, 0x1b, 0xe0, 0xc8, 0xa1, 0xbd, 0x89, 0xd1, 0x2d, - 0xc3, 0xd6, 0xe0, 0x68, 0x67, 0x0d, 0x27, 0x6c, 0xf8, 0x49, 0x9a, 0x59, 0x84, 0x00, 0x86, 0x77, 0xb4, 0x31, 0x29, - 0x30, 0x00, 0x93, 0xe1, 0xa4, 0xd4, 0x9b, 0x1e, 0x1f, 0x8d, 0x09, 0xb8, 0xbb, 0x18, 0x33, 0x14, 0xfd, 0xb0, 0x66, - 0x5f, 0xb1, 0x72, 0x0b, 0xc7, 0x11, 0x1b, 0x46, 0x3c, 0x03, 0x66, 0x5b, 0x38, 0xd8, 0x89, 0xb7, 0x10, 0xc1, 0xc2, - 0xc0, 0x7e, 0xff, 0x6e, 0xff, 0xc0, 0xf6, 0x4e, 0xb3, 0xd1, 0x55, 0x60, 0x83, 0x33, 0x06, 0xd6, 0x94, 0xeb, 0xf3, - 0x09, 0x4b, 0x1d, 0xe5, 0xf9, 0x64, 0x09, 0xb8, 0x9a, 0xd9, 0x99, 0xf8, 0xb6, 0x45, 0xf3, 0xa0, 0x03, 0x08, 0x4b, - 0x1f, 0xbf, 0xec, 0xef, 0x72, 0xf1, 0x5d, 0x58, 0x9e, 0xe3, 0x63, 0x1f, 0x53, 0x3d, 0x76, 0xb7, 0xe0, 0x01, 0x5f, - 0xf6, 0x51, 0xef, 0xd1, 0xdb, 0xc6, 0x62, 0xc9, 0x6d, 0x18, 0xe0, 0x10, 0x93, 0xbe, 0x40, 0xa1, 0xa0, 0x56, 0x27, - 0x01, 0x22, 0x06, 0x8f, 0x30, 0xd6, 0x96, 0x1a, 0x17, 0x21, 0x54, 0xfd, 0xb5, 0xe3, 0x52, 0xd9, 0xad, 0x34, 0xef, - 0x08, 0xcd, 0x52, 0x72, 0x5c, 0xb0, 0xf7, 0x48, 0x97, 0x08, 0x53, 0x87, 0x8a, 0xd6, 0x41, 0xa0, 0x6b, 0x2a, 0x73, - 0x45, 0x74, 0x30, 0x80, 0x21, 0x33, 0x57, 0x00, 0x02, 0x7f, 0x09, 0xed, 0x13, 0xf3, 0xfb, 0x6f, 0xe2, 0x53, 0x4d, - 0x9a, 0x38, 0x87, 0x7f, 0xf2, 0xae, 0x98, 0x77, 0x75, 0x42, 0x2d, 0x55, 0xb0, 0x01, 0xa3, 0x60, 0x18, 0x94, 0x69, - 0xab, 0xa8, 0x12, 0xd8, 0x69, 0x49, 0x34, 0x2b, 0x58, 0xa0, 0x1e, 0x64, 0xdc, 0x01, 0xc3, 0x17, 0xcb, 0x81, 0x1e, - 0xd3, 0x9e, 0x2b, 0xf9, 0x64, 0x61, 0x06, 0x26, 0x1e, 0xb5, 0xdb, 0x3d, 0xbc, 0x54, 0xd1, 0x8a, 0xc0, 0x3a, 0x48, - 0x83, 0x84, 0x8d, 0x79, 0xc9, 0xf1, 0xd6, 0xfe, 0x42, 0x45, 0x82, 0xfc, 0xee, 0x4e, 0xce, 0xa6, 0x96, 0x8f, 0xff, - 0xbf, 0x6d, 0xec, 0x51, 0x90, 0xf2, 0x49, 0x8b, 0xae, 0xf1, 0xe0, 0x15, 0x49, 0x80, 0xc8, 0x7c, 0x5f, 0x18, 0x13, - 0x0d, 0x19, 0x46, 0xc9, 0x4a, 0x9e, 0x83, 0xbc, 0xf7, 0x78, 0x6e, 0xb6, 0x03, 0x39, 0xbd, 0x14, 0x2a, 0x5b, 0x0e, - 0xd6, 0x6c, 0xbb, 0xd2, 0x3f, 0x5a, 0x6e, 0xac, 0x22, 0x5e, 0xf5, 0xb7, 0x25, 0x0a, 0x19, 0xb1, 0xb9, 0x52, 0xa8, - 0xa8, 0x85, 0xe8, 0x61, 0xe2, 0xb4, 0x1c, 0xb5, 0xbb, 0xd5, 0x62, 0x2e, 0x49, 0x5c, 0x1c, 0x92, 0xb8, 0x20, 0xf1, - 0x77, 0xb4, 0x10, 0x73, 0x0f, 0xa3, 0x64, 0xe8, 0x20, 0x00, 0x56, 0xcb, 0x7a, 0x02, 0xd4, 0x74, 0x55, 0xe4, 0xc8, - 0x7f, 0x8c, 0xc4, 0x2d, 0x85, 0xb0, 0x5c, 0x41, 0xa5, 0x93, 0xa3, 0xb2, 0xec, 0x31, 0xe6, 0x1c, 0x7e, 0x90, 0x97, - 0x40, 0xc4, 0xdd, 0x5f, 0xfd, 0xfd, 0xc4, 0x76, 0xe9, 0x1e, 0x79, 0x3f, 0x1b, 0x1f, 0xa5, 0xb3, 0x15, 0xb3, 0xdb, - 0x1e, 0x2c, 0x83, 0xd9, 0x53, 0x7e, 0x42, 0xf2, 0xa6, 0xbe, 0x26, 0x9b, 0x53, 0xff, 0x9f, 0x43, 0x1c, 0xe1, 0x8d, - 0x63, 0xa3, 0x89, 0x4e, 0x23, 0x5f, 0xb5, 0x88, 0x3f, 0x6d, 0xec, 0x2a, 0x8e, 0x40, 0xbe, 0x5e, 0x17, 0xc9, 0xfa, - 0xe6, 0xf6, 0x48, 0x56, 0x71, 0xc7, 0x48, 0xd6, 0x37, 0xbf, 0x73, 0x24, 0xeb, 0x6b, 0x33, 0x92, 0x85, 0x02, 0xfa, - 0xd5, 0xaf, 0x89, 0x36, 0xe5, 0xd9, 0x45, 0x11, 0x76, 0x64, 0xe6, 0x04, 0xc8, 0x3a, 0x0c, 0x3b, 0xfd, 0xf5, 0x23, - 0x4c, 0x30, 0x51, 0x23, 0xbe, 0x44, 0x01, 0x25, 0x91, 0xec, 0x09, 0x6a, 0x45, 0x86, 0x73, 0xda, 0x3a, 0xab, 0xb2, - 0xf5, 0x50, 0x5d, 0x23, 0x03, 0xd7, 0xd7, 0xd5, 0xa1, 0xb6, 0xae, 0x0a, 0xf8, 0x04, 0xf4, 0x1d, 0x58, 0xdd, 0xb1, - 0xbb, 0xa9, 0xd2, 0xf9, 0xcc, 0x11, 0x7a, 0xea, 0x94, 0x46, 0x30, 0xd1, 0xc2, 0xfe, 0x2f, 0x87, 0x9d, 0xde, 0x76, - 0x67, 0x0a, 0xbd, 0x41, 0x81, 0xc3, 0x5b, 0xbb, 0xb7, 0xbd, 0x8d, 0x6f, 0x17, 0xea, 0xad, 0x8b, 0x6f, 0xb1, 0x7a, - 0xdb, 0xc1, 0xb7, 0xa1, 0x7a, 0x7b, 0x84, 0x6f, 0x23, 0xf5, 0xf6, 0x18, 0xdf, 0xce, 0xed, 0xf2, 0x90, 0x6b, 0xe0, - 0x1e, 0x03, 0x5f, 0x91, 0x37, 0x13, 0xa8, 0x32, 0xd8, 0xf4, 0x78, 0xfd, 0x32, 0x3a, 0x0b, 0x62, 0x4f, 0x78, 0x97, - 0x41, 0xee, 0x5d, 0x80, 0xc6, 0x09, 0x28, 0xdb, 0xf0, 0x39, 0x7e, 0x87, 0x03, 0x9c, 0xa4, 0x83, 0x78, 0xca, 0xd4, - 0x07, 0x89, 0x15, 0xd6, 0x60, 0xc0, 0x1e, 0xb6, 0x8f, 0xca, 0x9e, 0x5e, 0x27, 0x11, 0xcf, 0x52, 0xd9, 0x1c, 0xb4, - 0x72, 0x55, 0x9d, 0x98, 0xae, 0xa5, 0x57, 0x78, 0x8d, 0xfe, 0x32, 0xe2, 0x11, 0x63, 0x30, 0xcc, 0x5a, 0x97, 0xe0, - 0xc1, 0xae, 0xd4, 0x69, 0x08, 0x91, 0xd6, 0x69, 0x84, 0x93, 0x7e, 0x3b, 0x88, 0xce, 0xf4, 0xf3, 0x1b, 0xb0, 0xb4, - 0xa3, 0x33, 0xd9, 0x72, 0xbd, 0x0e, 0x23, 0x10, 0x4d, 0xfd, 0xa5, 0x80, 0x20, 0x53, 0x0c, 0x96, 0x06, 0x3d, 0x69, - 0xa9, 0xbf, 0x90, 0x3a, 0x75, 0x8d, 0x46, 0xd3, 0xd7, 0x8b, 0x80, 0xa2, 0x55, 0xc1, 0x2e, 0x18, 0xfc, 0x54, 0x2a, - 0x28, 0x0c, 0x15, 0x58, 0x20, 0xaa, 0xd7, 0xa8, 0x32, 0x1d, 0x6c, 0x58, 0xab, 0xd0, 0x2c, 0xa5, 0xcb, 0xcc, 0xd3, - 0x1d, 0x7d, 0xb4, 0xb3, 0x2c, 0x5e, 0x3f, 0xeb, 0x0c, 0xf1, 0x1f, 0x29, 0xbc, 0x3f, 0x1b, 0x8f, 0xc7, 0x37, 0xea, - 0xb6, 0xcf, 0x46, 0x63, 0xd6, 0x65, 0x3b, 0x3d, 0x8c, 0xfc, 0xb7, 0xa4, 0x38, 0xed, 0x94, 0x44, 0xbb, 0xc5, 0xdd, - 0x1a, 0xa3, 0xe4, 0x05, 0x75, 0x77, 0x77, 0x25, 0x58, 0x02, 0x55, 0x16, 0x20, 0xfc, 0xcf, 0xe2, 0x34, 0x68, 0x97, - 0xfe, 0xb9, 0xd4, 0x1a, 0x9f, 0x3d, 0x79, 0xf2, 0xa4, 0xf4, 0x47, 0xea, 0xad, 0x3d, 0x1a, 0x95, 0xfe, 0x70, 0xa1, - 0xd1, 0x68, 0xb7, 0xc7, 0xe3, 0xd2, 0x8f, 0x55, 0xc1, 0x76, 0x77, 0x38, 0xda, 0xee, 0x96, 0xfe, 0x85, 0xd1, 0xa2, - 0xf4, 0x99, 0x7c, 0xcb, 0xd9, 0xa8, 0x76, 0x7c, 0xf0, 0xb8, 0x0d, 0x95, 0x82, 0xd1, 0x16, 0xe8, 0x5d, 0x8a, 0xc7, - 0x20, 0x9a, 0xf3, 0x0c, 0x0c, 0xbb, 0xb2, 0x57, 0x80, 0x7c, 0x1e, 0x4b, 0x09, 0x2f, 0xbe, 0xf7, 0x8b, 0x52, 0xfd, - 0x95, 0x29, 0xd5, 0x91, 0x99, 0x49, 0x9a, 0x17, 0xa4, 0x0d, 0x9a, 0xd5, 0xc8, 0x59, 0x54, 0xfd, 0x2a, 0x2c, 0x2a, - 0x61, 0x8f, 0xd2, 0x06, 0x5b, 0x0a, 0x19, 0xff, 0xc3, 0x3a, 0x19, 0xff, 0xfd, 0xed, 0x32, 0xfe, 0xf4, 0x6e, 0x22, - 0xfe, 0xfb, 0xdf, 0x59, 0xc4, 0xff, 0x60, 0x8a, 0x78, 0x21, 0xc4, 0xf6, 0xc0, 0x74, 0x26, 0x9b, 0xf9, 0x34, 0xbb, - 0x6c, 0xe1, 0x96, 0xc8, 0x6d, 0x92, 0x9e, 0xd3, 0x3b, 0x09, 0xff, 0x15, 0xf9, 0x60, 0x6a, 0x30, 0xe3, 0xe3, 0xc1, - 0x3c, 0x3b, 0x3b, 0x4b, 0x98, 0x92, 0xf1, 0x46, 0x05, 0x99, 0xe3, 0xef, 0xd2, 0xd0, 0x7e, 0x07, 0x9e, 0xb1, 0x51, - 0x32, 0x1e, 0x43, 0xd1, 0x78, 0x6c, 0xab, 0x7c, 0x69, 0x90, 0x67, 0xd4, 0xea, 0x6d, 0xad, 0x84, 0x5a, 0x7d, 0xf1, - 0x85, 0x59, 0x66, 0x16, 0xc8, 0x90, 0x9e, 0x69, 0x8c, 0xc8, 0x9a, 0x51, 0x5c, 0xe0, 0x1e, 0xac, 0x3e, 0x76, 0x8c, - 0xf6, 0xce, 0x14, 0x94, 0x4a, 0x3c, 0xc4, 0x73, 0x91, 0xe6, 0x87, 0x65, 0x44, 0x6e, 0xfb, 0x32, 0x72, 0xd5, 0xf9, - 0xb7, 0xf1, 0x0d, 0xc3, 0xea, 0xcc, 0x1b, 0x16, 0x5f, 0xe6, 0xb7, 0x3c, 0xbd, 0x7a, 0x35, 0x72, 0xf6, 0xc0, 0x1a, - 0x8e, 0x8b, 0x77, 0x69, 0x23, 0x6f, 0x50, 0x80, 0x1d, 0x86, 0x26, 0xa6, 0xa5, 0x20, 0x58, 0x75, 0x81, 0xa2, 0xaa, - 0xec, 0x19, 0x9d, 0x64, 0x7a, 0x19, 0x0e, 0x39, 0xa8, 0x91, 0x25, 0x30, 0x07, 0x93, 0xba, 0x90, 0x3e, 0x66, 0x2f, - 0x92, 0x6e, 0xce, 0xe5, 0x57, 0xcf, 0xe9, 0x70, 0x66, 0x21, 0xf5, 0x87, 0x4c, 0xc7, 0xa8, 0x7a, 0xe2, 0x79, 0x88, - 0x98, 0x61, 0x54, 0xaa, 0x33, 0x10, 0x20, 0xdc, 0x0c, 0x3f, 0xd1, 0x24, 0x86, 0x50, 0x07, 0x05, 0x15, 0xf5, 0xae, - 0xaf, 0xcd, 0x2f, 0x85, 0xd6, 0xbe, 0x2a, 0xd9, 0xe0, 0x01, 0x86, 0x9f, 0xf8, 0x45, 0x6d, 0x90, 0xcd, 0xb9, 0xe3, - 0x50, 0x2b, 0xc7, 0x2d, 0xbd, 0x9d, 0x76, 0x1b, 0x54, 0x8c, 0x2f, 0xbe, 0x03, 0xe5, 0xe8, 0xce, 0x12, 0xdf, 0x75, - 0xe7, 0x12, 0x4b, 0xdf, 0x65, 0xd3, 0x24, 0xc6, 0x0f, 0xc7, 0x08, 0x44, 0x8d, 0xbb, 0x43, 0x6a, 0x11, 0x9b, 0xef, - 0xbe, 0xf2, 0x1d, 0x0d, 0xc2, 0xba, 0xab, 0x38, 0x58, 0xe6, 0xd6, 0xd6, 0x0b, 0xb1, 0xad, 0xb0, 0x6a, 0x96, 0xc1, - 0xb9, 0x45, 0x67, 0x16, 0x17, 0x46, 0x00, 0xbf, 0xb6, 0x0d, 0x4a, 0x15, 0xc1, 0x17, 0x61, 0xf8, 0x3d, 0x0c, 0x36, - 0x0b, 0xc7, 0x5b, 0x01, 0x5d, 0x77, 0x79, 0x0d, 0xc8, 0xd1, 0x19, 0xd6, 0x8c, 0xae, 0xaa, 0x54, 0x41, 0x69, 0x1e, - 0xc1, 0x18, 0xc8, 0x50, 0x24, 0x1d, 0xd6, 0x38, 0x15, 0x7a, 0x0b, 0xa6, 0x21, 0x01, 0xac, 0xfd, 0x3a, 0x74, 0x6b, - 0x6c, 0x05, 0xb6, 0x90, 0x16, 0xa0, 0xf4, 0xb0, 0x43, 0xdf, 0xaa, 0x81, 0x9e, 0x2e, 0x07, 0xe0, 0x6f, 0x74, 0xf2, - 0x4e, 0xfc, 0xe2, 0xc2, 0x83, 0xff, 0xac, 0x3f, 0x2c, 0x40, 0xca, 0x9f, 0x7e, 0x8a, 0x39, 0xd8, 0xd4, 0xb3, 0x16, - 0x86, 0x5f, 0x28, 0x4e, 0x2b, 0xd5, 0x21, 0x1d, 0x45, 0x8b, 0x2b, 0x63, 0xbd, 0x79, 0x81, 0xbe, 0x20, 0x39, 0x3d, - 0x41, 0x9a, 0xa5, 0xac, 0x57, 0x4f, 0x39, 0x30, 0xfd, 0x0e, 0x45, 0xac, 0xa3, 0x45, 0x86, 0xbe, 0x23, 0xbf, 0x02, - 0xdf, 0x51, 0xa8, 0xd1, 0xb6, 0x72, 0x3a, 0xda, 0x2b, 0xdb, 0x07, 0x92, 0xb6, 0x9b, 0x64, 0x2d, 0xe4, 0xcb, 0xce, - 0xd5, 0x3a, 0xe7, 0xe8, 0xb6, 0x03, 0x78, 0x0c, 0x0a, 0xab, 0xff, 0x8c, 0xcc, 0x85, 0x66, 0x31, 0x1d, 0xc0, 0xdf, - 0x05, 0xb2, 0x20, 0x1a, 0xe3, 0x17, 0x16, 0xef, 0xd2, 0xf2, 0x94, 0xb2, 0x5f, 0x17, 0xa8, 0xd6, 0x83, 0xce, 0x13, - 0xf0, 0xf6, 0xee, 0x3c, 0xfc, 0xcd, 0xe8, 0x97, 0x92, 0x46, 0xea, 0x12, 0xb3, 0x6d, 0xf7, 0x50, 0x5e, 0x24, 0xd1, - 0x15, 0x38, 0x9d, 0x64, 0x63, 0x9c, 0x62, 0xf4, 0xb8, 0x37, 0xcb, 0x64, 0x26, 0x49, 0xce, 0x12, 0xfa, 0x19, 0x13, - 0xb9, 0x14, 0xdb, 0x8f, 0x66, 0x97, 0x6a, 0x35, 0x3a, 0x8d, 0x0c, 0x91, 0xdf, 0x35, 0x11, 0x64, 0x7d, 0xe6, 0x49, - 0x3d, 0x99, 0x61, 0x07, 0x60, 0x10, 0x86, 0x4d, 0x2b, 0x17, 0x50, 0xb5, 0xa1, 0xc4, 0x48, 0x85, 0xa9, 0x06, 0xb2, - 0xfc, 0x6d, 0x50, 0x95, 0x51, 0xc1, 0x7a, 0xf8, 0xa9, 0xcb, 0x18, 0x5c, 0x5b, 0x69, 0x3c, 0x4d, 0xe3, 0xd1, 0x28, - 0x61, 0x3d, 0x65, 0x1f, 0x59, 0x9d, 0x47, 0x98, 0x49, 0x62, 0x2e, 0x59, 0x7d, 0x55, 0x0c, 0xe2, 0x69, 0x3a, 0x45, - 0xa7, 0x60, 0xaf, 0xe1, 0xf7, 0x2a, 0x57, 0x92, 0x53, 0xa6, 0x58, 0xb4, 0x2b, 0xe2, 0xd1, 0x73, 0x1d, 0x97, 0x1d, - 0x30, 0x16, 0x69, 0xc1, 0xdb, 0x3d, 0x9e, 0xcd, 0x82, 0xd6, 0x76, 0x1d, 0x11, 0xac, 0xd2, 0x28, 0x78, 0x2b, 0xd0, - 0xf2, 0xd0, 0x3a, 0x10, 0x5a, 0xce, 0xf2, 0x3b, 0xb2, 0x8c, 0x06, 0xc0, 0x6f, 0x22, 0xea, 0xa2, 0xb2, 0x8e, 0xcc, - 0x5f, 0x67, 0xb7, 0x7c, 0xbe, 0x7a, 0xb7, 0x7c, 0xae, 0x76, 0xcb, 0xcd, 0x1c, 0xfb, 0xd9, 0xb8, 0x83, 0xff, 0xf4, - 0x2a, 0x84, 0x60, 0x55, 0x80, 0x1c, 0x16, 0xda, 0xc5, 0xad, 0x2e, 0xfc, 0x8f, 0x86, 0x6e, 0x7b, 0xf8, 0x8f, 0x0f, - 0x16, 0x60, 0xdb, 0xc2, 0x42, 0xfc, 0xaf, 0x5d, 0xab, 0xea, 0x3c, 0xc4, 0x3a, 0xec, 0xb5, 0xb3, 0x5c, 0xd7, 0xbd, - 0x79, 0xd3, 0x82, 0xbc, 0xe2, 0x4e, 0xa0, 0x84, 0x31, 0xb8, 0x6a, 0xd1, 0xe9, 0x29, 0x94, 0x8e, 0xb3, 0xe1, 0xbc, - 0xf8, 0x5b, 0x09, 0xbf, 0x24, 0xe2, 0x8d, 0x5b, 0xba, 0x31, 0x8e, 0xea, 0x2a, 0xd2, 0x92, 0xd4, 0x08, 0x0b, 0xbd, - 0x4e, 0x41, 0x01, 0x8c, 0xc9, 0x9c, 0xae, 0xff, 0x70, 0xc5, 0x26, 0xf8, 0xff, 0xb2, 0x36, 0x2b, 0x91, 0xf9, 0x8f, - 0x12, 0xe3, 0x46, 0x22, 0xfc, 0x2a, 0x1a, 0x98, 0x6b, 0xd8, 0x7e, 0xb2, 0x1a, 0xdc, 0x43, 0x35, 0xd3, 0x91, 0x52, - 0x0a, 0x52, 0xef, 0x80, 0x17, 0x10, 0xcd, 0x13, 0x7e, 0xf3, 0xa8, 0xeb, 0x38, 0x63, 0x69, 0xd4, 0x1b, 0x04, 0x7a, - 0xd5, 0xf6, 0x8e, 0x52, 0xfa, 0xb3, 0xcf, 0x1f, 0xe2, 0x3f, 0x22, 0x70, 0x76, 0x5a, 0xf9, 0x46, 0x22, 0x36, 0x80, - 0xbe, 0xd1, 0xb4, 0xe6, 0xfc, 0x08, 0x0d, 0x4e, 0xfe, 0xcf, 0x5d, 0x5b, 0xa3, 0xb1, 0x7e, 0xa7, 0xe6, 0xd2, 0x2a, - 0xfd, 0x55, 0xad, 0x7f, 0xdd, 0xe0, 0x77, 0x6c, 0x3b, 0x14, 0x0e, 0x41, 0xbd, 0xad, 0x8c, 0x07, 0x2e, 0x35, 0x56, - 0x14, 0xbf, 0x6b, 0xfb, 0xca, 0x24, 0xa6, 0x1e, 0xd3, 0xf0, 0x54, 0x3b, 0x91, 0xf2, 0xf0, 0x1e, 0x7b, 0x08, 0x3f, - 0xf2, 0x4b, 0x16, 0x3e, 0xc0, 0xaf, 0xb1, 0x59, 0x97, 0xd3, 0x24, 0x05, 0xb3, 0x6a, 0xc2, 0xf9, 0x2c, 0xd8, 0xda, - 0xba, 0xb8, 0xb8, 0xf0, 0x2f, 0xb6, 0xfd, 0x2c, 0x3f, 0xdb, 0xea, 0xb6, 0xdb, 0x6d, 0xfc, 0x88, 0x96, 0x6d, 0x9d, - 0xc7, 0xec, 0xe2, 0x29, 0xb8, 0x1f, 0xf6, 0x63, 0xeb, 0x89, 0xf5, 0x78, 0xdb, 0xda, 0x79, 0x64, 0x5b, 0xa4, 0x00, - 0xa0, 0x64, 0xdb, 0xb6, 0x84, 0x02, 0x08, 0x6d, 0x28, 0xee, 0xef, 0x9e, 0x29, 0x1b, 0x0e, 0x2f, 0x29, 0x08, 0x0b, - 0x09, 0xfc, 0xb7, 0xec, 0x13, 0xab, 0x6f, 0x75, 0x51, 0xd6, 0x92, 0x6a, 0x44, 0xbd, 0xe2, 0x7e, 0x1f, 0x46, 0xb3, - 0x80, 0xd8, 0xc8, 0x2c, 0xc4, 0x30, 0x99, 0x28, 0xa5, 0x29, 0xd0, 0x2e, 0x3d, 0x85, 0x27, 0xcc, 0x6a, 0xb3, 0xe0, - 0xf9, 0x4d, 0xf7, 0x31, 0xe8, 0xb8, 0xf3, 0xd6, 0xc3, 0x61, 0xbb, 0xd5, 0xb1, 0x3a, 0xad, 0xae, 0xff, 0xd8, 0xea, - 0x8a, 0xff, 0x83, 0x8c, 0xdc, 0xb6, 0x3a, 0xf0, 0xb4, 0x6d, 0xc1, 0xfb, 0xf9, 0x43, 0x91, 0x5b, 0x12, 0xd9, 0x5b, - 0xfd, 0x5d, 0xfc, 0x4d, 0x29, 0x40, 0xea, 0x73, 0x5b, 0xfc, 0x0a, 0x9e, 0xfd, 0x99, 0x59, 0xda, 0x79, 0xb2, 0xb2, - 0xb8, 0xfb, 0x78, 0x65, 0xf1, 0xf6, 0xa3, 0x95, 0xc5, 0x0f, 0x77, 0xea, 0xc5, 0x5b, 0x67, 0xa2, 0x4a, 0xcb, 0x85, - 0xd0, 0x9e, 0x46, 0xc0, 0x28, 0x97, 0x4e, 0x07, 0xe0, 0x6c, 0x5b, 0x2d, 0xfc, 0xf3, 0xb8, 0xeb, 0xea, 0x5e, 0xa7, - 0xd8, 0x4b, 0x63, 0xf9, 0xf8, 0x09, 0x60, 0xf9, 0xb2, 0xfb, 0x68, 0x88, 0xed, 0x08, 0x51, 0xf8, 0xef, 0x7c, 0xfb, - 0xc9, 0x10, 0x34, 0x82, 0x85, 0xff, 0xc1, 0x3f, 0x93, 0x9d, 0xee, 0x50, 0xbc, 0xb4, 0xb1, 0xfe, 0xdb, 0xce, 0xe3, - 0x02, 0x9a, 0xe2, 0x3f, 0xbf, 0x68, 0x13, 0x1a, 0x0d, 0x78, 0x73, 0xdc, 0x87, 0x40, 0xa3, 0x27, 0x93, 0xae, 0xff, - 0xf9, 0xf9, 0x63, 0xff, 0xc9, 0xa4, 0xf3, 0xf8, 0x5b, 0xf1, 0x96, 0x00, 0x05, 0x3f, 0xc7, 0xff, 0xbe, 0xdd, 0x6e, - 0x4f, 0x5a, 0x1d, 0xff, 0xc9, 0xf9, 0xb6, 0xbf, 0x9d, 0xb4, 0x1e, 0xf9, 0x4f, 0xf0, 0xbf, 0x6a, 0xb8, 0x49, 0x36, - 0x65, 0xb6, 0x85, 0xeb, 0xdd, 0xf0, 0x7b, 0xcd, 0x39, 0xba, 0x0f, 0xad, 0x9d, 0x87, 0x2f, 0x9f, 0xc0, 0x1a, 0x4d, - 0x3a, 0x5d, 0xf8, 0xff, 0xba, 0xc7, 0x6f, 0x91, 0xf0, 0x72, 0xe0, 0x88, 0x61, 0x7a, 0xb1, 0x22, 0x1c, 0x7d, 0xd0, - 0xed, 0x81, 0xf7, 0xa7, 0x75, 0x01, 0x10, 0xc6, 0x6f, 0x0d, 0x80, 0x70, 0x7e, 0xb7, 0x08, 0x08, 0xfd, 0xda, 0xc0, - 0xef, 0x18, 0x01, 0xf9, 0x53, 0x33, 0xc8, 0x7d, 0xc9, 0x96, 0x02, 0x1d, 0x4d, 0x67, 0xed, 0x2d, 0x73, 0x0e, 0xbf, - 0x64, 0x47, 0x98, 0x4a, 0x0f, 0xad, 0x39, 0x37, 0xe3, 0x41, 0x19, 0x6e, 0xe4, 0x4b, 0x26, 0x76, 0x72, 0xc1, 0xd7, - 0x10, 0x24, 0xbe, 0x9d, 0x20, 0xdf, 0xde, 0x8d, 0x1e, 0xf1, 0xef, 0x4c, 0x8f, 0x82, 0x1b, 0xf4, 0xa8, 0x45, 0xdc, - 0x29, 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0x9c, 0xbe, 0xc5, 0xb6, 0xc5, 0xb0, 0xa8, 0xb0, 0x45, 0xce, 0xe6, - 0xd3, 0x5f, 0x73, 0x44, 0x20, 0xd2, 0xcd, 0x43, 0x5b, 0x46, 0x61, 0x66, 0xf8, 0xd1, 0x62, 0xf5, 0x72, 0x2e, 0xae, - 0x34, 0x85, 0x74, 0x1f, 0x71, 0x47, 0x47, 0x70, 0xf0, 0x06, 0x40, 0xb8, 0xc8, 0x78, 0x84, 0xbf, 0x8a, 0x05, 0xe4, - 0xa6, 0xdf, 0xcf, 0x8a, 0x79, 0x82, 0x97, 0xa6, 0xbd, 0xa1, 0xf8, 0x80, 0x2c, 0x3c, 0xca, 0xbb, 0x86, 0x98, 0xc2, - 0xfe, 0x0d, 0xa6, 0xdf, 0xab, 0xb3, 0x83, 0x29, 0xc6, 0x11, 0xde, 0xb0, 0x51, 0x1c, 0x39, 0xb6, 0x33, 0x83, 0x8d, - 0x0c, 0xb3, 0xb4, 0x6a, 0xb9, 0xef, 0x94, 0xf6, 0xee, 0xda, 0xea, 0xa7, 0x99, 0x72, 0xfc, 0xd4, 0x5d, 0x78, 0x28, - 0xe3, 0x8e, 0xb6, 0x74, 0x0c, 0x60, 0x7c, 0x55, 0x92, 0xa3, 0x0e, 0xa8, 0x8c, 0x09, 0x5b, 0x58, 0x13, 0x1d, 0xbf, - 0x0b, 0xde, 0x05, 0x15, 0xe3, 0xa7, 0xc3, 0xbe, 0x77, 0x5a, 0xdb, 0x60, 0xed, 0x18, 0xdd, 0xf4, 0x40, 0x47, 0xfa, - 0x97, 0x7e, 0xf4, 0xaf, 0xd1, 0xd5, 0x2f, 0x0c, 0xd8, 0x82, 0x23, 0x3e, 0x13, 0xb8, 0xdb, 0xf4, 0x89, 0x06, 0x99, - 0x50, 0x82, 0x17, 0xe6, 0xa0, 0xcc, 0x31, 0x7f, 0x95, 0x4c, 0x7c, 0x9a, 0x4c, 0xfc, 0x00, 0x61, 0x59, 0x35, 0x61, - 0xd5, 0xcf, 0x7f, 0x20, 0x05, 0x99, 0xa7, 0x67, 0x23, 0xea, 0x61, 0x86, 0x07, 0xfe, 0xad, 0x8a, 0xd5, 0x83, 0x8c, - 0x58, 0x81, 0x17, 0x8f, 0xbf, 0xe9, 0x42, 0x7f, 0x96, 0xe2, 0x61, 0x22, 0xca, 0xd1, 0x28, 0xad, 0x86, 0xaa, 0xe2, - 0x5e, 0xc5, 0xd3, 0xab, 0x03, 0xf9, 0x41, 0x03, 0x1b, 0x43, 0xd0, 0x74, 0xf4, 0x50, 0x7d, 0x4c, 0x6d, 0x13, 0xf4, - 0x1e, 0xfd, 0xc4, 0x29, 0x65, 0x0f, 0xa0, 0x6a, 0xc3, 0xfb, 0x04, 0x96, 0x74, 0x81, 0x42, 0x5b, 0x28, 0xb6, 0x11, - 0x3b, 0x8f, 0x87, 0x52, 0x3f, 0x79, 0x96, 0xbc, 0x07, 0xd5, 0x22, 0xba, 0x87, 0x1d, 0x4f, 0x04, 0x01, 0xe0, 0x05, - 0xd5, 0x73, 0x98, 0x66, 0x76, 0xff, 0x41, 0x6f, 0x1d, 0x65, 0xf1, 0xf7, 0x56, 0x0f, 0xc1, 0xe9, 0xfc, 0xdb, 0xf0, - 0x01, 0xfe, 0xe2, 0xea, 0x83, 0x23, 0xdb, 0xf5, 0x49, 0xba, 0x3f, 0xa8, 0x7e, 0x76, 0x15, 0x45, 0xdb, 0x26, 0x28, - 0x62, 0xef, 0xae, 0x1a, 0x59, 0x6a, 0xdf, 0xee, 0x4e, 0xa5, 0x7d, 0xe1, 0xd9, 0x10, 0xb7, 0xa0, 0x09, 0xba, 0xfe, - 0x8e, 0x21, 0xd3, 0xcf, 0x5b, 0xf8, 0xb7, 0x26, 0xd5, 0x1f, 0x42, 0x03, 0x25, 0xd6, 0x5f, 0x43, 0xf3, 0x6d, 0xa1, - 0x41, 0xa0, 0xdf, 0x0f, 0x24, 0x73, 0x85, 0xbc, 0xad, 0xf3, 0xf8, 0x8a, 0xd3, 0x30, 0x91, 0x69, 0x61, 0x7b, 0x46, - 0xe0, 0x4c, 0x6c, 0x39, 0x19, 0x16, 0x7a, 0x0e, 0x7d, 0x1d, 0xfd, 0x8d, 0xf2, 0x55, 0x75, 0x5e, 0x4d, 0x04, 0xac, - 0x98, 0x02, 0x37, 0x6d, 0xe3, 0xc4, 0xad, 0x27, 0x92, 0xb8, 0xf5, 0x47, 0x4e, 0xd6, 0x73, 0xab, 0xcc, 0xf6, 0x76, - 0x8d, 0xfd, 0xcf, 0xe9, 0x3b, 0xaa, 0x34, 0xc9, 0xab, 0x51, 0xd9, 0x9c, 0x1f, 0x6c, 0x16, 0xfc, 0xd1, 0xc9, 0xea, - 0x0a, 0x8f, 0xbc, 0x9b, 0x8b, 0xf9, 0x14, 0xa3, 0x38, 0xa7, 0x2b, 0xdf, 0x0a, 0xf4, 0x5a, 0x54, 0xb5, 0xa2, 0x12, - 0x89, 0x00, 0x56, 0x0c, 0x6c, 0x2c, 0xb2, 0x03, 0x99, 0xf5, 0x67, 0x7e, 0x48, 0xdc, 0xbc, 0x93, 0x3b, 0x12, 0x09, - 0x7f, 0xf8, 0x43, 0x0b, 0xb6, 0xa0, 0x8f, 0x0d, 0xa2, 0x74, 0xed, 0x2e, 0x21, 0x03, 0x0b, 0x71, 0xad, 0x7e, 0x39, - 0xcb, 0x94, 0x2e, 0xb6, 0x49, 0x68, 0x3d, 0x2e, 0x91, 0xd0, 0x95, 0x74, 0x3a, 0x65, 0x11, 0xf7, 0xa3, 0x94, 0x92, - 0xb3, 0x1c, 0x43, 0x06, 0x79, 0x1d, 0xb6, 0xed, 0x96, 0x20, 0xf8, 0x8c, 0x9f, 0x16, 0x13, 0x9b, 0xd9, 0x87, 0x42, - 0xfd, 0x59, 0xab, 0x7a, 0xa2, 0xf5, 0xa4, 0xdb, 0x7f, 0x77, 0xb0, 0x67, 0x89, 0x4d, 0xb9, 0xbb, 0x05, 0xaf, 0xbb, - 0xe4, 0x9e, 0x8b, 0x3c, 0x95, 0x50, 0xe4, 0xa9, 0x58, 0x22, 0xbb, 0x4d, 0x24, 0x26, 0x6f, 0x09, 0xb4, 0x6d, 0x8b, - 0xa5, 0x43, 0x11, 0x57, 0x9c, 0x82, 0x0b, 0x13, 0xe3, 0xc7, 0xe7, 0xb6, 0xb0, 0x6b, 0x0b, 0x17, 0xcc, 0x56, 0x29, - 0x3f, 0xca, 0x68, 0xe1, 0xa9, 0x8a, 0x42, 0x82, 0xa9, 0xc1, 0x54, 0xf6, 0x8f, 0x1c, 0x4a, 0x27, 0x1d, 0x2f, 0xb7, - 0x2e, 0xe6, 0xa7, 0x53, 0x10, 0x82, 0x2a, 0x63, 0xe7, 0xa3, 0xec, 0xb0, 0x4b, 0x53, 0xf5, 0x4f, 0x4a, 0x19, 0x26, - 0x55, 0x1f, 0x06, 0x6f, 0xfc, 0x88, 0xaa, 0xc0, 0x5e, 0x0a, 0x7d, 0x4c, 0x38, 0x99, 0x6c, 0x1b, 0x09, 0x27, 0x46, - 0x5d, 0x09, 0xa8, 0x6f, 0xf7, 0x4f, 0x82, 0x99, 0x1c, 0xef, 0x75, 0xb6, 0xf4, 0x83, 0xac, 0xa2, 0x3d, 0x28, 0x94, - 0x01, 0x25, 0x8f, 0x8b, 0x4b, 0x1b, 0x12, 0x60, 0x58, 0x41, 0x80, 0x49, 0xea, 0x77, 0x8b, 0xce, 0xb5, 0xed, 0x9d, - 0xb6, 0xca, 0xc9, 0x85, 0x32, 0xdc, 0x90, 0xa2, 0x8b, 0x31, 0x49, 0x2d, 0xb6, 0x3b, 0xe9, 0xf4, 0x77, 0x23, 0x69, - 0x39, 0xa2, 0xf0, 0x28, 0x40, 0x7a, 0x40, 0x67, 0x34, 0xcf, 0xfc, 0x38, 0xdb, 0xba, 0x60, 0xa7, 0xad, 0x68, 0x16, - 0x57, 0x81, 0x54, 0xb4, 0x23, 0xf4, 0x94, 0x59, 0x35, 0x13, 0x3e, 0x46, 0x0d, 0x24, 0x49, 0x70, 0x97, 0x32, 0x4a, - 0x4b, 0xe6, 0x37, 0xb0, 0x10, 0x50, 0x98, 0xe4, 0xba, 0x8a, 0xe6, 0x4a, 0x75, 0x5a, 0xda, 0xfd, 0xbf, 0xfc, 0xf3, - 0xff, 0x96, 0x01, 0x5a, 0xa0, 0x4a, 0x47, 0x8d, 0xd5, 0x20, 0x74, 0xb9, 0x8b, 0xf9, 0x4d, 0xd5, 0x11, 0x2e, 0xbb, - 0x04, 0x4f, 0x3f, 0x1e, 0xb5, 0x26, 0x51, 0x32, 0x06, 0xc0, 0xd6, 0x12, 0xc8, 0xcc, 0x7e, 0x90, 0x50, 0xd7, 0x8b, - 0x90, 0x05, 0x7f, 0x53, 0x96, 0xb5, 0xca, 0x6e, 0xa7, 0xdd, 0x6a, 0xe4, 0x5c, 0x1b, 0x1b, 0xaa, 0x96, 0x77, 0xad, - 0x7e, 0x95, 0x4c, 0x0a, 0x35, 0x56, 0x4b, 0xba, 0x86, 0x96, 0xfa, 0xa4, 0xe9, 0xdf, 0xff, 0xe5, 0x1f, 0xfe, 0x87, - 0x7a, 0xc5, 0x03, 0xa4, 0xbf, 0xfc, 0xd3, 0xdf, 0x61, 0x7e, 0xb3, 0xa5, 0x0f, 0x99, 0x48, 0x4e, 0x58, 0xd5, 0x09, - 0x93, 0x10, 0x18, 0x56, 0xe5, 0xd1, 0xd5, 0x93, 0xb3, 0xf7, 0x69, 0x42, 0xda, 0x6c, 0x12, 0x3a, 0xda, 0xb4, 0x65, - 0xc5, 0x23, 0x35, 0x92, 0x13, 0x2f, 0x42, 0x25, 0xd2, 0xfb, 0x4e, 0x99, 0x4f, 0xbe, 0x5e, 0x8d, 0x85, 0x0a, 0xff, - 0x61, 0x49, 0x59, 0x95, 0x5b, 0x18, 0x97, 0x5f, 0xe0, 0x6b, 0xd0, 0x35, 0x8a, 0x69, 0xf1, 0x6a, 0x7d, 0x7a, 0x3f, - 0xcd, 0x01, 0xfe, 0x31, 0x52, 0x5c, 0x04, 0x19, 0xe9, 0xcc, 0xb9, 0x85, 0x06, 0x5d, 0x72, 0x55, 0xd2, 0x28, 0xc2, - 0x0b, 0x7c, 0xf8, 0xe4, 0x6f, 0xca, 0x3f, 0x4e, 0xd1, 0x6c, 0xb2, 0x9c, 0x69, 0x74, 0x29, 0x7d, 0xc3, 0x47, 0xed, - 0xf6, 0xec, 0xd2, 0x5d, 0x54, 0x33, 0x78, 0xeb, 0x26, 0xa3, 0xc0, 0xa4, 0x39, 0x20, 0x1d, 0x56, 0xeb, 0x18, 0x28, - 0xb8, 0x43, 0x6d, 0x0c, 0x99, 0x95, 0xe5, 0x1f, 0x16, 0x14, 0x86, 0x8b, 0x7f, 0xc1, 0x43, 0x65, 0x19, 0xb1, 0x84, - 0x12, 0x03, 0x8b, 0x85, 0xd1, 0xab, 0x2b, 0x7a, 0x4d, 0x3a, 0xcb, 0x39, 0x41, 0xe6, 0xa1, 0xb8, 0x79, 0x9c, 0xfd, - 0x10, 0x0f, 0xa8, 0x27, 0x1d, 0x6f, 0xd2, 0x5d, 0xe8, 0xe1, 0x39, 0xcf, 0xa6, 0xe6, 0x29, 0x38, 0x8b, 0xd8, 0x90, - 0x8d, 0x55, 0xa4, 0x57, 0xd6, 0x8b, 0x13, 0xee, 0x72, 0xb2, 0xbd, 0x62, 0x2e, 0x09, 0x12, 0x9d, 0x7e, 0x03, 0x3c, - 0x9f, 0xe1, 0x06, 0x04, 0xfa, 0x67, 0x11, 0x0f, 0x88, 0x5f, 0x7b, 0xe6, 0x59, 0x7a, 0x84, 0x52, 0x26, 0x5b, 0x18, - 0xf0, 0xf4, 0x44, 0x53, 0x8c, 0xb9, 0xd6, 0x73, 0xb2, 0x4a, 0x9f, 0xba, 0x9b, 0x43, 0x89, 0x90, 0xcd, 0xb7, 0xf2, - 0x88, 0xfa, 0x69, 0x2d, 0xd6, 0x21, 0x55, 0x4c, 0xd7, 0xf5, 0x56, 0xd6, 0x0b, 0x4d, 0x2d, 0x6a, 0xbf, 0x05, 0x03, - 0x8c, 0xc0, 0xb4, 0x9b, 0xad, 0xa8, 0x10, 0x5b, 0x3d, 0x0d, 0xbf, 0xd5, 0x7e, 0x4d, 0x34, 0x9b, 0x51, 0x43, 0x17, - 0x98, 0x98, 0xac, 0x51, 0x94, 0x1d, 0x94, 0x7e, 0x21, 0xb2, 0x1d, 0x64, 0x1b, 0xb9, 0x11, 0xc4, 0x93, 0xcc, 0x83, - 0xa0, 0xdf, 0xb7, 0xff, 0x7f, 0x47, 0x48, 0x09, 0x5d, 0xf5, 0x7e, 0x00, 0x00}; + 0xd9, 0xf4, 0x58, 0x0d, 0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x42, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, + 0x1f, 0x99, 0x04, 0x73, 0x15, 0x09, 0xf6, 0xa5, 0x40, 0x60, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x06, 0xcb, 0x73, 0x1a, + 0x0d, 0x3f, 0x69, 0x70, 0x2b, 0xde, 0x6b, 0xb2, 0x81, 0xd3, 0x28, 0x09, 0x0d, 0x71, 0x65, 0xe2, 0xad, 0x24, 0x74, + 0x6d, 0xa3, 0x80, 0x43, 0xb6, 0xc4, 0xf6, 0xcd, 0x85, 0x6e, 0x72, 0xbb, 0x64, 0x0f, 0xe5, 0x3f, 0x55, 0x5c, 0xb2, + 0x9e, 0xe5, 0x98, 0x92, 0x06, 0x4c, 0x31, 0x1e, 0x2c, 0x4d, 0x03, 0x12, 0xe0, 0xbb, 0x72, 0x14, 0x17, 0xeb, 0x49, + 0xf0, 0xbb, 0x82, 0xf9, 0xdc, 0x98, 0xe9, 0x56, 0x48, 0xb5, 0x84, 0x93, 0x66, 0xb0, 0x06, 0x4d, 0x1a, 0x0f, 0x4a, + 0xd4, 0x7c, 0x8d, 0x86, 0x0a, 0x71, 0xfc, 0x99, 0xa8, 0x42, 0x13, 0x0c, 0xc1, 0xc8, 0xbd, 0x42, 0x32, 0x5c, 0xb6, + 0x2c, 0x5a, 0xa4, 0x4c, 0x8d, 0x49, 0xa5, 0x6a, 0x96, 0xcb, 0xc0, 0xc0, 0xa2, 0xdd, 0xea, 0x4b, 0x4b, 0x5c, 0x89, + 0xdc, 0x34, 0xd4, 0xc2, 0xa4, 0x50, 0xde, 0x84, 0x93, 0xa3, 0xdf, 0xa5, 0xac, 0x77, 0x13, 0x9f, 0x5c, 0xe1, 0x93, + 0xfb, 0x86, 0x0f, 0x65, 0xf2, 0x76, 0x31, 0x28, 0x82, 0xaf, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, + 0x74, 0x41, 0xa0, 0x48, 0x36, 0x49, 0x07, 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xe6, 0x8a, 0x0d, 0x52, 0xf3, + 0x4a, 0x33, 0x2f, 0x75, 0x1b, 0xf6, 0x7b, 0x59, 0x4a, 0x3a, 0x31, 0x41, 0x99, 0xd8, 0xbb, 0x89, 0x36, 0x5e, 0x1a, + 0x66, 0xc2, 0xfa, 0x15, 0xc6, 0x4e, 0x8d, 0x42, 0xa9, 0x14, 0x81, 0x38, 0x36, 0xbe, 0x56, 0x96, 0x41, 0xe6, 0xaf, + 0xb0, 0xa7, 0x00, 0x94, 0x04, 0x16, 0x5f, 0x53, 0xc9, 0x8b, 0xc2, 0x3a, 0x1d, 0xef, 0x11, 0x1d, 0x2b, 0x11, 0x5a, + 0x13, 0xf9, 0x5a, 0x9f, 0xc5, 0x7e, 0xcd, 0x25, 0x34, 0x29, 0x99, 0x0f, 0xf2, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, + 0x25, 0x83, 0x84, 0x1c, 0xd2, 0x65, 0xa2, 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x0a, 0x89, 0x96, 0x1e, 0x85, 0x11, 0x8a, + 0x0d, 0xb1, 0x16, 0x4b, 0x84, 0x6c, 0xda, 0x9b, 0xc4, 0x8a, 0xe8, 0x9c, 0xe6, 0x68, 0xc2, 0x99, 0x3a, 0xdd, 0x71, + 0x00, 0x1d, 0x10, 0xfb, 0x4b, 0xac, 0xb7, 0xd2, 0xec, 0x74, 0xfd, 0xca, 0xe1, 0xbb, 0xbe, 0x9e, 0x00, 0x3f, 0x48, + 0x83, 0x17, 0xd6, 0x6c, 0xa0, 0x64, 0xef, 0xde, 0x6b, 0x6c, 0x45, 0xf6, 0x67, 0x55, 0x52, 0x79, 0x0a, 0x35, 0xce, + 0xad, 0xaf, 0x53, 0x2d, 0xb4, 0xa8, 0x2a, 0xf6, 0x0d, 0xa9, 0xbe, 0xaf, 0x14, 0x76, 0x85, 0xf2, 0xbe, 0x1c, 0x3a, + 0x76, 0x5d, 0x37, 0xc8, 0xc9, 0x79, 0xb9, 0xb7, 0xca, 0x85, 0xbc, 0x7f, 0xdf, 0xf4, 0x99, 0xce, 0xf5, 0xf0, 0xcf, + 0x1c, 0x54, 0xce, 0xc5, 0x55, 0x4a, 0x16, 0xcc, 0x33, 0xa5, 0x8e, 0x96, 0x1c, 0xd0, 0x76, 0x0f, 0x3d, 0xed, 0xe8, + 0x22, 0x8a, 0xb9, 0xa5, 0x47, 0x11, 0x9e, 0x36, 0xca, 0x27, 0x69, 0x74, 0x00, 0x5e, 0x68, 0x42, 0x92, 0x13, 0x6e, + 0xda, 0xa2, 0xc5, 0x70, 0xc2, 0x30, 0x04, 0xae, 0xec, 0x09, 0x53, 0xf6, 0xdc, 0x43, 0xbc, 0xe5, 0xc0, 0xab, 0x61, + 0x2f, 0x9b, 0xdd, 0x6b, 0xe6, 0x3f, 0xac, 0x11, 0xc8, 0xb6, 0xa9, 0xaa, 0x2b, 0x1b, 0xef, 0x52, 0x44, 0x62, 0x84, + 0x6d, 0xd5, 0xd8, 0xd2, 0xd6, 0xef, 0x35, 0xdc, 0xeb, 0xca, 0x31, 0xaf, 0x29, 0xd5, 0x86, 0x1e, 0x56, 0x6e, 0x0e, + 0x37, 0x1d, 0x79, 0xb1, 0x82, 0x6e, 0x4f, 0x04, 0x85, 0xc0, 0x89, 0x50, 0xf6, 0xa0, 0xe6, 0x06, 0x22, 0x25, 0x53, + 0x5a, 0x35, 0x9b, 0x27, 0x23, 0x09, 0x2c, 0xb8, 0xb0, 0x4c, 0xf2, 0xd1, 0x45, 0x9c, 0x24, 0x55, 0xe9, 0xef, 0x2a, + 0xe0, 0xc5, 0xb0, 0xb7, 0x89, 0x76, 0x81, 0xd1, 0x5c, 0x81, 0xe0, 0x6a, 0x23, 0xec, 0xa3, 0xe3, 0x56, 0xeb, 0x2e, + 0x22, 0x8e, 0xcc, 0x8c, 0x46, 0x7c, 0x44, 0x1b, 0xb2, 0x64, 0x9a, 0xb5, 0xf7, 0x5e, 0x60, 0x48, 0xcd, 0xc0, 0x07, + 0xd5, 0x19, 0x15, 0xff, 0x2a, 0x7b, 0xea, 0x57, 0xa2, 0x77, 0xab, 0xea, 0x6a, 0x06, 0x54, 0x54, 0xe0, 0xc3, 0x0c, + 0xb1, 0xb4, 0x55, 0x20, 0x20, 0xd7, 0xc3, 0x3a, 0xdc, 0xad, 0x91, 0x06, 0x0b, 0x4a, 0x81, 0xb5, 0x56, 0x76, 0xaf, + 0x6f, 0x0b, 0xe6, 0x50, 0x28, 0x5c, 0xf4, 0x7f, 0x96, 0x4d, 0x67, 0x68, 0x99, 0x35, 0x98, 0x1a, 0x1a, 0x7c, 0x6c, + 0xd4, 0x97, 0x2b, 0xca, 0x6a, 0x7d, 0x68, 0x47, 0xd6, 0xf8, 0x49, 0x3b, 0xca, 0xe0, 0x50, 0xcd, 0x75, 0x51, 0xdd, + 0x6e, 0x6e, 0x8a, 0x98, 0x55, 0x3c, 0xee, 0x93, 0xde, 0xd6, 0xd6, 0xa4, 0xa7, 0x69, 0x40, 0x32, 0x49, 0x32, 0xbc, + 0xc9, 0x00, 0x65, 0x45, 0x9c, 0x45, 0xd9, 0x20, 0xdf, 0xa2, 0x2c, 0x71, 0xfd, 0x7e, 0xe8, 0xed, 0xd5, 0x3c, 0x6b, + 0x6f, 0x6f, 0xbd, 0x8b, 0x5c, 0xd5, 0x49, 0x0f, 0xf2, 0xf0, 0x08, 0x8a, 0x96, 0x6c, 0xca, 0x70, 0x31, 0xcd, 0x46, + 0x2c, 0xb0, 0xa1, 0x7b, 0x6a, 0x97, 0x72, 0xd3, 0x44, 0xc0, 0x3d, 0x11, 0x73, 0x16, 0x1f, 0xea, 0x91, 0xd4, 0x60, + 0x0f, 0x58, 0x40, 0x9b, 0x0b, 0x5f, 0x85, 0x67, 0x49, 0x76, 0x1a, 0x25, 0x07, 0x42, 0x81, 0xd7, 0x5a, 0x7e, 0x0b, + 0x2e, 0x23, 0x59, 0xac, 0x86, 0x92, 0xfa, 0x6a, 0xf0, 0x55, 0x70, 0x7b, 0x8f, 0xca, 0x5b, 0xb1, 0x3b, 0x7e, 0xdb, + 0xef, 0xd8, 0x2a, 0x22, 0xf6, 0x93, 0x39, 0x1d, 0x68, 0x9c, 0x02, 0x28, 0x73, 0x00, 0x9a, 0xac, 0xf0, 0x86, 0x2c, + 0xfc, 0x69, 0xf0, 0x93, 0x72, 0xa9, 0x33, 0x70, 0x21, 0xc0, 0xc9, 0x4f, 0x62, 0xde, 0xc2, 0xf3, 0x48, 0xdb, 0x5b, + 0x88, 0x0a, 0x8c, 0x2b, 0x52, 0x5c, 0xba, 0x54, 0xde, 0xa0, 0x77, 0x1c, 0x9e, 0x40, 0xb3, 0x8d, 0x8d, 0x85, 0xf3, + 0x26, 0xe2, 0x13, 0x3f, 0x8f, 0xd2, 0x51, 0x36, 0x75, 0xdc, 0x4d, 0xdb, 0x76, 0xfd, 0x82, 0x3c, 0x91, 0xcf, 0xdd, + 0x72, 0xe3, 0x04, 0xfc, 0x80, 0xd0, 0x1e, 0xd8, 0x9b, 0xc7, 0xde, 0x01, 0x0b, 0x4f, 0x76, 0x37, 0x16, 0x23, 0x56, + 0xf6, 0x4f, 0xbc, 0x4b, 0x1d, 0x73, 0xf7, 0xde, 0xa3, 0x94, 0x81, 0x5e, 0x61, 0xff, 0x52, 0x82, 0x01, 0xec, 0x46, + 0xf1, 0x77, 0x90, 0x72, 0x1f, 0xe9, 0x40, 0x44, 0xc6, 0x69, 0xaf, 0xaf, 0xed, 0x8c, 0x22, 0x06, 0xf6, 0x3d, 0xed, + 0xac, 0xde, 0xbf, 0x5f, 0xa9, 0xf9, 0xaa, 0xd4, 0x9b, 0xb3, 0xb0, 0xe6, 0xa9, 0x7b, 0x2f, 0xe9, 0x68, 0xa5, 0xbe, + 0x91, 0xe7, 0x8c, 0x94, 0xe6, 0xb2, 0x9d, 0xe0, 0x18, 0x5b, 0x7c, 0xf5, 0xb6, 0x3e, 0x14, 0x51, 0x0a, 0x3f, 0x06, + 0xeb, 0x25, 0x02, 0xf5, 0x0d, 0x0e, 0x8e, 0x77, 0x10, 0x6e, 0xed, 0x3a, 0x83, 0xc0, 0xb9, 0xd7, 0x6a, 0x5d, 0xff, + 0xb8, 0x75, 0xf8, 0xe7, 0xa8, 0xf5, 0xcb, 0x5e, 0xeb, 0x87, 0x23, 0xf7, 0xda, 0xf9, 0x71, 0x6b, 0x70, 0x28, 0xdf, + 0x0e, 0xff, 0xdc, 0xff, 0xb1, 0x38, 0xfa, 0x83, 0x28, 0xdc, 0x70, 0xdd, 0xad, 0x33, 0x6f, 0xc6, 0xc2, 0xad, 0x56, + 0xab, 0x0f, 0x4f, 0x67, 0xf0, 0x84, 0x7f, 0x2f, 0xe0, 0xcf, 0xf5, 0xa1, 0xf5, 0x9f, 0x7e, 0x4c, 0xff, 0xf3, 0x8f, + 0xf9, 0x11, 0x8e, 0x79, 0xf8, 0xe7, 0x1f, 0x0b, 0xfb, 0x41, 0x3f, 0xdc, 0x3a, 0xda, 0x74, 0x1d, 0x5d, 0xf3, 0x87, + 0xb0, 0x7a, 0x84, 0x56, 0x87, 0x7f, 0x96, 0x6f, 0xf6, 0x83, 0x93, 0xdd, 0x7e, 0x78, 0x74, 0xed, 0xd8, 0xd7, 0x0f, + 0xdc, 0x6b, 0xd7, 0xbd, 0xde, 0xc0, 0x79, 0xce, 0x61, 0xf4, 0x07, 0xf0, 0x77, 0x0c, 0x7f, 0x6d, 0xf8, 0x3b, 0x85, + 0xbf, 0x7f, 0x86, 0x6e, 0x22, 0xfe, 0x76, 0x4d, 0xb1, 0x90, 0x6b, 0x3c, 0xb0, 0x88, 0x60, 0x15, 0xdc, 0x8d, 0xad, + 0xd8, 0xdb, 0x20, 0xa2, 0xc1, 0x3e, 0xf4, 0x7d, 0x1f, 0xc3, 0xa4, 0xce, 0xe2, 0x78, 0x03, 0x16, 0x1d, 0x39, 0x67, + 0x23, 0xe0, 0x9e, 0x88, 0x1c, 0x14, 0x01, 0x13, 0x67, 0xab, 0x05, 0x1e, 0xae, 0x7a, 0xc3, 0x70, 0x83, 0x39, 0x60, + 0x14, 0xbc, 0x65, 0xf8, 0xd0, 0x75, 0xbd, 0x17, 0xf2, 0xcc, 0x10, 0xf7, 0xb9, 0x60, 0xad, 0x34, 0x13, 0x26, 0x8d, + 0xed, 0x7a, 0xb3, 0x15, 0x95, 0xb0, 0xad, 0xd3, 0x33, 0xa8, 0x3b, 0x15, 0x27, 0x8c, 0xdf, 0xb1, 0xe8, 0x13, 0x6e, + 0xc9, 0x37, 0xc6, 0x21, 0xf0, 0x92, 0x25, 0xdf, 0x34, 0x1a, 0x0d, 0x1b, 0x51, 0xb8, 0x63, 0x4f, 0x19, 0xcc, 0xb0, + 0x64, 0x22, 0x32, 0x52, 0x9a, 0xc2, 0xb2, 0x85, 0xc9, 0xdf, 0x47, 0x39, 0xdf, 0xa8, 0x0c, 0xdb, 0xb0, 0x66, 0xc9, + 0x36, 0x2d, 0xfd, 0x3b, 0x4c, 0x81, 0xa6, 0x25, 0x9d, 0x7f, 0x98, 0xe3, 0x87, 0x29, 0xa1, 0xf5, 0xd6, 0x61, 0xe0, + 0xa1, 0x17, 0x20, 0x77, 0x44, 0x3f, 0xe7, 0x3d, 0xaa, 0x31, 0xf8, 0x9f, 0x0c, 0x33, 0x78, 0x62, 0x3e, 0x0c, 0xd1, + 0x2c, 0x4a, 0x1d, 0xdc, 0x4a, 0x51, 0xdc, 0xbf, 0xc2, 0x9d, 0x91, 0x96, 0xde, 0x7e, 0xa8, 0x76, 0xcc, 0x41, 0xce, + 0xd8, 0x77, 0x51, 0xf2, 0x89, 0xe5, 0xce, 0xa5, 0xd7, 0xe9, 0x7e, 0x4e, 0x9d, 0x3d, 0xb4, 0xcd, 0x3e, 0x54, 0xc7, + 0x68, 0xda, 0x2c, 0x90, 0x47, 0x84, 0xad, 0x8e, 0x97, 0x63, 0x54, 0x0b, 0x49, 0x50, 0x78, 0x59, 0xd8, 0x25, 0x0e, + 0xb7, 0x77, 0x8b, 0xf3, 0xb3, 0xbe, 0x1d, 0xd8, 0x36, 0x58, 0xfc, 0x07, 0x14, 0xb6, 0x12, 0x86, 0x45, 0xbb, 0xc7, + 0x76, 0xe3, 0x1e, 0xdb, 0xdc, 0xac, 0x02, 0x4e, 0x78, 0x90, 0x4e, 0xdd, 0x13, 0x2f, 0xf2, 0x26, 0x21, 0x0c, 0x38, + 0x84, 0x66, 0xd8, 0xa5, 0x37, 0xdc, 0x8d, 0xe5, 0x34, 0x18, 0x0b, 0xf1, 0x93, 0xa8, 0xe0, 0xaf, 0x30, 0x1e, 0x11, + 0x0e, 0xd1, 0xd8, 0xf7, 0xd9, 0x25, 0x1b, 0x2a, 0x3b, 0x03, 0x08, 0x15, 0xb9, 0x3d, 0x77, 0x18, 0x1a, 0xcd, 0x60, + 0xee, 0x30, 0x3c, 0x18, 0xd8, 0xb0, 0x97, 0x60, 0x57, 0x86, 0xd1, 0x61, 0xe7, 0x68, 0x90, 0x86, 0x33, 0x16, 0x68, + 0xda, 0xca, 0xa2, 0xb3, 0x5a, 0x51, 0xf7, 0x68, 0xe0, 0x4c, 0x99, 0xcf, 0xc1, 0x16, 0x77, 0xf0, 0x0d, 0x23, 0x14, + 0x45, 0xf8, 0x81, 0x9d, 0xbd, 0xb8, 0x9c, 0x39, 0xf6, 0xee, 0x96, 0xbd, 0x89, 0xa5, 0x9e, 0x0d, 0xec, 0x05, 0x73, + 0x87, 0x17, 0xae, 0xd9, 0x79, 0xfb, 0x08, 0x41, 0xc5, 0x42, 0x9c, 0xfc, 0x62, 0x60, 0xf7, 0xc5, 0xd4, 0x6d, 0x18, + 0x34, 0x95, 0xcb, 0x8f, 0x2b, 0x7a, 0x40, 0xa8, 0xaa, 0xae, 0x0a, 0x3a, 0x28, 0xeb, 0x06, 0xce, 0xc4, 0x44, 0xa2, + 0x85, 0x93, 0x49, 0x2a, 0x80, 0xc3, 0x83, 0xcd, 0x60, 0x52, 0xa3, 0xdb, 0xf6, 0xd1, 0xe0, 0x22, 0x78, 0x60, 0x3f, + 0x50, 0x2f, 0x63, 0x40, 0x86, 0x89, 0xe9, 0xc7, 0xa0, 0x45, 0xf0, 0xef, 0x39, 0x03, 0x24, 0x2f, 0xa8, 0x68, 0x26, + 0x8b, 0xce, 0xb0, 0xe8, 0x20, 0x40, 0x50, 0xbd, 0x42, 0x5b, 0x7f, 0x62, 0x4d, 0x46, 0x21, 0xc1, 0x0e, 0xb6, 0xd0, + 0x21, 0xdb, 0xec, 0x1c, 0xe1, 0x79, 0x43, 0xce, 0x8b, 0xef, 0x62, 0x0e, 0x2a, 0x61, 0xab, 0x6f, 0xbb, 0x03, 0xdb, + 0xc2, 0xa5, 0xed, 0x65, 0x9b, 0xa1, 0xa0, 0x70, 0xbc, 0x79, 0xc0, 0x82, 0x49, 0x3f, 0x6c, 0x0f, 0x9c, 0x5c, 0x86, + 0x1b, 0xf1, 0xdc, 0x52, 0x48, 0xf0, 0xb6, 0x37, 0x01, 0x81, 0x8e, 0x9c, 0xbb, 0x61, 0x6f, 0xaa, 0x42, 0x28, 0x3a, + 0xde, 0x1c, 0xb9, 0x41, 0x0c, 0x7f, 0x9c, 0x16, 0x32, 0xcd, 0x44, 0xf7, 0x55, 0x9a, 0x19, 0x90, 0x18, 0x29, 0x8b, + 0x3c, 0x09, 0xb3, 0x4d, 0x07, 0x23, 0xb4, 0x20, 0x69, 0x77, 0x07, 0x00, 0xc3, 0xa6, 0xa3, 0x38, 0x6d, 0x4b, 0xb1, + 0x9a, 0xb2, 0xcf, 0x0f, 0xf5, 0x72, 0x0c, 0xd9, 0x60, 0xc8, 0xfc, 0x4a, 0xfb, 0x00, 0x58, 0x41, 0xe2, 0xe5, 0x47, + 0xea, 0xcc, 0xeb, 0x65, 0xed, 0x7c, 0x6b, 0xa1, 0x44, 0x11, 0xf7, 0x0c, 0x09, 0xc5, 0x4a, 0xed, 0x86, 0x09, 0x73, + 0x7b, 0x86, 0xc4, 0xd0, 0x2c, 0x1f, 0xb6, 0x81, 0xe9, 0x55, 0x80, 0x3d, 0x35, 0xb7, 0x45, 0x12, 0x56, 0xcd, 0xbd, + 0x43, 0x60, 0xed, 0x23, 0xe0, 0x21, 0xda, 0x46, 0x3d, 0x15, 0xcd, 0x67, 0x49, 0xf8, 0xb2, 0x71, 0x5c, 0x1c, 0xe1, + 0x89, 0xd0, 0xbe, 0x3f, 0x9c, 0xe7, 0x20, 0x0f, 0xf8, 0x5b, 0xb0, 0x0c, 0x42, 0xd9, 0x14, 0x1d, 0x3d, 0x3c, 0x02, + 0xf6, 0x08, 0xf1, 0x46, 0xd8, 0xdc, 0xa8, 0x46, 0x8b, 0x92, 0x8c, 0x17, 0x3a, 0x18, 0xee, 0x31, 0xe9, 0xda, 0xa3, + 0x60, 0x90, 0x27, 0xc6, 0x0e, 0x9e, 0xf9, 0xfb, 0x43, 0xac, 0xc6, 0x09, 0x0a, 0xb7, 0xa4, 0xdd, 0x56, 0x89, 0xbf, + 0x7d, 0x3f, 0x05, 0x09, 0x8e, 0x75, 0xe0, 0x67, 0xdd, 0xbf, 0x9f, 0x48, 0xa4, 0x76, 0xd3, 0x1e, 0x9d, 0x44, 0x60, + 0x3c, 0x38, 0xf7, 0x53, 0xa8, 0x46, 0x12, 0x51, 0x51, 0x8e, 0x16, 0xa8, 0x79, 0xaa, 0x56, 0xc1, 0x77, 0x68, 0x46, + 0xe0, 0x19, 0x86, 0xad, 0xc9, 0x4f, 0xd5, 0x8d, 0x45, 0x2c, 0xdf, 0x75, 0xe9, 0x68, 0x0b, 0x0f, 0x20, 0x05, 0xa3, + 0x09, 0x86, 0x71, 0x29, 0x28, 0x59, 0xf1, 0xdf, 0xb1, 0x11, 0x2b, 0x9f, 0x1c, 0x66, 0x9b, 0x9b, 0x47, 0xe2, 0xdc, + 0x82, 0x18, 0x87, 0x19, 0xd1, 0xd5, 0xb8, 0x02, 0xa0, 0x3e, 0x9d, 0x13, 0xd7, 0x03, 0xd3, 0x8a, 0x35, 0x5d, 0x8a, + 0x7d, 0x72, 0x98, 0x01, 0x28, 0xb8, 0xe5, 0x1c, 0xfa, 0x83, 0x3f, 0x1e, 0x81, 0x7b, 0xec, 0xff, 0xc1, 0xdd, 0x52, + 0x82, 0xa6, 0x27, 0xcf, 0x14, 0x17, 0x74, 0xc6, 0xda, 0xf1, 0x28, 0x36, 0x1a, 0x14, 0x5e, 0x0a, 0x18, 0x80, 0x36, + 0x07, 0x99, 0x50, 0x71, 0x10, 0x72, 0x54, 0x60, 0xfb, 0xb8, 0xf9, 0x19, 0xee, 0xec, 0xe7, 0x60, 0xe1, 0x0d, 0xf4, + 0xdb, 0x6b, 0x78, 0xfb, 0xa3, 0x7e, 0xfb, 0x89, 0x05, 0xbf, 0x94, 0x32, 0x74, 0x5f, 0x9b, 0xe2, 0x91, 0x9a, 0xa2, + 0x14, 0x4b, 0x64, 0xd0, 0x90, 0xbb, 0xf9, 0x52, 0xcc, 0x86, 0xb9, 0x25, 0x10, 0x43, 0x89, 0xae, 0xdc, 0xe7, 0xd1, + 0x19, 0x12, 0xd7, 0x35, 0x49, 0x61, 0xe4, 0x12, 0x98, 0x08, 0x57, 0x7c, 0x8b, 0xf4, 0x64, 0xfd, 0x36, 0xd8, 0xe0, + 0xb5, 0xbc, 0x03, 0xb4, 0xef, 0xd8, 0x74, 0xc6, 0xaf, 0xf6, 0x49, 0xd1, 0x07, 0x32, 0x6d, 0x40, 0x9c, 0x9d, 0xb7, + 0x7b, 0xf1, 0x2e, 0xeb, 0xc5, 0x20, 0xd5, 0x73, 0xc5, 0x62, 0xb8, 0x57, 0xbd, 0xf7, 0x18, 0xa5, 0x34, 0x99, 0xc9, + 0xab, 0xa1, 0xd7, 0x95, 0xe8, 0x6d, 0x6e, 0x02, 0x82, 0x3d, 0xa3, 0x2b, 0x13, 0x5d, 0xcb, 0x52, 0xd0, 0x04, 0x20, + 0x7a, 0x52, 0x67, 0x39, 0xe2, 0x38, 0xcc, 0x66, 0x83, 0xe2, 0x11, 0x73, 0x57, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, + 0xb0, 0x03, 0x98, 0x11, 0x97, 0xb7, 0x3a, 0x22, 0x3a, 0x2c, 0xfa, 0xeb, 0xf8, 0xf6, 0xb1, 0xc7, 0x37, 0x3b, 0x2e, + 0x68, 0x90, 0xda, 0x58, 0x8f, 0xab, 0xb1, 0xa0, 0x3e, 0x3c, 0xd6, 0x54, 0x2a, 0x8b, 0xcd, 0xcd, 0xb2, 0x7e, 0x54, + 0xab, 0x76, 0x70, 0xed, 0x34, 0xe5, 0xb2, 0x99, 0x0d, 0xc2, 0x81, 0x88, 0x09, 0x14, 0x68, 0x69, 0x65, 0xc5, 0x00, + 0x43, 0xca, 0x72, 0x94, 0x4f, 0x21, 0xf7, 0xe2, 0xb2, 0xd4, 0xa9, 0x2f, 0xcf, 0x64, 0xd0, 0x11, 0x4f, 0x3d, 0xc9, + 0x58, 0x01, 0x05, 0xeb, 0xa5, 0x5e, 0x42, 0x4b, 0x04, 0x98, 0xbf, 0x50, 0x39, 0x34, 0xc2, 0x02, 0x89, 0x42, 0xc3, + 0x2c, 0x51, 0xc6, 0x67, 0x11, 0xc6, 0xa0, 0xed, 0x9f, 0xd5, 0x62, 0x5f, 0x85, 0x32, 0x3a, 0x8a, 0xc3, 0xfc, 0x28, + 0xa0, 0xfa, 0xb9, 0x94, 0x60, 0x93, 0xf0, 0x23, 0xb0, 0x51, 0xe5, 0x78, 0x92, 0x20, 0x7c, 0x1e, 0xe7, 0x8c, 0x3c, + 0x85, 0x0d, 0x09, 0xb3, 0x34, 0x6d, 0x23, 0xd5, 0x2e, 0x32, 0x83, 0x50, 0x2e, 0xcc, 0x3f, 0x31, 0xce, 0x2e, 0xb2, + 0x70, 0xa9, 0x35, 0x98, 0x1f, 0xef, 0x4c, 0x80, 0xb2, 0xeb, 0xeb, 0x4c, 0xf8, 0xb8, 0x11, 0xd9, 0x1b, 0xba, 0x62, + 0x32, 0x50, 0x48, 0x05, 0x4e, 0x44, 0x16, 0x0f, 0x9d, 0xa1, 0xd0, 0x08, 0x07, 0x74, 0x8a, 0x9c, 0xbb, 0xc6, 0xa6, + 0xcf, 0x07, 0xda, 0x37, 0x4a, 0x43, 0x27, 0x01, 0x21, 0x20, 0x70, 0x37, 0xac, 0xa9, 0x74, 0x90, 0x06, 0x09, 0x95, + 0xa2, 0x9f, 0x03, 0xf8, 0x87, 0x91, 0xa4, 0x00, 0xd8, 0x0f, 0xd5, 0x48, 0x11, 0x65, 0x59, 0xe0, 0x02, 0xd0, 0x5c, + 0xfb, 0xb8, 0x12, 0xbe, 0x30, 0x50, 0x61, 0x7a, 0x9a, 0x95, 0x95, 0x42, 0x89, 0x3c, 0x5d, 0x91, 0xb2, 0x46, 0x32, + 0xf9, 0x1c, 0x1d, 0x3e, 0xe5, 0x5d, 0xbf, 0x95, 0x78, 0xe8, 0x82, 0xe7, 0xb0, 0xac, 0xea, 0xf9, 0x4d, 0xc8, 0xc8, + 0xb9, 0x06, 0x5d, 0x21, 0x85, 0xfe, 0x92, 0x93, 0xbc, 0xf7, 0xc6, 0xaf, 0x6a, 0xa9, 0x31, 0x94, 0x7d, 0x5c, 0xd5, + 0x0c, 0xcb, 0xcb, 0x59, 0x15, 0xa6, 0x20, 0xe0, 0x16, 0x2c, 0x09, 0x16, 0x52, 0x43, 0x80, 0x85, 0xed, 0x91, 0x56, + 0x0a, 0xf2, 0x52, 0x87, 0x77, 0x9e, 0x83, 0x15, 0x60, 0x1c, 0x6a, 0xa9, 0x64, 0x1a, 0x49, 0x7c, 0x99, 0xd4, 0x04, + 0x4c, 0xb9, 0x3f, 0x04, 0x3f, 0xb5, 0x79, 0xd2, 0x75, 0xe9, 0xfa, 0xf1, 0x14, 0x53, 0x7b, 0x08, 0xf4, 0xd8, 0xbb, + 0x07, 0xa6, 0x44, 0x5d, 0x87, 0x15, 0xc4, 0xa1, 0x59, 0x4d, 0xb3, 0x80, 0x19, 0xd3, 0x06, 0x2d, 0xd9, 0x06, 0x5b, + 0x2e, 0x07, 0xfb, 0x48, 0x6c, 0xcf, 0x6a, 0x05, 0x84, 0xae, 0x41, 0x03, 0x43, 0xee, 0x52, 0xa1, 0x85, 0x59, 0xaf, + 0x4b, 0x45, 0xb8, 0x3f, 0x07, 0x4c, 0x5a, 0xc1, 0x99, 0x97, 0xd1, 0xc0, 0xfb, 0xf1, 0x69, 0x82, 0x89, 0x2f, 0x88, + 0x15, 0xd8, 0xc1, 0x41, 0xa7, 0xd9, 0x14, 0x38, 0x15, 0x17, 0x29, 0x83, 0x65, 0x45, 0xa9, 0x0d, 0x7f, 0xa4, 0xc8, + 0xd6, 0x5d, 0x1e, 0xe9, 0x2e, 0xc4, 0x02, 0xd8, 0xe9, 0x17, 0x8c, 0x7c, 0xcb, 0x7a, 0x19, 0x30, 0x38, 0xd7, 0x1a, + 0x07, 0x81, 0xdf, 0xdc, 0x4c, 0x8e, 0xca, 0x94, 0xd8, 0xae, 0xc9, 0xea, 0x02, 0x72, 0x4c, 0x02, 0x6c, 0xe0, 0x0e, + 0xc2, 0x52, 0xd9, 0xe3, 0x45, 0x39, 0xc5, 0xe5, 0x52, 0x16, 0x72, 0x33, 0x1d, 0x8b, 0xe6, 0x73, 0x2b, 0xcd, 0xa6, + 0xe3, 0xad, 0xf8, 0xa2, 0xe0, 0x1f, 0x38, 0xb1, 0xb4, 0xea, 0x29, 0xb5, 0xc2, 0xa3, 0xcc, 0x2d, 0x59, 0xa7, 0xa4, + 0x56, 0xd7, 0x0d, 0x54, 0x23, 0x3c, 0x4d, 0xc3, 0x46, 0x20, 0xc4, 0x04, 0x17, 0xbf, 0x6d, 0x32, 0x31, 0xed, 0x2d, + 0x21, 0x75, 0x84, 0xdd, 0x43, 0x39, 0xc1, 0x5d, 0xcd, 0xb3, 0x2f, 0xc3, 0xd9, 0x7a, 0xe6, 0xde, 0x33, 0x98, 0xfb, + 0x69, 0xc8, 0x0c, 0x46, 0x8f, 0x65, 0xc2, 0x8f, 0x8c, 0x7d, 0xe4, 0xaa, 0xea, 0xd9, 0x59, 0x58, 0x89, 0x2c, 0xf1, + 0x64, 0x1c, 0x75, 0x18, 0xa7, 0xa2, 0x35, 0x41, 0x76, 0x7d, 0x5d, 0x98, 0x7b, 0x81, 0x82, 0xa6, 0x1e, 0xab, 0xc7, + 0x69, 0x2b, 0x76, 0x36, 0x22, 0x91, 0x7b, 0x6f, 0x6a, 0x91, 0xc8, 0x8a, 0xcf, 0x71, 0xa4, 0x35, 0x07, 0xb9, 0xcf, + 0xce, 0x96, 0x37, 0xa9, 0xd0, 0x2d, 0x1a, 0x6d, 0x63, 0x8f, 0xea, 0x03, 0x49, 0x3d, 0xa3, 0x02, 0xab, 0x1a, 0xfb, + 0xfe, 0xfd, 0x8e, 0x48, 0xb7, 0x54, 0x8a, 0x0d, 0x43, 0x5a, 0x21, 0x33, 0x46, 0xc1, 0xa0, 0xa4, 0xc8, 0x40, 0x8d, + 0xf2, 0x35, 0x82, 0x61, 0x8f, 0x1a, 0x80, 0xe2, 0x5c, 0x5d, 0xfd, 0xb4, 0x94, 0x6c, 0x21, 0x20, 0x01, 0xd9, 0x84, + 0x62, 0x8d, 0x98, 0x19, 0xf9, 0xe4, 0x23, 0x70, 0xde, 0x80, 0xa3, 0x63, 0x00, 0x7e, 0x81, 0xd8, 0xf4, 0x60, 0x62, + 0xdb, 0x44, 0x14, 0x7d, 0x36, 0xf0, 0x12, 0x80, 0x9d, 0x55, 0xa1, 0xd1, 0x0f, 0x55, 0x0a, 0x18, 0xb2, 0x81, 0x1b, + 0xf0, 0x2a, 0x2c, 0xb7, 0xf7, 0x12, 0xda, 0xc1, 0xeb, 0x0b, 0xd9, 0x7c, 0x03, 0xf3, 0x04, 0xab, 0xd8, 0x9d, 0x5f, + 0x59, 0xd6, 0xe2, 0xdc, 0xe9, 0xa0, 0x51, 0xaf, 0x28, 0x21, 0x6a, 0xf7, 0xb1, 0xf6, 0x25, 0x23, 0x18, 0xf1, 0xfd, + 0x0d, 0x65, 0x1d, 0xaa, 0x71, 0xcb, 0x3d, 0x8d, 0x16, 0x61, 0xba, 0x4c, 0x1a, 0x83, 0x92, 0x75, 0x3f, 0x19, 0x71, + 0x2f, 0xf7, 0x45, 0x2c, 0xb8, 0xc2, 0xd1, 0x08, 0x9b, 0x37, 0x90, 0xa4, 0xa7, 0x3d, 0x3a, 0x60, 0xdf, 0x68, 0xf6, + 0x02, 0xca, 0x7c, 0xac, 0x48, 0x25, 0x21, 0xa5, 0xd9, 0x0d, 0x91, 0x24, 0xac, 0x15, 0x79, 0xea, 0xbc, 0xef, 0x68, + 0x9f, 0x5b, 0x49, 0x04, 0x23, 0x38, 0x89, 0xd3, 0x95, 0x07, 0x4d, 0x01, 0xae, 0xa2, 0x23, 0xa6, 0x6f, 0x82, 0xf2, + 0x1b, 0xe4, 0xf6, 0x52, 0x72, 0x6d, 0xae, 0x61, 0x78, 0x86, 0x04, 0xab, 0x22, 0x11, 0x78, 0x44, 0x0d, 0x38, 0xe6, + 0xab, 0x3c, 0x0f, 0x30, 0xe1, 0x6b, 0x7b, 0x13, 0x00, 0xca, 0xc9, 0x55, 0x71, 0x96, 0x02, 0xdd, 0x80, 0xe5, 0xea, + 0x38, 0x35, 0x2a, 0x12, 0x17, 0x37, 0xa6, 0xab, 0x5b, 0xfa, 0x53, 0xb4, 0x9c, 0xc9, 0x10, 0xd3, 0x41, 0x10, 0x90, + 0xa9, 0x4f, 0x99, 0x23, 0x64, 0xae, 0xb0, 0x3e, 0x67, 0x4e, 0x6d, 0xea, 0x1e, 0xa7, 0x6e, 0x9e, 0xa4, 0x16, 0xab, + 0xd3, 0xa6, 0x94, 0x88, 0x49, 0x89, 0x79, 0x2a, 0x53, 0xb1, 0x95, 0xb8, 0x73, 0xeb, 0x1b, 0x2d, 0xa4, 0x8d, 0x76, + 0x2a, 0x73, 0xb0, 0xb5, 0xbc, 0x17, 0xa2, 0xfd, 0x25, 0x11, 0x9e, 0x95, 0xc8, 0x58, 0x8b, 0x39, 0x73, 0x4c, 0x04, + 0xab, 0x17, 0x53, 0x91, 0x7f, 0x70, 0x74, 0x9a, 0xbd, 0x41, 0x0f, 0x52, 0x6f, 0x20, 0x31, 0x6b, 0xe2, 0xbb, 0x90, + 0x86, 0x3a, 0x42, 0xa0, 0x32, 0xaa, 0x65, 0x3a, 0x4e, 0x2c, 0x15, 0x97, 0xe4, 0xab, 0xf7, 0xfa, 0x38, 0xdf, 0x78, + 0x6e, 0xac, 0x46, 0x10, 0x83, 0xb7, 0x90, 0x1f, 0x79, 0x52, 0x84, 0x03, 0xe1, 0xf2, 0xcd, 0xcd, 0x5e, 0xbe, 0xcb, + 0xaa, 0x10, 0x49, 0x05, 0x63, 0x8c, 0x19, 0xc5, 0xb8, 0x27, 0x6a, 0x6a, 0x31, 0x07, 0x54, 0x65, 0xeb, 0x30, 0xc7, + 0x03, 0x00, 0x68, 0x69, 0x4a, 0x2f, 0xb3, 0xad, 0x3a, 0xcf, 0x25, 0x7c, 0x8c, 0x3c, 0x14, 0xd9, 0xf8, 0xfd, 0x9a, + 0x0c, 0x14, 0x84, 0xfb, 0x5e, 0xc7, 0xc3, 0xc4, 0x38, 0x58, 0x45, 0x21, 0x0b, 0xf4, 0x06, 0xed, 0x55, 0x89, 0x50, + 0xdc, 0x9c, 0xac, 0xc7, 0x0d, 0x27, 0x15, 0x6c, 0xa1, 0x12, 0x96, 0x4a, 0x0b, 0xfc, 0x6a, 0x23, 0x34, 0x4f, 0x19, + 0xf7, 0xde, 0x54, 0x38, 0x83, 0xfe, 0xe0, 0xde, 0x32, 0xa3, 0xbe, 0x5f, 0x3a, 0x91, 0xa9, 0xc0, 0xc4, 0xcd, 0x2c, + 0xb5, 0xdf, 0x2f, 0xab, 0xb4, 0x9f, 0x57, 0xc8, 0x7d, 0x4e, 0x9a, 0xaf, 0x73, 0x07, 0xcd, 0x27, 0xc3, 0xfd, 0x4a, + 0xf9, 0xa1, 0x85, 0x51, 0x53, 0x7e, 0x79, 0x5d, 0xf9, 0x15, 0x9e, 0x0a, 0x6f, 0xf5, 0xbb, 0x28, 0x74, 0x51, 0x9f, + 0x83, 0x21, 0xa4, 0x1f, 0xc1, 0x35, 0x34, 0x78, 0x50, 0x24, 0x8b, 0xc5, 0xda, 0x05, 0x71, 0x7d, 0xcc, 0xa9, 0x76, + 0x28, 0x63, 0x8c, 0x78, 0x5a, 0x72, 0x90, 0x64, 0x70, 0x30, 0x7e, 0x03, 0x03, 0x62, 0x52, 0x12, 0xd2, 0x21, 0x74, + 0x56, 0x66, 0x22, 0x2a, 0x77, 0xf1, 0x76, 0xe3, 0xb2, 0xa6, 0x50, 0x84, 0x9d, 0x60, 0xa6, 0x52, 0x2a, 0x08, 0xa4, + 0xc9, 0x77, 0xaf, 0x53, 0x0b, 0x86, 0x82, 0x68, 0x30, 0x14, 0x90, 0xd7, 0x76, 0x3d, 0x68, 0xf2, 0x51, 0x1c, 0x3c, + 0xaf, 0x50, 0x23, 0x5e, 0x66, 0xf0, 0x35, 0x6c, 0xfe, 0x9a, 0x28, 0xc9, 0x43, 0x2e, 0x62, 0xaf, 0xe0, 0x13, 0x21, + 0x9b, 0xf2, 0xb0, 0x00, 0xfa, 0xa1, 0x5d, 0xd9, 0x4b, 0x77, 0x8b, 0xca, 0xa5, 0x45, 0x63, 0x2b, 0x51, 0xb3, 0xe6, + 0x87, 0xf1, 0x66, 0x7a, 0x04, 0x53, 0x53, 0x02, 0x01, 0x69, 0x2a, 0x27, 0xa9, 0xe6, 0x3d, 0x4c, 0x8f, 0x00, 0x24, + 0xd8, 0xfd, 0x04, 0x16, 0xfa, 0x4d, 0x89, 0x09, 0x16, 0x55, 0x63, 0xb7, 0x19, 0x68, 0xcd, 0x19, 0x69, 0xbe, 0x19, + 0x42, 0xb8, 0xa9, 0xac, 0x67, 0xcc, 0x0e, 0xb0, 0x6d, 0x77, 0xb3, 0x38, 0x4c, 0x37, 0x3b, 0x47, 0x86, 0xe0, 0xc2, + 0xe3, 0xff, 0xa4, 0xc4, 0x34, 0x90, 0x5c, 0xea, 0xc6, 0x4f, 0xa8, 0xc3, 0x3e, 0x91, 0x3a, 0x11, 0x03, 0x9a, 0xab, + 0xd1, 0x74, 0xee, 0x35, 0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, 0x89, 0x62, 0x9e, 0x13, + 0x00, 0xa3, 0xd8, 0xfc, 0x39, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4c, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0xcf, 0xe8, 0x48, + 0x45, 0x65, 0x73, 0x12, 0xf3, 0x6f, 0x4b, 0x30, 0x8d, 0x89, 0x0f, 0xf5, 0x5c, 0x47, 0xa1, 0x00, 0x5f, 0xd9, 0x50, + 0x6a, 0xb6, 0xd7, 0xbf, 0x75, 0xb6, 0x87, 0x72, 0x36, 0xc1, 0x02, 0x0d, 0xba, 0xac, 0xc1, 0x17, 0xb0, 0x0c, 0xee, + 0x48, 0x3f, 0x05, 0xdf, 0x4f, 0xeb, 0xe0, 0x33, 0xf6, 0xbf, 0x00, 0xb4, 0x2a, 0x30, 0xa0, 0xdc, 0x69, 0x1a, 0x56, + 0x42, 0x5c, 0xa2, 0xc2, 0xac, 0xe2, 0xfc, 0x71, 0x9d, 0xd7, 0x4d, 0xcb, 0x12, 0x83, 0xf2, 0x33, 0xd7, 0x70, 0xe3, + 0x7b, 0x8d, 0xfc, 0xf1, 0xbd, 0x97, 0xa0, 0xdb, 0x89, 0xb4, 0xf7, 0xef, 0xe7, 0xf7, 0xc8, 0x42, 0x03, 0x3f, 0x2c, + 0x9a, 0x41, 0x5b, 0xbc, 0x08, 0x90, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x03, 0x0c, + 0xc1, 0x63, 0xa7, 0x32, 0xf9, 0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, 0xe4, 0xc5, 0x3a, + 0xd9, 0x5e, 0x9c, 0xc3, 0xf3, 0xeb, 0xb8, 0x00, 0xea, 0x20, 0xfa, 0x9a, 0xca, 0x62, 0x03, 0xb9, 0xb8, 0x29, 0x6b, + 0xbd, 0xa2, 0xd1, 0xe8, 0xc6, 0x2e, 0xbc, 0xae, 0xc0, 0x27, 0x51, 0x3a, 0x4a, 0xc4, 0x24, 0x66, 0x52, 0xe5, 0x8a, + 0x5c, 0x1b, 0xdd, 0x4b, 0x5b, 0x34, 0x2f, 0x85, 0x04, 0xaf, 0x08, 0xdc, 0x10, 0xfa, 0x4a, 0x5f, 0xae, 0x36, 0x50, + 0xf0, 0xa8, 0xbd, 0xb9, 0x08, 0x26, 0x26, 0x1e, 0x37, 0xa4, 0xa6, 0x5f, 0x87, 0x53, 0x2b, 0x8b, 0x25, 0x87, 0x5f, + 0xe7, 0x8c, 0x35, 0x14, 0x00, 0xf1, 0xc9, 0xa3, 0xf5, 0x6e, 0xd2, 0x1b, 0xa5, 0x1d, 0x94, 0x46, 0x88, 0xef, 0x2a, + 0x7c, 0xdd, 0x85, 0xe2, 0x2b, 0x57, 0xdd, 0xfb, 0x3a, 0x66, 0xc6, 0x05, 0xa3, 0x97, 0x7c, 0x9a, 0x34, 0xae, 0xdd, + 0xd0, 0x5d, 0x9d, 0xef, 0xbd, 0x2f, 0x65, 0xde, 0xc2, 0x31, 0xb0, 0xc9, 0x31, 0x73, 0x5e, 0x7a, 0x6f, 0x8d, 0x13, + 0xe5, 0x1f, 0xcc, 0x23, 0x5e, 0x39, 0xcc, 0xaa, 0x93, 0xe4, 0x1f, 0x06, 0x3f, 0x04, 0xeb, 0x5b, 0x1a, 0x27, 0xc8, + 0x5d, 0x75, 0x82, 0x4c, 0x94, 0xdb, 0xd0, 0x1b, 0x6e, 0xef, 0xae, 0x02, 0x41, 0x9c, 0x8a, 0xe9, 0xa3, 0x72, 0x5c, + 0x3f, 0x5a, 0xa0, 0x52, 0x11, 0xf1, 0xb9, 0xca, 0x5d, 0x59, 0x9b, 0x1a, 0xea, 0x31, 0x9d, 0xcc, 0x42, 0xd3, 0xac, + 0xc8, 0xa5, 0x6c, 0x7a, 0x8c, 0x5c, 0xb3, 0x53, 0x6d, 0x7e, 0x77, 0xed, 0x21, 0x1d, 0xc7, 0xfb, 0x9e, 0xb5, 0x5a, + 0x70, 0xbf, 0xab, 0x28, 0xbc, 0xeb, 0xc5, 0x46, 0x2a, 0x43, 0xcd, 0x7a, 0x14, 0x7d, 0x1c, 0xb7, 0x99, 0xcb, 0xa3, + 0xec, 0xcf, 0x1a, 0x00, 0xa6, 0x23, 0x2c, 0xba, 0x9b, 0x9e, 0xb1, 0x27, 0xd0, 0xd3, 0x13, 0x19, 0x24, 0x7a, 0xa3, + 0xf3, 0x55, 0xab, 0xc4, 0xd2, 0x15, 0x04, 0x76, 0x6f, 0xc8, 0x58, 0x95, 0xb4, 0x5b, 0xae, 0x5f, 0xce, 0xf3, 0x79, + 0xce, 0x97, 0xf2, 0x7c, 0x6a, 0x16, 0xdd, 0xbd, 0xb6, 0x7b, 0x73, 0x6a, 0xa8, 0x98, 0x6b, 0x75, 0x93, 0xdf, 0x30, + 0x5d, 0x07, 0x43, 0x2d, 0x82, 0xcc, 0x6a, 0x57, 0xbd, 0x28, 0xcb, 0x8d, 0x7a, 0x26, 0xc7, 0x86, 0xf0, 0x4d, 0xa5, + 0x3b, 0x44, 0x37, 0x4c, 0xd5, 0x4c, 0xdf, 0x37, 0xb6, 0x85, 0x6c, 0xf3, 0xf2, 0x6a, 0x94, 0x03, 0xa5, 0xe5, 0xfe, + 0x32, 0x61, 0xf8, 0xfe, 0xfa, 0xfa, 0x7b, 0x21, 0xa7, 0xaa, 0x8e, 0xde, 0xe2, 0xb5, 0xee, 0x19, 0x6c, 0x94, 0xca, + 0x89, 0xb8, 0x60, 0xab, 0x07, 0x6f, 0xee, 0x5e, 0x01, 0xcb, 0x05, 0xec, 0xda, 0x0b, 0xe6, 0x34, 0x86, 0xaa, 0x36, + 0xf0, 0x97, 0xab, 0x07, 0x5b, 0xb5, 0x87, 0xbf, 0x1c, 0x7c, 0x19, 0xdc, 0xd8, 0xd8, 0xd8, 0xc6, 0xdb, 0xb5, 0x44, + 0x90, 0x37, 0x78, 0xa0, 0x8f, 0x57, 0x1f, 0x05, 0x2d, 0x57, 0x88, 0x6d, 0x36, 0x70, 0x28, 0x6c, 0x0d, 0xf2, 0x4d, + 0xca, 0xa4, 0xe1, 0xbc, 0xe0, 0xd9, 0x54, 0xce, 0x50, 0xc8, 0x6b, 0x3e, 0x0e, 0xda, 0x8e, 0xf0, 0xbf, 0xc0, 0xa9, + 0x1d, 0x2f, 0x2f, 0x3e, 0x41, 0x1f, 0xf0, 0x74, 0xa5, 0x34, 0xa5, 0x38, 0xa5, 0x0a, 0xea, 0x2c, 0xd7, 0x79, 0x30, + 0x52, 0x5c, 0x4c, 0x60, 0x71, 0xc1, 0x65, 0xb9, 0x71, 0x36, 0x72, 0xfa, 0x4b, 0xbc, 0xba, 0x48, 0x97, 0x8f, 0x44, + 0xb6, 0x6a, 0xe9, 0xfd, 0xac, 0x4f, 0xb7, 0xed, 0x29, 0xe3, 0x93, 0x6c, 0x44, 0x07, 0x33, 0x3e, 0x4e, 0x84, 0xd7, + 0x27, 0x46, 0xfa, 0x6e, 0x11, 0x98, 0x6e, 0x8e, 0x4d, 0x7e, 0x38, 0x5e, 0x6f, 0x36, 0x6b, 0xdc, 0xc1, 0x3b, 0xe7, + 0x93, 0xb3, 0x28, 0x31, 0xa2, 0xb2, 0xd0, 0xf0, 0x80, 0x56, 0x88, 0x9b, 0xf7, 0x4c, 0x60, 0x5c, 0x76, 0x45, 0x52, + 0xdb, 0x0d, 0x04, 0x2e, 0xf6, 0x38, 0x66, 0xc9, 0xc8, 0xf6, 0xa0, 0x3c, 0xd0, 0x17, 0xa3, 0xe9, 0x16, 0x30, 0x2d, + 0xaf, 0x9d, 0x5d, 0xa4, 0xb6, 0x57, 0x4d, 0x15, 0xc0, 0x2c, 0x59, 0x1e, 0x9f, 0x21, 0xeb, 0x7e, 0x0d, 0x5d, 0xc4, + 0x80, 0xb1, 0x71, 0x65, 0xce, 0x5d, 0xac, 0x5a, 0x11, 0xdf, 0x68, 0x22, 0x4d, 0xea, 0x43, 0xea, 0x7b, 0x14, 0xd6, + 0xea, 0x2a, 0x07, 0x09, 0xdc, 0x23, 0xef, 0x8e, 0xb8, 0xf4, 0xf4, 0x99, 0xc5, 0xb8, 0x4a, 0xdf, 0x52, 0xd7, 0xe2, + 0x9a, 0x61, 0xaf, 0x78, 0x00, 0xf6, 0x07, 0xc6, 0x2d, 0x62, 0x11, 0x6f, 0x67, 0xb5, 0x14, 0xd6, 0xc6, 0x1c, 0x68, + 0x6e, 0xb8, 0xc1, 0xcf, 0xac, 0x5a, 0x33, 0x30, 0xc3, 0x8c, 0x33, 0x92, 0x0f, 0xc6, 0xbd, 0xaa, 0xb1, 0x23, 0x57, + 0x01, 0x44, 0xdf, 0x82, 0x2e, 0xc9, 0xe1, 0x95, 0x2c, 0x57, 0x9d, 0x21, 0xbf, 0x82, 0x75, 0xd6, 0x8b, 0x13, 0x70, + 0x93, 0xa6, 0xac, 0xc4, 0xc4, 0x14, 0x71, 0xb9, 0x59, 0xc6, 0x3c, 0x4d, 0x9f, 0x45, 0x3b, 0x38, 0xb9, 0x91, 0xc0, + 0x11, 0xfb, 0xc6, 0x32, 0x34, 0x13, 0x36, 0x62, 0x22, 0x8d, 0x4a, 0x29, 0x61, 0x03, 0xb9, 0xd4, 0x92, 0xbf, 0xcc, + 0xe5, 0xd5, 0x97, 0xdb, 0x04, 0x07, 0xe4, 0x35, 0xb0, 0x1c, 0x1a, 0xc7, 0x2d, 0x03, 0x89, 0x58, 0x0c, 0x88, 0x51, + 0xab, 0x72, 0x39, 0x19, 0xd5, 0xc9, 0x7c, 0x85, 0x5c, 0xa8, 0xc8, 0x83, 0x5b, 0x02, 0x2f, 0x54, 0xe4, 0x98, 0x3a, + 0x98, 0x95, 0xda, 0x4d, 0x8b, 0x4d, 0x92, 0xf7, 0xcc, 0x80, 0xe4, 0xea, 0x6b, 0x78, 0x68, 0xfc, 0x32, 0xbc, 0xa1, + 0xe8, 0xe9, 0x18, 0x21, 0xa7, 0xa5, 0x31, 0x97, 0xfe, 0x5b, 0x79, 0x9f, 0x56, 0x02, 0xf6, 0x0a, 0xc4, 0x94, 0x81, + 0x4b, 0x6c, 0x5c, 0x90, 0x94, 0xd7, 0xf2, 0x94, 0xdd, 0xd7, 0x50, 0xbe, 0x4b, 0x26, 0x5d, 0xa5, 0xb2, 0xd6, 0x58, + 0x75, 0x3f, 0xcf, 0x59, 0x7e, 0xb5, 0xcf, 0x30, 0x37, 0x19, 0x0d, 0xb2, 0x25, 0x33, 0x9b, 0xf2, 0xab, 0xbd, 0x1b, + 0xbf, 0xf2, 0x50, 0xd2, 0xa1, 0x5a, 0xa5, 0x9b, 0x97, 0x6e, 0x38, 0xc6, 0x8d, 0x1b, 0x8e, 0x00, 0x36, 0x86, 0x9d, + 0x2a, 0x52, 0xeb, 0xfc, 0xf7, 0xa5, 0xf0, 0x93, 0xd8, 0x6b, 0x47, 0x7a, 0xd7, 0x1d, 0xad, 0x4c, 0x4f, 0xbf, 0x01, + 0x55, 0x23, 0x4b, 0xe8, 0x26, 0x54, 0x31, 0x19, 0x89, 0x12, 0xd3, 0x55, 0xca, 0xa3, 0xbe, 0x46, 0x9c, 0x83, 0xb8, + 0xa1, 0xfc, 0xc5, 0x3f, 0x85, 0x57, 0x27, 0x01, 0x1a, 0x51, 0x8b, 0x71, 0x96, 0xf2, 0xd6, 0x38, 0x9a, 0xc6, 0xc9, + 0x55, 0x30, 0x8f, 0x5b, 0xd3, 0x2c, 0xcd, 0x8a, 0x19, 0x70, 0xa5, 0x57, 0x5c, 0x81, 0x0d, 0x3f, 0x6d, 0xcd, 0x63, + 0xef, 0x25, 0x4b, 0xce, 0x19, 0x8f, 0x87, 0x91, 0x67, 0xef, 0xe5, 0x20, 0x1e, 0xac, 0xb7, 0x51, 0x9e, 0x67, 0x17, + 0xb6, 0xf7, 0x21, 0x3b, 0x05, 0xa6, 0xf5, 0xde, 0x5d, 0x5e, 0x9d, 0xb1, 0xd4, 0xfb, 0x78, 0x3a, 0x4f, 0xf9, 0xdc, + 0x2b, 0xa2, 0xb4, 0x68, 0x15, 0x2c, 0x8f, 0xc7, 0xa0, 0x26, 0x92, 0x2c, 0x6f, 0x61, 0xfe, 0xf3, 0x94, 0x05, 0x49, + 0x7c, 0x36, 0xe1, 0xd6, 0x28, 0xca, 0x3f, 0xf5, 0x5a, 0xad, 0x59, 0x1e, 0x4f, 0xa3, 0xfc, 0xaa, 0x45, 0x2d, 0x82, + 0xcf, 0xda, 0xdb, 0xd1, 0xe7, 0xe3, 0x87, 0x3d, 0x9e, 0x43, 0xdf, 0x18, 0xa9, 0x18, 0x80, 0xf0, 0xb1, 0xb6, 0x77, + 0xda, 0xd3, 0xe2, 0x9e, 0x38, 0x51, 0x8a, 0x52, 0x5e, 0x9e, 0x78, 0x57, 0x0c, 0xe0, 0xf6, 0x4f, 0x79, 0xea, 0x81, + 0x2f, 0xc7, 0xb3, 0x74, 0x31, 0x9c, 0xe7, 0x05, 0x0c, 0x30, 0xcb, 0xe2, 0x94, 0xb3, 0xbc, 0x77, 0x9a, 0xe5, 0x40, + 0xb6, 0x56, 0x1e, 0x8d, 0xe2, 0x79, 0x11, 0x3c, 0x9c, 0x5d, 0xf6, 0xd0, 0x56, 0x38, 0xcb, 0xb3, 0x79, 0x3a, 0x92, + 0x73, 0xc5, 0x29, 0x6c, 0x8c, 0x98, 0x9b, 0x15, 0xf4, 0x25, 0x14, 0x80, 0x2f, 0x65, 0x51, 0xde, 0x3a, 0xc3, 0xce, + 0x68, 0xe8, 0xb7, 0x47, 0xec, 0xcc, 0xcb, 0xcf, 0x4e, 0x23, 0xa7, 0xd3, 0x7d, 0xec, 0xa9, 0x7f, 0xfe, 0x8e, 0x0b, + 0x86, 0xfb, 0xca, 0xe2, 0x4e, 0xbb, 0xfd, 0x37, 0x6e, 0xaf, 0x31, 0x0b, 0x01, 0x14, 0x74, 0x66, 0x97, 0x56, 0x91, + 0x25, 0xb0, 0x3e, 0xab, 0x7a, 0xf6, 0x66, 0xe0, 0x37, 0xc5, 0xe9, 0x59, 0xd0, 0x9d, 0x5d, 0x96, 0x88, 0x5d, 0x20, + 0x12, 0x32, 0x25, 0x92, 0xf2, 0x6d, 0xf1, 0x5b, 0x21, 0x7e, 0xb2, 0x1a, 0xe2, 0xae, 0x82, 0xb8, 0xa2, 0x7a, 0x6b, + 0x04, 0xfb, 0x80, 0xc8, 0xdf, 0x29, 0x04, 0x20, 0x13, 0x70, 0x02, 0x73, 0x05, 0x07, 0xbd, 0xfc, 0x66, 0x30, 0xba, + 0xab, 0xc1, 0x78, 0x72, 0x1b, 0x18, 0x79, 0x3a, 0x5a, 0xd4, 0xd7, 0xb5, 0x03, 0xce, 0x69, 0x6f, 0xc2, 0x90, 0x9f, + 0x82, 0x2e, 0x3e, 0x5f, 0xc4, 0x23, 0x3e, 0x11, 0x8f, 0xc4, 0xce, 0x17, 0xa2, 0x6e, 0xa7, 0xdd, 0x16, 0xef, 0x05, + 0x28, 0xb4, 0xa0, 0xe3, 0x63, 0x03, 0x60, 0xa2, 0x2f, 0xd6, 0x7d, 0xc4, 0xe6, 0xbb, 0x5b, 0xbf, 0x54, 0xe3, 0x31, + 0x95, 0x37, 0x28, 0x54, 0x84, 0xfa, 0x66, 0x0b, 0x66, 0xbc, 0xe5, 0xfd, 0x8e, 0x3e, 0xa8, 0x1a, 0x7c, 0xc7, 0x48, + 0xeb, 0x05, 0xcc, 0x33, 0x73, 0x81, 0x7a, 0x69, 0x1f, 0x43, 0x52, 0xad, 0x96, 0x0b, 0x7a, 0x83, 0x63, 0x08, 0x89, + 0x0e, 0x04, 0x9d, 0x7c, 0x50, 0xd0, 0x37, 0x35, 0x32, 0x37, 0x28, 0x9c, 0xcc, 0x85, 0x2d, 0x9f, 0x69, 0xb9, 0x0e, + 0x4a, 0x1a, 0xbc, 0xec, 0x2f, 0x98, 0x6c, 0x00, 0xd2, 0xbb, 0x92, 0xb4, 0xbc, 0x3a, 0x7a, 0x52, 0x2e, 0x5f, 0x36, + 0x24, 0xca, 0x81, 0xaf, 0xcf, 0x27, 0xe8, 0x77, 0xeb, 0xab, 0xeb, 0x46, 0x4a, 0xcd, 0x96, 0xed, 0x0e, 0xb8, 0xce, + 0xca, 0xc2, 0xec, 0x33, 0x5e, 0xe2, 0x28, 0x5f, 0x81, 0x9c, 0xc5, 0xd0, 0xeb, 0xcf, 0xa1, 0x70, 0xd3, 0x94, 0x93, + 0xb6, 0x71, 0xd3, 0xf5, 0x7f, 0x58, 0xf1, 0x98, 0xb2, 0x9d, 0x55, 0x6c, 0x1c, 0x5c, 0x97, 0xe3, 0xa1, 0xb8, 0x76, + 0x58, 0x60, 0xb6, 0xf8, 0x6f, 0xf7, 0x24, 0x1c, 0x8d, 0x56, 0x91, 0xcd, 0xf3, 0x21, 0x26, 0xfd, 0xaf, 0x08, 0x31, + 0xd8, 0xa4, 0xe1, 0x6d, 0x8f, 0x6b, 0xc5, 0xc2, 0x30, 0x7f, 0xc2, 0xfc, 0xaa, 0x02, 0xa3, 0x53, 0x17, 0x71, 0xa9, + 0x41, 0x86, 0x55, 0x14, 0xd8, 0xa8, 0x2b, 0x47, 0x94, 0x60, 0x47, 0x17, 0x3e, 0xfd, 0x79, 0x1a, 0x83, 0x68, 0x3d, + 0x8e, 0x47, 0x74, 0xd1, 0x25, 0x1e, 0xd1, 0xc9, 0x47, 0x8b, 0x32, 0x9d, 0x30, 0x94, 0x0e, 0x05, 0x92, 0xe0, 0xf8, + 0x2c, 0x33, 0x67, 0xec, 0x96, 0x8d, 0xa7, 0x17, 0x86, 0x6e, 0x1e, 0x65, 0xd3, 0x28, 0x4e, 0x03, 0xfc, 0x20, 0x89, + 0xa7, 0x47, 0x0c, 0xb0, 0x8b, 0x07, 0x7f, 0x15, 0xed, 0x3b, 0xae, 0xff, 0x13, 0x08, 0x2e, 0xea, 0x5f, 0x4a, 0xc7, + 0x4f, 0xc3, 0xa5, 0xce, 0x95, 0xeb, 0xa5, 0x20, 0xec, 0xb8, 0xce, 0x6d, 0xa7, 0xc0, 0xca, 0x2e, 0xa3, 0x3f, 0x83, + 0x56, 0x27, 0xe8, 0xb8, 0xcb, 0x2b, 0x60, 0x5c, 0x0c, 0xa8, 0x56, 0x85, 0x4a, 0xe4, 0x1b, 0xcc, 0x21, 0xf9, 0xf3, + 0xfa, 0x5a, 0x7f, 0x3c, 0xa0, 0x71, 0x81, 0x56, 0xa4, 0xdf, 0xc8, 0x4b, 0x98, 0x84, 0x85, 0x7e, 0x16, 0x98, 0x56, + 0xef, 0x1a, 0x5b, 0x4f, 0x6e, 0x25, 0x8c, 0x39, 0x9d, 0xa5, 0x4e, 0x0d, 0x0d, 0x3a, 0xbe, 0x58, 0x33, 0x95, 0x5b, + 0x46, 0xc4, 0xdc, 0x4f, 0x49, 0xe6, 0xd4, 0xaf, 0x3f, 0xc5, 0x18, 0xb8, 0xaf, 0x65, 0x6d, 0x29, 0xf6, 0x1e, 0x9e, + 0xec, 0x0a, 0x21, 0x65, 0x11, 0xeb, 0x86, 0x36, 0x48, 0x0d, 0xdb, 0xfa, 0xe3, 0x10, 0xe8, 0xfc, 0x29, 0xb4, 0x37, + 0x16, 0x8e, 0xba, 0x0b, 0x90, 0xc3, 0x5c, 0x7b, 0x42, 0x51, 0xd3, 0x47, 0x04, 0xec, 0xfe, 0xc6, 0x82, 0x95, 0xbb, + 0x5b, 0xa2, 0x77, 0xff, 0xa4, 0x2c, 0x48, 0xa7, 0x9a, 0xb1, 0xbf, 0x6a, 0x0a, 0x51, 0x07, 0xc3, 0x52, 0xc6, 0x31, + 0x8e, 0x9b, 0x6b, 0x3b, 0x51, 0x04, 0xb9, 0x25, 0xe3, 0x16, 0x98, 0x61, 0x15, 0xe5, 0x20, 0x46, 0x74, 0x0e, 0x4d, + 0x21, 0xd2, 0x46, 0x7a, 0xcb, 0x50, 0x9c, 0x20, 0x04, 0x83, 0x8d, 0x45, 0x5c, 0x86, 0xf0, 0x94, 0x0e, 0xb3, 0x11, + 0xfb, 0xf8, 0xe1, 0x15, 0x5e, 0x93, 0xc8, 0x52, 0x94, 0xa7, 0x99, 0x5b, 0x9e, 0x80, 0x81, 0x85, 0x90, 0xe6, 0xea, + 0x2b, 0x35, 0x00, 0x8c, 0x88, 0x15, 0x59, 0x34, 0x2a, 0x82, 0xc2, 0x4b, 0xdb, 0x1a, 0x08, 0x08, 0xc1, 0x91, 0xc5, + 0x02, 0x30, 0x41, 0xa9, 0x17, 0x07, 0xfc, 0x44, 0xeb, 0x3e, 0x0c, 0xb4, 0xbb, 0x25, 0x1a, 0x01, 0xae, 0x39, 0xa2, + 0x51, 0xa1, 0x8a, 0x59, 0x45, 0x26, 0xba, 0xa3, 0xf8, 0x5c, 0x93, 0x93, 0x52, 0xac, 0xfb, 0xbb, 0x49, 0x74, 0xca, + 0x12, 0x18, 0x12, 0xf8, 0xaa, 0x0d, 0x23, 0x89, 0x57, 0x6b, 0x37, 0x4e, 0x67, 0x73, 0xf9, 0xb5, 0x30, 0x98, 0xb8, + 0x83, 0x07, 0xb8, 0x78, 0x99, 0x61, 0xa0, 0x4e, 0x24, 0x03, 0x39, 0x00, 0x80, 0x48, 0x87, 0x21, 0x08, 0x5d, 0xc5, + 0x2a, 0x50, 0x1a, 0x8f, 0x96, 0xcb, 0x60, 0x7f, 0xcf, 0xb0, 0x34, 0x85, 0xe7, 0x69, 0x9c, 0xe2, 0x63, 0x81, 0x8f, + 0xd1, 0x25, 0x3e, 0x66, 0xf0, 0xa8, 0x71, 0xcf, 0x4b, 0xfb, 0xaf, 0xba, 0x2a, 0x99, 0x5c, 0x01, 0x4b, 0x13, 0x20, + 0xbb, 0xbe, 0x06, 0xb5, 0xa5, 0x49, 0xb0, 0xbb, 0x05, 0xc4, 0x42, 0xee, 0x11, 0xdf, 0x8e, 0xe1, 0x26, 0x19, 0x59, + 0x31, 0x6b, 0x89, 0x72, 0x8b, 0x8c, 0x83, 0x10, 0x7c, 0xc7, 0xdc, 0x69, 0xd8, 0x40, 0x9e, 0xcc, 0x92, 0x79, 0x86, + 0x2f, 0xae, 0x6d, 0x89, 0x8f, 0x7b, 0x08, 0xa2, 0xd0, 0x23, 0x62, 0xa8, 0xcb, 0x98, 0xfc, 0x6c, 0x4f, 0x1c, 0xda, + 0x38, 0x0b, 0x98, 0xa1, 0xe8, 0x85, 0xf2, 0x28, 0x4e, 0x44, 0xe3, 0x15, 0xf8, 0x34, 0xd2, 0x1d, 0x09, 0x9d, 0xdd, + 0xad, 0x0a, 0x36, 0x00, 0x5e, 0x49, 0x04, 0x4e, 0x19, 0x37, 0xb6, 0x28, 0xa7, 0x14, 0x00, 0xb9, 0xcd, 0xab, 0x4f, + 0x3a, 0x01, 0x53, 0x80, 0x11, 0x3d, 0x3a, 0xa6, 0xd9, 0x06, 0x43, 0x20, 0x16, 0xcd, 0xd8, 0xd8, 0xba, 0xf6, 0x5f, + 0xfe, 0xf9, 0x1f, 0x6c, 0x4f, 0x80, 0x98, 0x8d, 0xc7, 0x20, 0xe5, 0xac, 0x75, 0x0d, 0xff, 0xd7, 0x3f, 0xfe, 0xdf, + 0xff, 0xf3, 0x5f, 0x75, 0xdb, 0x14, 0x9a, 0x9e, 0x04, 0xe2, 0x68, 0x41, 0x93, 0x94, 0x52, 0x3c, 0xed, 0x71, 0x94, + 0xae, 0x00, 0xe9, 0x10, 0xb3, 0x18, 0x19, 0x1b, 0x79, 0xb6, 0x05, 0x9a, 0x40, 0x3c, 0x1f, 0x27, 0xec, 0x9c, 0xc9, + 0x0f, 0xcb, 0xe8, 0x41, 0x74, 0xe5, 0x10, 0x2c, 0x18, 0x2e, 0xef, 0xbc, 0xca, 0x6d, 0xa0, 0x68, 0x29, 0x29, 0x5e, + 0x27, 0x98, 0x67, 0x1b, 0x83, 0x36, 0xe7, 0x68, 0xd7, 0x87, 0xf5, 0x40, 0xa5, 0xda, 0xb6, 0x80, 0x97, 0xcc, 0xde, + 0x95, 0x10, 0x37, 0xe1, 0x3a, 0xcd, 0xb1, 0x69, 0xca, 0x8a, 0x62, 0x15, 0x58, 0x40, 0x13, 0xcf, 0xae, 0x9a, 0xd8, + 0xb5, 0x0e, 0x00, 0x40, 0x77, 0x67, 0x47, 0x4c, 0x0b, 0x15, 0x6c, 0x3c, 0x86, 0x0d, 0x8e, 0xba, 0x2d, 0xe1, 0x18, + 0x84, 0x0f, 0xfb, 0xf6, 0x5b, 0x90, 0x25, 0x78, 0xa7, 0xc5, 0xd5, 0x9f, 0xf4, 0xa2, 0xe9, 0x95, 0xb0, 0x33, 0xe6, + 0x10, 0x9d, 0x8d, 0x61, 0xf4, 0x93, 0x81, 0x54, 0x36, 0xfc, 0xb4, 0x8a, 0x31, 0xd6, 0x32, 0xc2, 0xbf, 0xff, 0xcb, + 0x3f, 0xfe, 0x37, 0x18, 0x9b, 0xfa, 0xad, 0xe7, 0x02, 0x68, 0xf5, 0x3f, 0xa1, 0xd5, 0x3c, 0xbd, 0xa5, 0xdd, 0x5f, + 0xfe, 0xfe, 0xbf, 0x43, 0x33, 0xba, 0x28, 0x05, 0x7c, 0x42, 0x10, 0x0d, 0xd1, 0x36, 0xfd, 0x55, 0x20, 0xd5, 0x06, + 0x59, 0x3b, 0xd3, 0x3f, 0x21, 0xd8, 0x05, 0xcf, 0x66, 0x37, 0x82, 0x83, 0x50, 0x0f, 0x93, 0xac, 0x60, 0x1a, 0x1e, + 0xa1, 0x4f, 0x7e, 0x1d, 0x40, 0x34, 0xd7, 0x0c, 0x76, 0x6d, 0x61, 0xe9, 0x71, 0xc4, 0x0a, 0xad, 0xdc, 0x84, 0xf5, + 0x05, 0x2c, 0x18, 0x27, 0x74, 0x28, 0xdc, 0x03, 0x4b, 0x26, 0x9e, 0xe0, 0x81, 0x04, 0x9c, 0x5b, 0xff, 0xf8, 0xda, + 0xea, 0xc1, 0x34, 0xc3, 0x89, 0xb1, 0x44, 0x84, 0x4b, 0x8d, 0x00, 0x7f, 0x41, 0x08, 0x1f, 0xeb, 0xe7, 0xe8, 0x52, + 0x3f, 0xa3, 0xa0, 0x16, 0x13, 0x80, 0xbe, 0x9d, 0xa2, 0x31, 0x66, 0xce, 0x20, 0xb2, 0x33, 0x2a, 0xf7, 0xde, 0x48, + 0xf2, 0x11, 0xc2, 0xf8, 0x18, 0x73, 0x61, 0xf1, 0xe6, 0xd3, 0x3c, 0x67, 0xc7, 0x49, 0x76, 0x81, 0x31, 0x43, 0x22, + 0xd2, 0x9a, 0xfa, 0xf2, 0xdf, 0xfe, 0xd5, 0xf7, 0xff, 0xed, 0x5f, 0xd7, 0x34, 0x98, 0xc0, 0x9e, 0x00, 0x23, 0x9f, + 0x85, 0x9a, 0xce, 0x0d, 0xb4, 0x56, 0x0f, 0x8a, 0x78, 0xae, 0xae, 0x91, 0x88, 0x63, 0xa9, 0xc4, 0x5b, 0x3e, 0x12, + 0xda, 0x9a, 0x29, 0x6e, 0x9f, 0x05, 0x21, 0x5b, 0x33, 0x0d, 0x56, 0xdd, 0x32, 0xcf, 0x89, 0x1b, 0xdc, 0x40, 0x97, + 0x5f, 0x89, 0xf1, 0x6a, 0x30, 0x6e, 0x85, 0xc0, 0x03, 0x6d, 0x26, 0xf4, 0xdd, 0x33, 0xa1, 0xad, 0x02, 0xb1, 0x0c, + 0x52, 0x77, 0xd5, 0x00, 0xf2, 0xac, 0x03, 0x9a, 0x80, 0x9a, 0xc4, 0x95, 0xad, 0x40, 0xe6, 0xd6, 0x69, 0xde, 0x7f, + 0x83, 0x97, 0x1d, 0x91, 0x78, 0x64, 0x29, 0x14, 0x64, 0xd8, 0x30, 0x32, 0x6c, 0xa4, 0x46, 0x35, 0x6d, 0x0a, 0x74, + 0xfc, 0xb2, 0xd5, 0xb6, 0xc3, 0x31, 0x76, 0xaf, 0x69, 0x7f, 0x26, 0xb5, 0x7f, 0x2c, 0xed, 0x7d, 0xa9, 0xfd, 0xf1, + 0x93, 0x36, 0x0d, 0xed, 0x1f, 0xaf, 0xd5, 0xfe, 0x48, 0xb9, 0x01, 0x8e, 0x1c, 0xda, 0x9b, 0x18, 0xdd, 0x32, 0x6c, + 0x0d, 0xd4, 0xc4, 0x83, 0xe1, 0x84, 0x0d, 0x3f, 0x49, 0x33, 0x8b, 0x10, 0xc0, 0x40, 0x94, 0x36, 0x26, 0x05, 0x06, + 0x60, 0x32, 0x9c, 0x94, 0x7a, 0xd3, 0xe3, 0xa3, 0x31, 0x01, 0x73, 0x17, 0x63, 0x86, 0xa2, 0x1f, 0xd6, 0xec, 0x2b, + 0x56, 0x6e, 0xe1, 0x38, 0x62, 0xc3, 0x88, 0x67, 0xc0, 0x6c, 0x0b, 0x07, 0x3b, 0xf1, 0x16, 0x22, 0x58, 0x18, 0xd8, + 0xef, 0xdf, 0xed, 0x1f, 0xd8, 0xde, 0x69, 0x36, 0xba, 0x0a, 0x6c, 0x70, 0xc6, 0xc0, 0x9a, 0x72, 0x7d, 0x3e, 0x61, + 0xa9, 0xa3, 0x3c, 0x9f, 0x2c, 0x61, 0xe0, 0x00, 0x9e, 0x89, 0x6f, 0x5b, 0x34, 0x0f, 0x3a, 0x80, 0xb0, 0xf4, 0xf1, + 0xcb, 0xfe, 0x2e, 0x17, 0xdf, 0x85, 0xe5, 0x39, 0x3e, 0xf6, 0x31, 0xd5, 0x63, 0x77, 0x0b, 0x1e, 0xf0, 0x65, 0x1f, + 0xf5, 0x1e, 0xbd, 0x6d, 0x2c, 0x96, 0xdc, 0x86, 0x01, 0x0e, 0x31, 0xe9, 0x0b, 0x14, 0x0a, 0x6a, 0x75, 0x12, 0x20, + 0x62, 0xf0, 0x08, 0x63, 0x6d, 0xa9, 0x71, 0x11, 0x42, 0xd5, 0x5f, 0x3b, 0x2e, 0x95, 0xdd, 0x4a, 0xf3, 0x8e, 0xb0, + 0x01, 0x39, 0x2e, 0xd8, 0x7b, 0xa4, 0x4b, 0x84, 0xa9, 0x43, 0x45, 0xeb, 0x20, 0xd0, 0x35, 0x95, 0xb9, 0x22, 0x3a, + 0x18, 0xc0, 0x90, 0x99, 0x2b, 0x00, 0x81, 0xbf, 0x84, 0xf6, 0x89, 0xf9, 0xfd, 0x37, 0xf1, 0xa9, 0x26, 0x4d, 0x9c, + 0xc3, 0x3f, 0x79, 0x57, 0xcc, 0xbb, 0x3a, 0xa1, 0x96, 0x2a, 0xd8, 0x80, 0x51, 0x30, 0x0c, 0xca, 0xb4, 0x55, 0x54, + 0x09, 0xec, 0xb4, 0x24, 0x9a, 0x15, 0x2c, 0x50, 0x0f, 0x32, 0xee, 0x80, 0xe1, 0x8b, 0xe5, 0x40, 0x8f, 0x69, 0xcf, + 0x95, 0x7c, 0xb2, 0x30, 0x03, 0x13, 0x8f, 0xda, 0xed, 0x1e, 0x5e, 0xaa, 0x68, 0x45, 0x60, 0x1d, 0xa4, 0x41, 0xc2, + 0xc6, 0xbc, 0xe4, 0x78, 0x6b, 0x7f, 0xa1, 0x22, 0x41, 0x7e, 0x77, 0x27, 0x67, 0x53, 0xcb, 0xc7, 0xff, 0xbf, 0x6d, + 0xec, 0x51, 0x90, 0xf2, 0x49, 0x8b, 0xae, 0xf1, 0xe0, 0x15, 0x49, 0x80, 0xc8, 0x7c, 0x5f, 0x18, 0x13, 0x0d, 0x19, + 0x46, 0xc9, 0x4a, 0x0e, 0xce, 0x37, 0x88, 0x9b, 0xdc, 0x6c, 0x07, 0x72, 0x7a, 0x29, 0x54, 0xb6, 0x1c, 0xac, 0xd9, + 0x76, 0xa5, 0x7f, 0xb4, 0xdc, 0x58, 0x45, 0xbc, 0xea, 0x6f, 0x4b, 0x14, 0x32, 0x62, 0x73, 0xa5, 0x50, 0x51, 0x0b, + 0xd1, 0xc3, 0xc4, 0x69, 0x39, 0x6a, 0x77, 0xab, 0xc5, 0x5c, 0x92, 0xb8, 0x38, 0x24, 0x71, 0x41, 0xe2, 0xef, 0x68, + 0x21, 0xe6, 0x1e, 0x46, 0xc9, 0xd0, 0x41, 0x00, 0xac, 0x96, 0xf5, 0x04, 0xa8, 0xe9, 0xaa, 0xc8, 0x91, 0xff, 0x18, + 0x89, 0x5b, 0x0a, 0x61, 0xb9, 0x82, 0x4a, 0x27, 0x47, 0x65, 0xd9, 0x63, 0xcc, 0x39, 0xfc, 0x20, 0x2f, 0x81, 0x88, + 0xbb, 0xbf, 0xfa, 0xfb, 0x89, 0xed, 0xd2, 0x3d, 0xf2, 0x7e, 0x36, 0x3e, 0x4a, 0x67, 0x2b, 0x66, 0xb7, 0x3d, 0x58, + 0x06, 0xb3, 0xa7, 0xfc, 0x84, 0xe4, 0x4d, 0x7d, 0x4d, 0x36, 0xa7, 0xfe, 0x3f, 0x87, 0x38, 0xc2, 0x1b, 0xc7, 0x46, + 0x13, 0x9d, 0x46, 0xbe, 0x6a, 0x11, 0x7f, 0xda, 0xd8, 0x55, 0x1c, 0x81, 0x7c, 0xbd, 0x2e, 0x92, 0xf5, 0xcd, 0xed, + 0x91, 0xac, 0xe2, 0x8e, 0x91, 0xac, 0x6f, 0x7e, 0xe7, 0x48, 0xd6, 0xd7, 0x66, 0x24, 0x0b, 0x05, 0xf4, 0xab, 0x5f, + 0x13, 0x6d, 0xca, 0xb3, 0x8b, 0x22, 0xec, 0xc8, 0xcc, 0x09, 0x90, 0x75, 0x18, 0x76, 0xfa, 0xeb, 0x47, 0x98, 0x60, + 0xa2, 0x46, 0x7c, 0x89, 0x02, 0x4a, 0x22, 0xd9, 0x13, 0xd4, 0x8a, 0x0c, 0xe7, 0xb4, 0x75, 0x56, 0x65, 0xeb, 0xa1, + 0xba, 0x46, 0x06, 0xae, 0xaf, 0xab, 0x43, 0x6d, 0x5d, 0x15, 0xf0, 0x09, 0xe8, 0x3b, 0xb0, 0xba, 0x63, 0x77, 0x53, + 0xa5, 0xf3, 0x99, 0x23, 0xf4, 0xd4, 0x29, 0x8d, 0x60, 0xa2, 0x85, 0xfd, 0x5f, 0x0e, 0x3b, 0xbd, 0xed, 0xce, 0x14, + 0x7a, 0x83, 0x02, 0x87, 0xb7, 0x76, 0x6f, 0x7b, 0x1b, 0xdf, 0x2e, 0xd4, 0x5b, 0x17, 0xdf, 0x62, 0xf5, 0xb6, 0x83, + 0x6f, 0x43, 0xf5, 0xf6, 0x08, 0xdf, 0x46, 0xea, 0xed, 0x31, 0xbe, 0x9d, 0xdb, 0xe5, 0x21, 0xd3, 0xc0, 0x3d, 0x06, + 0xbe, 0x22, 0x6f, 0x26, 0x50, 0x65, 0xb0, 0xe9, 0xf1, 0xc3, 0x08, 0xd1, 0x59, 0x10, 0x7b, 0xc2, 0xbb, 0x0c, 0x72, + 0xef, 0x02, 0x34, 0x4e, 0x40, 0xd9, 0x86, 0xcf, 0xf1, 0x3b, 0x1c, 0xe0, 0x24, 0x1d, 0xc4, 0x53, 0xa6, 0x3e, 0x48, + 0xac, 0xb0, 0x06, 0x03, 0xf6, 0xb0, 0x7d, 0x54, 0xf6, 0xf4, 0x3a, 0x89, 0x78, 0x96, 0xca, 0xe6, 0xa0, 0x95, 0xab, + 0xea, 0xc4, 0x74, 0x2d, 0xbd, 0xc2, 0x6b, 0xf4, 0x97, 0x11, 0x8f, 0x18, 0x83, 0x61, 0xd6, 0xba, 0x04, 0x0f, 0x76, + 0xa5, 0x4e, 0x43, 0x88, 0xb4, 0x4e, 0x23, 0x9c, 0xf4, 0xdb, 0x41, 0x74, 0xa6, 0x9f, 0xdf, 0x80, 0xa5, 0x1d, 0x9d, + 0xc9, 0x96, 0xeb, 0x75, 0x18, 0x81, 0x68, 0xea, 0x2f, 0x05, 0x04, 0x99, 0x62, 0xb0, 0x34, 0xe8, 0x49, 0x4b, 0xfd, + 0x85, 0xd4, 0xa9, 0x6b, 0x34, 0x9a, 0xbe, 0x5e, 0x04, 0x14, 0xad, 0x0a, 0x76, 0xc1, 0xe0, 0xa7, 0x52, 0x41, 0x61, + 0xa8, 0xc0, 0x02, 0x51, 0xbd, 0x46, 0x95, 0xe9, 0x60, 0xc3, 0x5a, 0x85, 0x66, 0x29, 0x5d, 0x66, 0x9e, 0xee, 0xe8, + 0xa3, 0x9d, 0x65, 0xf1, 0xfa, 0x59, 0x67, 0x88, 0xff, 0x49, 0xe1, 0xfd, 0xd9, 0x78, 0x3c, 0xbe, 0x51, 0xb7, 0x7d, + 0x36, 0x1a, 0xb3, 0x2e, 0xdb, 0xe9, 0x61, 0xe4, 0xbf, 0x25, 0xc5, 0x69, 0xa7, 0x24, 0xda, 0x2d, 0xee, 0xd6, 0x18, + 0x25, 0x2f, 0xa8, 0xbb, 0xbb, 0x2b, 0xc1, 0x12, 0xa8, 0xb2, 0x00, 0xe1, 0x7f, 0x16, 0xa7, 0x41, 0xbb, 0xf4, 0xcf, + 0xa5, 0xd6, 0xf8, 0xec, 0xc9, 0x93, 0x27, 0xa5, 0x3f, 0x52, 0x6f, 0xed, 0xd1, 0xa8, 0xf4, 0x87, 0x0b, 0x8d, 0x46, + 0xbb, 0x3d, 0x1e, 0x97, 0x7e, 0xac, 0x0a, 0xb6, 0xbb, 0xc3, 0xd1, 0x76, 0xb7, 0xf4, 0x2f, 0x8c, 0x16, 0xa5, 0xcf, + 0xe4, 0x5b, 0xce, 0x46, 0xb5, 0xe3, 0x83, 0xc7, 0x6d, 0xa8, 0x14, 0x8c, 0xb6, 0x40, 0xef, 0x52, 0x3c, 0x06, 0xd1, + 0x9c, 0x67, 0x60, 0xd8, 0x95, 0xbd, 0x02, 0xe4, 0xf3, 0x58, 0x4a, 0x78, 0xf1, 0xbd, 0x5f, 0x94, 0xea, 0xaf, 0x4c, + 0xa9, 0x8e, 0xcc, 0x4c, 0xd2, 0xbc, 0x20, 0x6d, 0xd0, 0xac, 0x46, 0xce, 0xa2, 0xea, 0x57, 0x61, 0x51, 0x09, 0x7b, + 0x94, 0x36, 0xd8, 0x52, 0xc8, 0xf8, 0x1f, 0xd6, 0xc9, 0xf8, 0xef, 0x6f, 0x97, 0xf1, 0xa7, 0x77, 0x13, 0xf1, 0xdf, + 0xff, 0xce, 0x22, 0xfe, 0x07, 0x53, 0xc4, 0x0b, 0x21, 0xb6, 0x07, 0xa6, 0x33, 0xd9, 0xcc, 0xa7, 0xd9, 0x65, 0x0b, + 0xb7, 0x44, 0x6e, 0x93, 0xf4, 0x9c, 0xde, 0x49, 0xf8, 0xaf, 0xc8, 0x07, 0x53, 0x83, 0x19, 0x1f, 0x0f, 0xe6, 0xd9, + 0xd9, 0x59, 0xc2, 0x94, 0x8c, 0x37, 0x2a, 0xc8, 0x1c, 0x7f, 0x97, 0x86, 0xf6, 0x3b, 0xf4, 0x8c, 0xab, 0x92, 0xf1, + 0x18, 0x8a, 0xc6, 0x63, 0x5b, 0xe5, 0x4b, 0x83, 0x3c, 0xa3, 0x56, 0x6f, 0x6b, 0x25, 0xd4, 0xea, 0x8b, 0x2f, 0xcc, + 0x32, 0xb3, 0x40, 0x86, 0xf4, 0x4c, 0x63, 0x44, 0xd6, 0x8c, 0xe2, 0x02, 0xf7, 0x60, 0xf5, 0xb1, 0x63, 0xb4, 0x77, + 0xa6, 0xa0, 0x54, 0xe2, 0x21, 0x9e, 0x8b, 0x34, 0x3f, 0x2c, 0x23, 0x72, 0xdb, 0x97, 0x91, 0xab, 0xce, 0xbf, 0x8d, + 0x6f, 0x18, 0x56, 0x67, 0xde, 0xb0, 0xf8, 0x32, 0xbf, 0xe5, 0xe9, 0xd5, 0xab, 0x91, 0xb3, 0x87, 0x97, 0x7f, 0x8b, + 0x77, 0x69, 0x23, 0x6f, 0x50, 0x80, 0x1d, 0x86, 0x26, 0xa6, 0xa5, 0x20, 0x58, 0x75, 0x81, 0xa2, 0xaa, 0xec, 0x19, + 0x9d, 0x64, 0x7a, 0x19, 0x0e, 0x39, 0xa8, 0x91, 0x25, 0x30, 0x07, 0x93, 0xba, 0x90, 0x3e, 0x66, 0x2f, 0x92, 0x6e, + 0xce, 0xe5, 0x57, 0xcf, 0xe9, 0x70, 0x66, 0x21, 0xf5, 0x87, 0x4c, 0xc7, 0xa8, 0x7a, 0xd2, 0x79, 0x08, 0xcd, 0x30, + 0x2a, 0xd5, 0x19, 0x08, 0x10, 0x6e, 0x86, 0x9f, 0x68, 0x12, 0x43, 0xa8, 0x83, 0x82, 0x8a, 0x7a, 0xd7, 0xd7, 0xe6, + 0x97, 0x42, 0x6b, 0x5f, 0x95, 0x6c, 0xf0, 0x00, 0xc7, 0x4f, 0xfc, 0xa2, 0x36, 0xc8, 0xe6, 0xdc, 0xc1, 0x33, 0x80, + 0x05, 0x1e, 0x31, 0x78, 0x3b, 0xed, 0x36, 0xa8, 0x18, 0x5f, 0x7c, 0x07, 0xca, 0xd1, 0x9d, 0x05, 0xbe, 0x6c, 0xdd, + 0xb9, 0xc4, 0xd2, 0x77, 0xd9, 0x2a, 0x12, 0xdf, 0xbf, 0x2f, 0x11, 0x35, 0xee, 0x0e, 0xa9, 0x45, 0x6c, 0xbe, 0xfb, + 0xca, 0x77, 0x34, 0x08, 0xeb, 0xae, 0xe2, 0x60, 0x99, 0x5b, 0x5b, 0x2f, 0xc4, 0xb6, 0xc2, 0xaa, 0x59, 0x06, 0xe7, + 0x16, 0x9d, 0x59, 0x5c, 0x18, 0x01, 0xfc, 0xda, 0x36, 0x28, 0x55, 0x04, 0x5f, 0x84, 0xe1, 0xf7, 0xd0, 0xc5, 0x15, + 0x8e, 0xb7, 0x02, 0xba, 0xe1, 0xf2, 0x56, 0x90, 0xa3, 0x33, 0xac, 0x19, 0x5d, 0x55, 0xa9, 0x82, 0xd2, 0x3c, 0x82, + 0x31, 0x90, 0xa1, 0x48, 0x3a, 0xac, 0x71, 0x2a, 0xf4, 0x16, 0x4c, 0x43, 0x02, 0x58, 0xfb, 0x75, 0xe8, 0xd6, 0xd8, + 0x0a, 0x6c, 0x21, 0x2d, 0x40, 0xe9, 0x61, 0x87, 0xbe, 0x55, 0x03, 0x3d, 0x5d, 0x0e, 0xc0, 0xdf, 0xe8, 0xe4, 0x9d, + 0xf8, 0xc5, 0x85, 0x07, 0xff, 0xac, 0x3f, 0x2c, 0x40, 0xca, 0x9f, 0x7e, 0x8a, 0x39, 0xd8, 0xd4, 0xb3, 0x16, 0x86, + 0x5f, 0x28, 0x4e, 0x2b, 0xd5, 0x21, 0x1d, 0x45, 0x8b, 0x2b, 0x63, 0xbd, 0x79, 0x81, 0xbe, 0x20, 0x39, 0x3d, 0x41, + 0x9a, 0xa5, 0xac, 0x57, 0x4f, 0x39, 0x30, 0xfd, 0x0e, 0x45, 0xac, 0xa3, 0x45, 0x86, 0xbe, 0x23, 0xbf, 0x02, 0xdf, + 0x51, 0xa8, 0xd1, 0xb6, 0x72, 0x3a, 0xda, 0x2b, 0xdb, 0x07, 0x92, 0xb6, 0x9b, 0x64, 0x2d, 0xe4, 0xcb, 0xce, 0xd5, + 0x3a, 0xe7, 0xe8, 0xb6, 0x03, 0x78, 0x0c, 0x0a, 0xab, 0x7f, 0x46, 0xe6, 0x42, 0xb3, 0x98, 0x0e, 0xe0, 0xef, 0x02, + 0x59, 0x10, 0x8d, 0xf1, 0x0b, 0x8b, 0x77, 0x69, 0x79, 0x4a, 0xd9, 0xaf, 0x0b, 0x54, 0xeb, 0x41, 0xe7, 0x09, 0x78, + 0x7b, 0x77, 0x1e, 0xfe, 0x66, 0xf4, 0x4b, 0x49, 0x23, 0x75, 0x89, 0xd9, 0xb6, 0x7b, 0x28, 0x2f, 0x92, 0xe8, 0x0a, + 0x9c, 0x4e, 0xb2, 0x31, 0x4e, 0x31, 0x7a, 0xdc, 0x9b, 0x65, 0x32, 0x93, 0x24, 0x67, 0x09, 0xfd, 0x8c, 0x89, 0x5c, + 0x8a, 0xed, 0x47, 0xb3, 0x4b, 0xb5, 0x1a, 0x9d, 0x46, 0x86, 0xc8, 0xef, 0x9a, 0x08, 0xb2, 0x3e, 0xf3, 0xa4, 0x9e, + 0xcc, 0xb0, 0x03, 0x30, 0x08, 0xc3, 0xa6, 0x95, 0x0b, 0xa8, 0xda, 0x50, 0x62, 0xa4, 0xc2, 0x54, 0x03, 0x59, 0xfe, + 0x36, 0xa8, 0xca, 0xa8, 0x60, 0x3d, 0xfc, 0xd4, 0x65, 0x0c, 0xae, 0xad, 0x34, 0x9e, 0xa6, 0xf1, 0x68, 0x94, 0xb0, + 0x9e, 0xb2, 0x8f, 0xac, 0xce, 0x23, 0xcc, 0x24, 0x31, 0x97, 0xac, 0xbe, 0x2a, 0x06, 0xf1, 0x34, 0x9d, 0xa2, 0x53, + 0xb0, 0xd7, 0xf0, 0x7b, 0x95, 0x2b, 0xc9, 0x29, 0x53, 0x2c, 0xda, 0x15, 0xf1, 0xe8, 0xb9, 0x8e, 0xcb, 0x0e, 0x18, + 0x8b, 0xb4, 0xe0, 0xed, 0x1e, 0xcf, 0x66, 0x41, 0x6b, 0xbb, 0x8e, 0x08, 0x56, 0x69, 0x14, 0xbc, 0x15, 0x68, 0x79, + 0x68, 0x1d, 0x08, 0x2d, 0x67, 0xf9, 0x1d, 0x59, 0x46, 0x03, 0xe0, 0x37, 0x11, 0x75, 0x51, 0x59, 0x47, 0xe6, 0xaf, + 0xb3, 0x5b, 0x3e, 0x5f, 0xbd, 0x5b, 0x3e, 0x57, 0xbb, 0xe5, 0x66, 0x8e, 0xfd, 0x6c, 0xdc, 0xc1, 0xff, 0x7a, 0x15, + 0x42, 0xb0, 0x2a, 0x40, 0x0e, 0x0b, 0xed, 0xe2, 0x56, 0x17, 0xfe, 0x8f, 0x86, 0x6e, 0x7b, 0xf8, 0x9f, 0x0f, 0x16, + 0x60, 0xdb, 0xc2, 0x42, 0xfc, 0xd7, 0xae, 0x55, 0x75, 0x1e, 0x62, 0x1d, 0xf6, 0xda, 0x59, 0xae, 0xeb, 0xde, 0xbc, + 0x69, 0x41, 0x5e, 0x71, 0x27, 0x50, 0xc2, 0x18, 0x5c, 0xb5, 0xe8, 0xf4, 0x14, 0x4a, 0xc7, 0xd9, 0x70, 0x5e, 0xfc, + 0xad, 0x84, 0x5f, 0x12, 0xf1, 0xc6, 0x2d, 0xdd, 0x18, 0x47, 0x75, 0x15, 0x69, 0x49, 0x6a, 0x84, 0x85, 0x5e, 0xa7, + 0xa0, 0x00, 0xc6, 0x64, 0x4e, 0xd7, 0x7f, 0xb8, 0x62, 0x13, 0xfc, 0x7f, 0x59, 0x9b, 0x95, 0xc8, 0xfc, 0x47, 0x89, + 0x71, 0x23, 0x11, 0x7e, 0x15, 0x0d, 0xcc, 0x35, 0x6c, 0x3f, 0x59, 0x0d, 0xee, 0xa1, 0x9a, 0xe9, 0x48, 0x29, 0x05, + 0xa9, 0x77, 0xc0, 0x0b, 0x88, 0xe6, 0x09, 0xbf, 0x79, 0xd4, 0x75, 0x9c, 0xb1, 0x34, 0xea, 0x0d, 0x02, 0xbd, 0x6a, + 0x7b, 0x47, 0x29, 0xfd, 0xd9, 0xe7, 0x0f, 0xf1, 0x3f, 0x11, 0x38, 0x3b, 0xad, 0x7c, 0x23, 0x11, 0x1b, 0x40, 0xdf, + 0x68, 0x5a, 0x73, 0x7e, 0x84, 0x06, 0x27, 0xff, 0xe7, 0xae, 0xad, 0xd1, 0x58, 0xbf, 0x53, 0x73, 0x69, 0x95, 0xfe, + 0xaa, 0xd6, 0xbf, 0x6e, 0xf0, 0x3b, 0xb6, 0x1d, 0x0a, 0x87, 0xa0, 0xde, 0x56, 0xc6, 0x03, 0x97, 0x1a, 0x2b, 0x8a, + 0xdf, 0xb5, 0x7d, 0x65, 0x12, 0x53, 0x8f, 0x69, 0x78, 0xaa, 0x9d, 0x48, 0x79, 0x78, 0x8f, 0x3d, 0x84, 0x1f, 0xf9, + 0x25, 0x0b, 0x1f, 0xe0, 0xd7, 0xd8, 0xac, 0xcb, 0x69, 0x92, 0x82, 0x59, 0x35, 0xe1, 0x7c, 0x16, 0x6c, 0x6d, 0x5d, + 0x5c, 0x5c, 0xf8, 0x17, 0xdb, 0x7e, 0x96, 0x9f, 0x6d, 0x75, 0xdb, 0xed, 0x36, 0x7e, 0x44, 0xcb, 0xb6, 0xce, 0x63, + 0x76, 0xf1, 0x14, 0xdc, 0x0f, 0xfb, 0xb1, 0xf5, 0xc4, 0x7a, 0xbc, 0x6d, 0xed, 0x3c, 0xb2, 0x2d, 0x52, 0x00, 0x50, + 0xb2, 0x6d, 0x5b, 0x42, 0x01, 0x84, 0x36, 0x14, 0xf7, 0x77, 0xcf, 0x94, 0x0d, 0x87, 0x97, 0x14, 0x84, 0x85, 0x04, + 0xfe, 0x5b, 0xf6, 0x89, 0xd5, 0xb7, 0xba, 0x28, 0x6b, 0x49, 0x35, 0xa2, 0x5e, 0x71, 0xbf, 0x0f, 0xa3, 0x59, 0x40, + 0x6c, 0x64, 0x16, 0x62, 0x98, 0x4c, 0x94, 0xd2, 0x14, 0x68, 0x97, 0x9e, 0xc2, 0x13, 0x66, 0xb5, 0x59, 0xf0, 0xfc, + 0xa6, 0xfb, 0x18, 0x74, 0xdc, 0x79, 0xeb, 0xe1, 0xb0, 0xdd, 0xea, 0x58, 0x9d, 0x56, 0xd7, 0x7f, 0x6c, 0x75, 0xc5, + 0xff, 0x83, 0x8c, 0xdc, 0xb6, 0x3a, 0xf0, 0xb4, 0x6d, 0xc1, 0xfb, 0xf9, 0x43, 0x91, 0x5b, 0x12, 0xd9, 0x5b, 0xfd, + 0x5d, 0xfc, 0x4d, 0x29, 0x40, 0xea, 0x73, 0x5b, 0xfc, 0x0a, 0x9e, 0xfd, 0x99, 0x59, 0xda, 0x79, 0xb2, 0xb2, 0xb8, + 0xfb, 0x78, 0x65, 0xf1, 0xf6, 0xa3, 0x95, 0xc5, 0x0f, 0x77, 0xea, 0xc5, 0x5b, 0x67, 0xa2, 0x4a, 0xcb, 0x85, 0xd0, + 0x9e, 0x46, 0xc0, 0x28, 0x97, 0x4e, 0x07, 0xe0, 0x6c, 0x5b, 0x2d, 0xfc, 0xf3, 0xb8, 0xeb, 0xea, 0x5e, 0xa7, 0xd8, + 0x4b, 0x63, 0xf9, 0xf8, 0x09, 0x60, 0xf9, 0xb2, 0xfb, 0x68, 0x88, 0xed, 0x08, 0x51, 0xf8, 0x77, 0xbe, 0xfd, 0x64, + 0x08, 0x1a, 0xc1, 0xc2, 0x7f, 0xf0, 0xdf, 0x64, 0xa7, 0x3b, 0x14, 0x2f, 0x6d, 0xac, 0xff, 0xb6, 0xf3, 0xb8, 0x80, + 0xa6, 0xf8, 0xdf, 0x2f, 0xda, 0x84, 0x46, 0x03, 0xde, 0x1c, 0xf7, 0x21, 0xd0, 0xe8, 0xc9, 0xa4, 0xeb, 0x7f, 0x7e, + 0xfe, 0xd8, 0x7f, 0x32, 0xe9, 0x3c, 0xfe, 0x56, 0xbc, 0x25, 0x40, 0xc1, 0xcf, 0xf1, 0xdf, 0xb7, 0xdb, 0xed, 0x49, + 0xab, 0xe3, 0x3f, 0x39, 0xdf, 0xf6, 0xb7, 0x93, 0xd6, 0x23, 0xff, 0x09, 0xfe, 0xab, 0x86, 0x9b, 0x64, 0x53, 0x66, + 0x5b, 0xb8, 0xde, 0x0d, 0xbf, 0xd7, 0x9c, 0xa3, 0xfb, 0xd0, 0xda, 0x79, 0xf8, 0xf2, 0x09, 0xac, 0xd1, 0xa4, 0xd3, + 0x85, 0xff, 0x5f, 0xf7, 0xf8, 0x2d, 0x12, 0x5e, 0x0e, 0x1c, 0x31, 0x4c, 0x2f, 0x56, 0x84, 0xa3, 0x0f, 0xba, 0x3d, + 0xf0, 0xfe, 0xb4, 0x2e, 0x00, 0xc2, 0xf8, 0xad, 0x01, 0x10, 0xce, 0xef, 0x16, 0x01, 0xa1, 0x5f, 0x1b, 0xf8, 0x1d, + 0x23, 0x20, 0x7f, 0x6a, 0x06, 0xb9, 0x2f, 0xd9, 0x52, 0xa0, 0xa3, 0xe9, 0xac, 0xbd, 0x65, 0xce, 0xe1, 0x97, 0xf8, + 0xe3, 0x06, 0x65, 0x0f, 0x5a, 0x73, 0x6e, 0xc6, 0x83, 0x32, 0xdc, 0xc8, 0x97, 0xf2, 0xe2, 0x43, 0xc1, 0xd7, 0x10, + 0x24, 0xbe, 0x9d, 0x20, 0xdf, 0xde, 0x8d, 0x1e, 0xf1, 0xef, 0x4c, 0x8f, 0x82, 0x1b, 0xf4, 0xa8, 0x45, 0xdc, 0x29, + 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0x9c, 0xbe, 0xc5, 0xb6, 0xc5, 0xb0, 0xa8, 0xb0, 0x45, 0xce, 0xe6, 0xd3, + 0x5f, 0x73, 0x44, 0x20, 0xd2, 0xcd, 0x43, 0x5b, 0x46, 0x61, 0x66, 0xf8, 0xd1, 0x62, 0xf5, 0x72, 0x2e, 0xae, 0x34, + 0x85, 0x74, 0x1f, 0x71, 0x47, 0x47, 0x70, 0xf0, 0x06, 0x40, 0xb8, 0xc8, 0x78, 0x84, 0xbf, 0x8a, 0x05, 0xe4, 0xa6, + 0xdf, 0xcf, 0x8a, 0x79, 0xc2, 0x30, 0x9d, 0x66, 0x28, 0x3e, 0x20, 0x0b, 0x8f, 0xf2, 0xae, 0x21, 0xa6, 0xb0, 0x7f, + 0x83, 0xe9, 0xf7, 0xea, 0xec, 0x60, 0x8a, 0x71, 0x84, 0x37, 0x6c, 0x14, 0x47, 0x8e, 0xed, 0xcc, 0x60, 0x23, 0xc3, + 0x2c, 0xad, 0x5a, 0xee, 0x3b, 0xa5, 0xbd, 0xbb, 0xb6, 0xfa, 0x69, 0xa6, 0x1c, 0x3f, 0x75, 0x17, 0x1e, 0xca, 0xb8, + 0xa3, 0x2d, 0x1d, 0x03, 0x18, 0x5f, 0x95, 0xe4, 0xa8, 0x03, 0x2a, 0x63, 0xc2, 0x16, 0xd6, 0x44, 0xc7, 0xef, 0x82, + 0x77, 0x41, 0xc5, 0xf8, 0xe9, 0xb0, 0xef, 0x9d, 0xd6, 0x36, 0x58, 0x3b, 0x46, 0x37, 0x3d, 0xd0, 0x91, 0xfe, 0xa5, + 0x1f, 0xfd, 0x6b, 0x74, 0xf5, 0x0b, 0x03, 0xb6, 0xe0, 0x88, 0xcf, 0x04, 0xee, 0xb6, 0xf8, 0x44, 0x83, 0x48, 0x28, + 0xc1, 0x0b, 0x73, 0x50, 0xe6, 0x98, 0xbf, 0x4a, 0x26, 0x3e, 0x4d, 0x26, 0x7e, 0x80, 0xb0, 0xac, 0x9a, 0x70, 0x77, + 0x41, 0x67, 0x23, 0xf8, 0x23, 0x9a, 0x98, 0x68, 0x8a, 0xa1, 0xf2, 0xd0, 0xa0, 0x29, 0xbe, 0xbb, 0x35, 0x22, 0x73, + 0x4f, 0x03, 0x44, 0x04, 0x0e, 0xe5, 0xdf, 0xaa, 0x58, 0x3d, 0xc8, 0xa0, 0x16, 0x38, 0xfa, 0xf8, 0xb3, 0x2f, 0xf4, + 0x67, 0x29, 0x64, 0x26, 0x02, 0x21, 0x8d, 0xd2, 0x6a, 0xa8, 0x2a, 0x34, 0x56, 0x3c, 0xbd, 0x3a, 0x90, 0xdf, 0x3c, + 0xb0, 0x31, 0x4a, 0x4d, 0xa7, 0x13, 0xd5, 0xf7, 0xd6, 0x36, 0x41, 0x35, 0xd2, 0xaf, 0xa0, 0x52, 0x82, 0x01, 0x6a, + 0x3f, 0xbc, 0x72, 0x60, 0x49, 0x2f, 0x29, 0xb4, 0x85, 0xee, 0x1b, 0xb1, 0xf3, 0x78, 0x28, 0x55, 0x98, 0x67, 0xc9, + 0xab, 0x52, 0x2d, 0x5a, 0x9a, 0xb0, 0xe3, 0x89, 0x38, 0x01, 0xbc, 0xa0, 0x06, 0x0f, 0xd3, 0xcc, 0xee, 0x3f, 0xe8, + 0xad, 0x23, 0x3e, 0xfe, 0x24, 0xeb, 0x21, 0xf8, 0xa5, 0x7f, 0x1b, 0x3e, 0xc0, 0x1f, 0x65, 0x7d, 0x70, 0x64, 0xbb, + 0x3e, 0x29, 0x80, 0x07, 0xd5, 0x2f, 0xb3, 0xa2, 0xf4, 0xdb, 0x04, 0x5d, 0xed, 0xdd, 0x55, 0x69, 0x4b, 0x05, 0xdd, + 0xdd, 0xa9, 0x14, 0x34, 0x3c, 0x1b, 0x12, 0x19, 0x94, 0x45, 0xd7, 0xdf, 0x31, 0xc4, 0xfe, 0x79, 0x0b, 0xff, 0xd6, + 0x04, 0xff, 0x43, 0x68, 0xa0, 0x24, 0xff, 0x6b, 0x68, 0xbe, 0x2d, 0x94, 0x0c, 0xf4, 0xfb, 0x81, 0xc4, 0xb2, 0x10, + 0xc9, 0xf5, 0x6d, 0xb0, 0xe2, 0xc0, 0x4c, 0x24, 0x63, 0xd8, 0x9e, 0x11, 0x5b, 0x13, 0xbb, 0x52, 0x46, 0x8e, 0x9e, + 0x43, 0x5f, 0x47, 0x7f, 0xc6, 0x7c, 0x55, 0x9d, 0x57, 0x93, 0x12, 0x2b, 0xa6, 0xc0, 0x7d, 0xdd, 0x38, 0x94, 0xeb, + 0x89, 0x3c, 0x6f, 0xfd, 0x1d, 0x94, 0xf5, 0x0c, 0x2d, 0x13, 0xc2, 0x5d, 0x43, 0x44, 0x30, 0xfa, 0xd4, 0x2a, 0x4d, + 0xf2, 0x6a, 0x54, 0x36, 0xe7, 0x07, 0xb3, 0x06, 0x7f, 0x97, 0xb2, 0xba, 0xe5, 0x23, 0xaf, 0xef, 0x62, 0xca, 0xc5, + 0x28, 0xce, 0xe9, 0x56, 0xb8, 0x02, 0xbd, 0x16, 0x78, 0xad, 0xa8, 0x44, 0x52, 0x82, 0x15, 0x03, 0x1b, 0x8b, 0xec, + 0x40, 0x26, 0x06, 0x9a, 0xdf, 0x1a, 0x37, 0xaf, 0xed, 0x8e, 0x44, 0x4e, 0x20, 0xfe, 0x16, 0x83, 0x2d, 0xe8, 0x63, + 0x83, 0xb4, 0x5d, 0xbb, 0x4b, 0xc8, 0x06, 0x43, 0x5c, 0xab, 0x1f, 0xd7, 0x32, 0x05, 0x90, 0x6d, 0x12, 0x5a, 0x8f, + 0x4b, 0x24, 0x74, 0x25, 0x9d, 0x4e, 0x59, 0xc4, 0xfd, 0x28, 0xa5, 0xfc, 0x2d, 0xc7, 0x10, 0x53, 0x5e, 0x87, 0x6d, + 0xbb, 0x25, 0xc8, 0x46, 0xe3, 0xd7, 0xc7, 0xe4, 0xee, 0x86, 0x42, 0xfd, 0xe5, 0xab, 0x7a, 0x2e, 0xf6, 0xa4, 0xdb, + 0x7f, 0x77, 0xb0, 0x67, 0x89, 0x4d, 0xb9, 0xbb, 0x05, 0xaf, 0xbb, 0xe4, 0xc1, 0x8b, 0x54, 0x96, 0x50, 0xa4, 0xb2, + 0x58, 0x22, 0x01, 0x4e, 0xe4, 0x2e, 0x6f, 0x09, 0xb4, 0x6d, 0x8b, 0xa5, 0x43, 0x11, 0x7a, 0x9c, 0x82, 0x97, 0x13, + 0xe3, 0xf7, 0xe9, 0xb6, 0xb0, 0x6b, 0x0b, 0x17, 0xcc, 0x56, 0x59, 0x41, 0xca, 0xae, 0xe1, 0xa9, 0x0a, 0x54, 0x82, + 0x35, 0xc2, 0x54, 0x82, 0x90, 0x1c, 0x4a, 0xe7, 0x25, 0x2f, 0xb7, 0x2e, 0xe6, 0xa7, 0x53, 0x90, 0x93, 0x2a, 0xa9, + 0xe7, 0xa3, 0xec, 0xb0, 0x4b, 0x53, 0xf5, 0x4f, 0x4a, 0x19, 0x49, 0x55, 0xdf, 0x0e, 0x6f, 0xfc, 0xce, 0xaa, 0xc0, + 0x5e, 0xea, 0x05, 0xcc, 0x49, 0x99, 0x6c, 0x1b, 0x39, 0x29, 0x46, 0x5d, 0x09, 0xa8, 0x6f, 0xf7, 0x4f, 0x82, 0x99, + 0x1c, 0xef, 0x75, 0xb6, 0xf4, 0x9b, 0xad, 0x5a, 0x4e, 0x0e, 0x28, 0xbf, 0x5c, 0xdc, 0xeb, 0x90, 0x00, 0xc3, 0x0a, + 0x02, 0x4c, 0xd2, 0x04, 0xb0, 0xe8, 0xe8, 0xdb, 0xde, 0x69, 0xab, 0xb4, 0x5d, 0x28, 0xc3, 0x0d, 0x29, 0xba, 0x18, + 0x93, 0xd4, 0xc2, 0xbf, 0x93, 0x4e, 0x7f, 0x37, 0x92, 0xc6, 0x25, 0x0a, 0x8f, 0x02, 0xa4, 0x07, 0x74, 0x46, 0x0b, + 0xce, 0x8f, 0xb3, 0xad, 0x0b, 0x76, 0xda, 0x8a, 0x66, 0x71, 0x15, 0x6b, 0x45, 0x53, 0x43, 0x4f, 0x99, 0x55, 0x33, + 0xe1, 0x63, 0xd4, 0x40, 0x92, 0x04, 0x77, 0x29, 0x03, 0xb9, 0x64, 0xa1, 0x03, 0x0b, 0x01, 0x85, 0x49, 0xae, 0xab, + 0x80, 0xaf, 0xd4, 0xb8, 0xa5, 0xdd, 0xff, 0xcb, 0x3f, 0xff, 0x6f, 0x19, 0xc3, 0x05, 0xaa, 0x74, 0xd4, 0x58, 0x0d, + 0x42, 0x97, 0xbb, 0x98, 0x02, 0x55, 0x9d, 0xf2, 0xb2, 0xcb, 0xd6, 0x59, 0x1e, 0x8f, 0x5a, 0x93, 0x28, 0x19, 0x03, + 0x60, 0x6b, 0x09, 0x64, 0x26, 0x48, 0x48, 0xa8, 0xeb, 0x45, 0xc8, 0x82, 0xbf, 0x29, 0x11, 0x5b, 0x25, 0xc0, 0xd3, + 0x6e, 0x35, 0xd3, 0xb2, 0xab, 0x0d, 0x55, 0x4b, 0xcd, 0x56, 0x3f, 0x5c, 0xa6, 0x84, 0x5a, 0x2d, 0x2f, 0x1b, 0x5a, + 0xea, 0xc3, 0xa8, 0x7f, 0xff, 0x97, 0x7f, 0xf8, 0x1f, 0xea, 0x15, 0xcf, 0x98, 0xfe, 0xf2, 0x4f, 0x7f, 0x87, 0x29, + 0xd0, 0x96, 0x3e, 0x87, 0x22, 0x39, 0x61, 0x55, 0x87, 0x50, 0x42, 0x60, 0x58, 0x95, 0xd3, 0x57, 0xcf, 0xdf, 0xde, + 0xa7, 0x09, 0x69, 0xb3, 0x49, 0xe8, 0x68, 0xd3, 0x96, 0x15, 0x8f, 0xd4, 0x48, 0x4e, 0xbc, 0x08, 0x95, 0x48, 0xef, + 0x3b, 0x25, 0x47, 0xf9, 0x7a, 0x35, 0x16, 0x2a, 0x42, 0x88, 0x25, 0x65, 0x55, 0x6e, 0x61, 0xe8, 0x7e, 0x81, 0xaf, + 0x41, 0xd7, 0x28, 0xa6, 0xc5, 0xab, 0xf5, 0xe9, 0xfd, 0x34, 0x07, 0xf8, 0xc7, 0x48, 0x71, 0x11, 0x87, 0xa4, 0x63, + 0xe9, 0x16, 0xda, 0x7c, 0xc9, 0x55, 0x49, 0xa3, 0x08, 0x47, 0xf1, 0xe1, 0x93, 0xbf, 0x29, 0xff, 0x38, 0x45, 0xcb, + 0xca, 0x72, 0xa6, 0xd1, 0xa5, 0x74, 0x1f, 0x1f, 0xb5, 0xdb, 0xb3, 0x4b, 0x77, 0x51, 0xcd, 0xe0, 0xad, 0x9b, 0x8c, + 0x62, 0x97, 0xe6, 0x80, 0x74, 0x9e, 0xad, 0xc3, 0xa4, 0xe0, 0x31, 0xb5, 0x31, 0xaa, 0x56, 0x96, 0x7f, 0x58, 0x50, + 0xa4, 0x2e, 0xfe, 0x05, 0xcf, 0x9d, 0x65, 0x50, 0x13, 0x4a, 0x0c, 0x2c, 0x16, 0x46, 0xaf, 0xae, 0xe8, 0x35, 0xe9, + 0x2c, 0xa7, 0x0d, 0x99, 0xe7, 0xe6, 0xe6, 0x89, 0xf7, 0x43, 0x3c, 0xc3, 0x9e, 0x74, 0xbc, 0x49, 0x77, 0xa1, 0x87, + 0xe7, 0x3c, 0x9b, 0x9a, 0x07, 0xe5, 0x2c, 0x62, 0x43, 0x36, 0x56, 0xc1, 0x60, 0x59, 0x2f, 0x0e, 0xc1, 0xcb, 0xc9, + 0xf6, 0x8a, 0xb9, 0x24, 0x48, 0x74, 0x40, 0x0e, 0xf0, 0x7c, 0x86, 0x1b, 0x10, 0xe8, 0x9f, 0x45, 0x3c, 0x20, 0x7e, + 0xed, 0x99, 0xc7, 0xed, 0x11, 0x4a, 0x99, 0x6c, 0x61, 0xc0, 0xd3, 0x13, 0x4d, 0x31, 0x2c, 0x5b, 0x4f, 0xdb, 0x2a, + 0x7d, 0xea, 0x6e, 0x0e, 0x25, 0xa2, 0x3a, 0xdf, 0xca, 0x53, 0xec, 0xa7, 0xb5, 0x70, 0x88, 0x54, 0x31, 0x5d, 0xd7, + 0x5b, 0x59, 0x2f, 0x34, 0xb5, 0xa8, 0xfd, 0x16, 0x0c, 0x30, 0x02, 0xd3, 0x6e, 0xb6, 0xa2, 0x42, 0x6c, 0xf5, 0x34, + 0xfc, 0x56, 0xbb, 0x3e, 0xd1, 0x6c, 0x46, 0x0d, 0x5d, 0x60, 0x62, 0x32, 0x58, 0x51, 0x76, 0x50, 0x86, 0x86, 0x48, + 0x88, 0x90, 0x6d, 0xe4, 0x46, 0x10, 0x4f, 0x32, 0x55, 0x02, 0x7f, 0x72, 0xa2, 0xff, 0xff, 0x00, 0x69, 0x5b, 0x88, + 0x58, 0x18, 0x7f, 0x00, 0x00}; } // namespace web_server } // namespace esphome From 70de2f527824a5cd7d22b6627f7e15884a32723b Mon Sep 17 00:00:00 2001 From: esphomebot Date: Wed, 28 Jun 2023 10:19:36 +1200 Subject: [PATCH 008/270] Synchronise Device Classes from Home Assistant (#5018) --- esphome/components/button/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 55f2fe794a..a999c6d91e 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_MQTT_ID, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_IDENTIFY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) @@ -24,6 +25,7 @@ IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, + DEVICE_CLASS_IDENTIFY, DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ] diff --git a/esphome/const.py b/esphome/const.py index 48e62b9b86..3036d13801 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -968,6 +968,7 @@ DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_GATE = "gate" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_HUMIDITY = "humidity" +DEVICE_CLASS_IDENTIFY = "identify" DEVICE_CLASS_ILLUMINANCE = "illuminance" DEVICE_CLASS_IRRADIANCE = "irradiance" DEVICE_CLASS_LIGHT = "light" From 832ba38f1b0c58b7117a3a7c6db9c011a310592a Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Tue, 27 Jun 2023 20:13:14 -0300 Subject: [PATCH 009/270] Fixes compressed downloads (#5014) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/dashboard/dashboard.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 22bbe0aae9..dd800f534c 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -546,22 +546,11 @@ class DownloadBinaryRequestHandler(BaseHandler): return with open(path, "rb") as f: - while True: - # For a 528KB image used as benchmark: - # - using 256KB blocks resulted in the smallest file size. - # - blocks larger than 256KB didn't improve the size of compressed file. - # - blocks smaller than 256KB hindered compression, making the output file larger. + data = f.read() + if compressed: + data = gzip.compress(data, 9) + self.write(data) - # Read file in blocks of 256KB. - data = f.read(256 * 1024) - - if not data: - break - - if compressed: - data = gzip.compress(data, 9) - - self.write(data) self.finish() From abc8e903c11d479f1832296d1eac7cd6c1592ee3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 28 Jun 2023 11:35:35 +1200 Subject: [PATCH 010/270] Add CONFIG_BT_BLE_42_FEATURES_SUPPORTED for ble (#5008) --- esphome/components/esp32_ble/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index f508cecb87..b4cb595da0 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -55,3 +55,4 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) From d34c074b92796c41ec6f10c9a7bccda3fdc95c5f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:35:16 +1200 Subject: [PATCH 011/270] Bump version to 2023.6.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3036d13801..0d5b211c18 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.2" +__version__ = "2023.6.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From c3ef12d5807bc58400a2a837ceda138b735013f6 Mon Sep 17 00:00:00 2001 From: Ryan DeShone Date: Wed, 28 Jun 2023 19:42:39 -0400 Subject: [PATCH 012/270] [SCD30] Disable negative temperature offset (#4850) --- esphome/components/scd30/scd30.cpp | 19 ++++++++++++------- esphome/components/scd30/sensor.py | 5 ++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 01abca0a1f..3eeca23800 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -42,13 +42,18 @@ void SCD30Component::setup() { ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8), uint16_t(raw_firmware_version[0] & 0xFF)); - if (this->temperature_offset_ != 0) { - if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t) (temperature_offset_ * 100.0))) { - ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } + uint16_t temp_offset; + if (this->temperature_offset_ > 0) { + temp_offset = (this->temperature_offset_ * 100); + } else { + temp_offset = 0; + } + + if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, temp_offset)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; } #ifdef USE_ESP32 // According ESP32 clock stretching is typically 30ms and up to 150ms "due to diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 1ddf0f1e85..f72b43fd37 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -68,7 +68,10 @@ CONFIG_SCHEMA = ( cv.int_range(min=0, max=0xFFFF, max_included=False), ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure, - cv.Optional(CONF_TEMPERATURE_OFFSET): cv.temperature, + cv.Optional(CONF_TEMPERATURE_OFFSET): cv.All( + cv.temperature, + cv.float_range(min=0, max=655.35), + ), cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All( cv.positive_time_period_seconds, cv.Range( From e823067a6b1bd2eff4d579e5612da2c067353219 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Tue, 4 Jul 2023 04:18:51 +0400 Subject: [PATCH 013/270] fix template binary_sensor publish_initial_state option (#5033) --- .../binary_sensor/template_binary_sensor.cpp | 16 +++++++++++++--- .../binary_sensor/template_binary_sensor.h | 3 ++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 66ff4be4c4..fce11f63d6 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,11 +6,21 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) +void TemplateBinarySensor::setup() { + if (!this->publish_initial_state_) return; - auto s = (*this->f_)(); + if (this->f_ != nullptr) { + this->publish_initial_state(*this->f_()); + } else { + this->publish_initial_state(false); + } +} +void TemplateBinarySensor::loop() { + if (this->f_ == nullptr) + return; + + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index a28929b122..5e5624d82e 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -10,13 +10,14 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso public: void set_template(std::function()> &&f) { this->f_ = f; } + void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional()>> f_{}; + std::function()> f_{nullptr}; }; } // namespace template_ From 36782f13bf0bec2d7c0c989b3a8100903fa224f3 Mon Sep 17 00:00:00 2001 From: Graham Brown Date: Tue, 4 Jul 2023 02:28:19 +0200 Subject: [PATCH 014/270] Add alarm to reserved ids (#5042) --- esphome/config_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 0a6b2dfbb0..cf0b1d3aca 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -108,6 +108,7 @@ ROOT_CONFIG_PATH = object() RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword + "alarm", "alignas", "alignof", "and", From 8df455f55b8251005e0d150b2ffbaca0fc8079eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Jul 2023 19:52:42 -0500 Subject: [PATCH 015/270] Advertise noise is enabled (#5034) --- esphome/components/mdns/mdns_component.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index cdb9aa8e74..581758cf2d 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -57,6 +57,10 @@ void MDNSComponent::compile_records_() { service.txt_records.push_back({"network", "ethernet"}); #endif +#ifdef USE_API_NOISE + service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); +#endif + #ifdef ESPHOME_PROJECT_NAME service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); From 8dd509ba53084f9177550d5ff577bc3d8e6825b0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 4 Jul 2023 13:45:06 +1200 Subject: [PATCH 016/270] Update webserver to ea86d81 (#5023) --- esphome/components/web_server/server_index.h | 1185 +++++++++--------- 1 file changed, 594 insertions(+), 591 deletions(-) diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h index 2dbb839c5e..180dffab67 100644 --- a/esphome/components/web_server/server_index.h +++ b/esphome/components/web_server/server_index.h @@ -6,597 +6,600 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xbd, 0x7d, 0xdb, 0x76, 0xe3, 0x46, 0x92, 0xe0, 0xf3, - 0x9e, 0xb3, 0x7f, 0xb0, 0x2f, 0x28, 0x58, 0x53, 0x05, 0xb4, 0x40, 0x88, 0xa4, 0x4a, 0x55, 0x65, 0x50, 0x20, 0x5b, - 0x75, 0xb1, 0xab, 0xec, 0xba, 0xb9, 0xa4, 0xb2, 0xdb, 0x96, 0xd5, 0x12, 0x44, 0x26, 0x45, 0xb8, 0x40, 0x80, 0x06, - 0x92, 0xba, 0x98, 0xc2, 0x9c, 0x79, 0x9a, 0xa7, 0x39, 0x67, 0x6f, 0xf3, 0x30, 0x0f, 0x3b, 0x67, 0xe6, 0x61, 0x3f, - 0x62, 0x9f, 0xe7, 0x53, 0xfa, 0x07, 0x76, 0x3e, 0x61, 0x23, 0x22, 0x2f, 0x48, 0x80, 0xa4, 0x24, 0x7b, 0xdc, 0x7b, - 0xdc, 0xd5, 0x02, 0xf2, 0x1a, 0x11, 0x19, 0x19, 0xb7, 0x8c, 0x04, 0x77, 0xef, 0x8d, 0xb2, 0x21, 0xbf, 0x9a, 0x31, - 0x6b, 0xc2, 0xa7, 0x49, 0x7f, 0x57, 0xfe, 0x3f, 0x8b, 0x46, 0xfd, 0xdd, 0x24, 0x4e, 0x3f, 0x59, 0x39, 0x4b, 0xc2, - 0x78, 0x98, 0xa5, 0xd6, 0x24, 0x67, 0xe3, 0x70, 0x14, 0xf1, 0x28, 0x88, 0xa7, 0xd1, 0x19, 0xb3, 0xb6, 0xfa, 0xbb, - 0x53, 0xc6, 0x23, 0x6b, 0x38, 0x89, 0xf2, 0x82, 0xf1, 0xf0, 0xe3, 0xc1, 0x17, 0xad, 0x27, 0xfd, 0xdd, 0x62, 0x98, - 0xc7, 0x33, 0x6e, 0xe1, 0x90, 0xe1, 0x34, 0x1b, 0xcd, 0x13, 0xd6, 0x3f, 0x8f, 0x72, 0xeb, 0x05, 0x0b, 0xdf, 0x9d, - 0xfe, 0xc4, 0x86, 0xdc, 0x1f, 0xb1, 0x71, 0x9c, 0xb2, 0xf7, 0x79, 0x36, 0x63, 0x39, 0xbf, 0xf2, 0xf6, 0x57, 0x57, - 0xc4, 0xac, 0xf0, 0x9e, 0xe9, 0xaa, 0x33, 0xc6, 0xdf, 0x5d, 0xa4, 0xaa, 0xcf, 0x73, 0x26, 0x26, 0xc9, 0xf2, 0xc2, - 0x8b, 0xd7, 0xb4, 0xd9, 0xbf, 0x9a, 0x9e, 0x66, 0x49, 0xe1, 0x7d, 0xd2, 0xf5, 0xb3, 0x3c, 0xe3, 0x19, 0x82, 0xe5, - 0x4f, 0xa2, 0xc2, 0x68, 0xe9, 0xbd, 0x5b, 0xd1, 0x64, 0x26, 0x2b, 0x5f, 0x15, 0x2f, 0xd2, 0xf9, 0x94, 0xe5, 0xd1, - 0x69, 0xc2, 0xbc, 0x9c, 0x85, 0x0e, 0xf7, 0x98, 0x17, 0xbb, 0x61, 0x9f, 0x59, 0x71, 0x6a, 0xf1, 0xc1, 0x0b, 0x46, - 0x25, 0x0b, 0xa6, 0x5b, 0x05, 0xf7, 0xda, 0x1e, 0x90, 0x6b, 0x1c, 0x9f, 0xcd, 0xf5, 0xfb, 0x45, 0x1e, 0x73, 0xf5, - 0x7c, 0x1e, 0x25, 0x73, 0x16, 0xc4, 0xa5, 0x1b, 0xf0, 0x43, 0x76, 0x14, 0xc6, 0xde, 0x27, 0x1a, 0x14, 0x86, 0x5c, - 0x8c, 0xb3, 0xdc, 0x41, 0x5a, 0xc5, 0x38, 0x36, 0xbb, 0xbe, 0x76, 0x58, 0xb8, 0x28, 0x5d, 0xf7, 0x13, 0xf3, 0x87, - 0x51, 0x92, 0x38, 0x38, 0xf1, 0xfd, 0xfb, 0x39, 0xce, 0x18, 0x7b, 0xec, 0x30, 0x3e, 0x72, 0x7b, 0xf1, 0xd8, 0x89, - 0x99, 0x5b, 0xf5, 0xcb, 0xc6, 0x56, 0xcc, 0x1c, 0xe6, 0xba, 0xef, 0xd6, 0xf7, 0xc9, 0x19, 0x9f, 0xe7, 0x00, 0x7b, - 0xe9, 0xbd, 0x53, 0x33, 0xef, 0x63, 0xfd, 0x33, 0xea, 0xd8, 0x03, 0xd8, 0x0b, 0x6e, 0x7d, 0x11, 0x5e, 0xc4, 0xe9, - 0x28, 0xbb, 0xf0, 0xf7, 0x27, 0x11, 0xfc, 0xf9, 0x90, 0x65, 0xfc, 0xfe, 0x7d, 0xe7, 0x3c, 0x8b, 0x47, 0x56, 0x3b, - 0x0c, 0xcd, 0xca, 0xab, 0x67, 0xfb, 0xfb, 0xd7, 0xd7, 0x8d, 0x02, 0x3f, 0x8d, 0x78, 0x7c, 0xce, 0x44, 0x67, 0x00, - 0xc0, 0x86, 0xbf, 0x33, 0xce, 0x46, 0xfb, 0xfc, 0x2a, 0x81, 0x52, 0xc6, 0x78, 0x61, 0x03, 0x8e, 0xcf, 0xb3, 0x21, - 0x90, 0x2d, 0x35, 0x08, 0x0f, 0x4d, 0x73, 0x36, 0x4b, 0xa2, 0x21, 0xc3, 0x7a, 0x18, 0xa9, 0xea, 0x51, 0x35, 0xf2, - 0xbe, 0x0b, 0xc5, 0xf2, 0x3a, 0xae, 0x97, 0xb1, 0x30, 0x65, 0x17, 0xd6, 0x9b, 0x68, 0xd6, 0x1b, 0x26, 0x51, 0x51, - 0x58, 0x29, 0x5b, 0x10, 0x0a, 0xf9, 0x7c, 0x08, 0x0c, 0x42, 0x08, 0x2e, 0x80, 0x4c, 0x7c, 0x12, 0x17, 0xfe, 0xf1, - 0xc6, 0xb0, 0x28, 0x3e, 0xb0, 0x62, 0x9e, 0xf0, 0x8d, 0x10, 0xd6, 0x82, 0xdd, 0x0b, 0xc3, 0xef, 0x5c, 0x3e, 0xc9, - 0xb3, 0x0b, 0xeb, 0x45, 0x9e, 0x43, 0x73, 0x1b, 0xa6, 0x14, 0x0d, 0xac, 0x18, 0xc6, 0xca, 0xb8, 0xa5, 0x07, 0xc3, - 0x05, 0xf4, 0xad, 0x8f, 0x05, 0xb3, 0x4e, 0xe6, 0x69, 0x11, 0x8d, 0x19, 0x34, 0x3d, 0xb1, 0xb2, 0xdc, 0x3a, 0x81, - 0x41, 0x4f, 0x60, 0xc9, 0x0a, 0x0e, 0xbb, 0xc6, 0xb7, 0xdd, 0x1e, 0xcd, 0x05, 0x85, 0x07, 0xec, 0x92, 0x87, 0xbc, - 0x04, 0xc6, 0xb4, 0x0a, 0x8d, 0x86, 0xe3, 0x2e, 0x12, 0x28, 0xe0, 0x61, 0xc6, 0x90, 0x65, 0x1d, 0xb3, 0xb1, 0x5e, - 0x9c, 0x2f, 0xee, 0xdf, 0xd7, 0xb4, 0x46, 0xc2, 0x43, 0xdb, 0xa2, 0xd1, 0xd6, 0xe3, 0x84, 0x78, 0x8d, 0x44, 0xae, - 0xc7, 0x7d, 0x49, 0xbe, 0xfd, 0xab, 0x74, 0x58, 0x1f, 0x1b, 0x2a, 0x4b, 0x9e, 0xed, 0xf3, 0x3c, 0x4e, 0xcf, 0x00, - 0x08, 0xc5, 0x06, 0x46, 0x93, 0xb2, 0x14, 0x8b, 0xff, 0x9e, 0x85, 0x3c, 0xec, 0xe3, 0xe8, 0x29, 0x73, 0xec, 0x82, - 0x7a, 0xd8, 0x00, 0x08, 0x90, 0x1e, 0x18, 0x8c, 0x0f, 0x78, 0xc0, 0x37, 0x6d, 0xdb, 0xfb, 0xce, 0xf5, 0xae, 0x90, - 0x83, 0x7c, 0xdf, 0x27, 0xf6, 0x15, 0x9d, 0xe3, 0xb0, 0x83, 0x40, 0xfb, 0x09, 0x4b, 0xcf, 0xf8, 0x64, 0xc0, 0x0f, - 0xdb, 0x47, 0x01, 0x03, 0xa8, 0x46, 0xf3, 0x21, 0x73, 0x90, 0x1f, 0xbd, 0x1c, 0xb7, 0xcf, 0xa6, 0x03, 0x53, 0xe0, - 0xc2, 0xdc, 0x23, 0x1c, 0x6b, 0x4b, 0xe3, 0x2a, 0xd8, 0x14, 0x60, 0xc8, 0xe7, 0x36, 0xec, 0xb0, 0x53, 0x96, 0x1b, - 0x70, 0xe8, 0x66, 0xbd, 0xda, 0x0a, 0xce, 0x61, 0x85, 0xa0, 0x9f, 0x35, 0x9e, 0xa7, 0x43, 0x1e, 0x83, 0xe0, 0xb2, - 0x37, 0x01, 0x5c, 0xb1, 0x72, 0x7a, 0xe1, 0x6c, 0xb7, 0x74, 0x9d, 0xd8, 0xdd, 0xe4, 0x87, 0xf9, 0x66, 0xe7, 0xc8, - 0x43, 0x28, 0x35, 0xf1, 0x25, 0xe2, 0x31, 0x20, 0x58, 0x7a, 0x1f, 0x99, 0xde, 0x9e, 0x5f, 0x0c, 0xb8, 0xbf, 0xcc, - 0xc7, 0x21, 0xf3, 0xa7, 0xd1, 0x0c, 0xb1, 0xe1, 0xc4, 0x03, 0x51, 0x3a, 0x44, 0xe8, 0x6a, 0xeb, 0x82, 0x14, 0xf3, - 0x2b, 0x16, 0x70, 0x81, 0x20, 0xb0, 0x67, 0x5f, 0x44, 0xc3, 0x09, 0x6c, 0xf1, 0x8a, 0x70, 0x23, 0xb5, 0x1d, 0x86, - 0x39, 0x8b, 0x38, 0x7b, 0x91, 0x30, 0x7c, 0xc3, 0x15, 0x80, 0x9e, 0xb6, 0xeb, 0xe5, 0x6a, 0xdf, 0x25, 0x31, 0x7f, - 0x9b, 0xc1, 0x3c, 0x3d, 0xc1, 0x24, 0xc0, 0xc5, 0xf9, 0xfd, 0xfb, 0x31, 0xb2, 0xc8, 0x1e, 0x87, 0xd5, 0x3a, 0x9d, - 0x73, 0x58, 0xb7, 0x14, 0x5b, 0xd8, 0x40, 0x6d, 0x2f, 0xf6, 0x39, 0x10, 0xf1, 0x59, 0x96, 0x72, 0x18, 0x0e, 0xe0, - 0xd5, 0x1c, 0xe4, 0x47, 0xb3, 0x19, 0x4b, 0x47, 0xcf, 0x26, 0x71, 0x32, 0x02, 0x6a, 0x94, 0x80, 0x6f, 0xc2, 0x42, - 0xc0, 0x13, 0x90, 0x09, 0x6e, 0xc6, 0x88, 0x96, 0x0f, 0x19, 0x99, 0x85, 0xb6, 0xdd, 0x43, 0x09, 0x24, 0xb1, 0x40, - 0x19, 0x44, 0x0b, 0xf7, 0x01, 0x44, 0x7f, 0xe1, 0xb2, 0xcd, 0x30, 0xd6, 0xcb, 0x28, 0x09, 0xfc, 0x1e, 0x25, 0x0d, - 0xd0, 0x1f, 0x08, 0xc1, 0x7b, 0x28, 0xb8, 0xbe, 0x92, 0x52, 0x27, 0x62, 0x0a, 0x43, 0x20, 0xc0, 0x10, 0x25, 0x88, - 0xa4, 0xc1, 0xfb, 0x2c, 0xb9, 0x1a, 0xc7, 0x49, 0xb2, 0x3f, 0x9f, 0xcd, 0xb2, 0x9c, 0x7b, 0x5f, 0x87, 0x0b, 0x9e, - 0x55, 0xb8, 0xd2, 0x26, 0x2f, 0x2e, 0x62, 0x8e, 0x04, 0x75, 0x17, 0xc3, 0x08, 0x96, 0xfa, 0x69, 0x96, 0x25, 0x2c, - 0x4a, 0x01, 0x0d, 0x3e, 0xb0, 0xed, 0x20, 0x9d, 0x27, 0x49, 0xef, 0x14, 0x86, 0xfd, 0xd4, 0xa3, 0x6a, 0x21, 0xf1, - 0x03, 0x7a, 0xde, 0xcb, 0xf3, 0xe8, 0x0a, 0x1a, 0x62, 0x1b, 0x60, 0x2f, 0x58, 0xad, 0xaf, 0xf6, 0xdf, 0xbd, 0xf5, - 0x05, 0xe3, 0xc7, 0xe3, 0x2b, 0x00, 0xb4, 0xac, 0xa4, 0xe6, 0x38, 0xcf, 0xa6, 0x8d, 0xa9, 0x91, 0x0e, 0x71, 0xc8, - 0x7b, 0x6b, 0x40, 0x88, 0x69, 0x64, 0x58, 0x25, 0x6e, 0x42, 0xf0, 0x96, 0xf8, 0x59, 0x56, 0xe2, 0x1e, 0x18, 0xe0, - 0x43, 0x20, 0x8a, 0x61, 0xca, 0x5b, 0xa0, 0xcd, 0xaf, 0x16, 0x71, 0x48, 0x70, 0xce, 0x50, 0xff, 0x22, 0x8c, 0xc3, - 0x08, 0x66, 0x5f, 0x88, 0x01, 0x4b, 0x05, 0x71, 0x5c, 0x96, 0xde, 0x44, 0x33, 0x31, 0x4a, 0x3c, 0x14, 0x28, 0x2c, - 0x0c, 0x41, 0xc1, 0x70, 0x78, 0x71, 0xbd, 0x6f, 0xc2, 0x45, 0xa4, 0xf0, 0x41, 0x0d, 0x85, 0xfb, 0x2b, 0x10, 0x72, - 0x02, 0x35, 0xd9, 0x39, 0xe8, 0x41, 0x80, 0xf3, 0x6b, 0x50, 0x7f, 0xe3, 0x04, 0xa1, 0xb8, 0xd7, 0xf1, 0x40, 0x83, - 0x3e, 0x9b, 0x44, 0xe9, 0x19, 0x1b, 0x05, 0x13, 0x56, 0x4a, 0xc9, 0xbb, 0x67, 0xc1, 0x1a, 0x03, 0x3b, 0x15, 0xd6, - 0xcb, 0x83, 0x37, 0xaf, 0xe5, 0xca, 0xd5, 0x84, 0x31, 0x2c, 0xd2, 0x1c, 0xd4, 0x2a, 0x88, 0x6d, 0x29, 0x8e, 0x5f, - 0x70, 0x25, 0xbd, 0x45, 0x49, 0x5c, 0x7c, 0x9c, 0x81, 0x89, 0xc1, 0xde, 0xc3, 0x30, 0x30, 0x7d, 0x08, 0x53, 0x51, - 0x39, 0xcc, 0x27, 0x2a, 0x46, 0xba, 0x08, 0x3a, 0x0b, 0x4c, 0xc5, 0x6b, 0xe6, 0xb8, 0x25, 0xb0, 0x2a, 0x8f, 0x87, - 0x56, 0x34, 0x1a, 0xbd, 0x4a, 0x63, 0x1e, 0x47, 0x49, 0xfc, 0x0b, 0x51, 0x72, 0x81, 0x3c, 0xc6, 0x7a, 0x72, 0x11, - 0x00, 0x77, 0xea, 0x91, 0xb8, 0x4a, 0xc8, 0xde, 0x23, 0x62, 0x08, 0x69, 0x99, 0x84, 0x87, 0x47, 0x12, 0xbc, 0xc4, - 0x9f, 0xcd, 0x8b, 0x09, 0x12, 0x56, 0x0e, 0x8c, 0x82, 0x3c, 0x3b, 0x2d, 0x58, 0x7e, 0xce, 0x46, 0x9a, 0x03, 0x0a, - 0xc0, 0x8a, 0x9a, 0x83, 0xf1, 0x42, 0x33, 0x3a, 0x4a, 0x87, 0x72, 0x18, 0xaa, 0x67, 0x8a, 0x59, 0x26, 0x99, 0x59, - 0x5b, 0x38, 0x5a, 0x0a, 0x38, 0xc2, 0xa8, 0x90, 0x92, 0x20, 0x0f, 0x15, 0x86, 0x13, 0x90, 0x42, 0xcc, 0xad, 0x6d, - 0x73, 0xa5, 0xc9, 0x5e, 0xcc, 0x49, 0x25, 0xe4, 0xd0, 0x11, 0x36, 0x32, 0x41, 0x9a, 0xbb, 0xb0, 0xab, 0x40, 0xca, - 0x4b, 0x70, 0x85, 0x14, 0x51, 0x66, 0x0e, 0x32, 0x40, 0xf8, 0x0d, 0xe9, 0x42, 0x50, 0x26, 0xd0, 0x82, 0x21, 0x1b, - 0xf8, 0x7a, 0xe5, 0x81, 0xb0, 0x12, 0xef, 0x0a, 0x11, 0x6f, 0x0d, 0xd8, 0xa4, 0x8b, 0x00, 0x30, 0xef, 0x1e, 0xf3, - 0xd3, 0x6c, 0x6f, 0x38, 0x64, 0x45, 0x91, 0x01, 0x6c, 0xf7, 0xa8, 0xfd, 0x3a, 0x43, 0x0b, 0x28, 0xe9, 0x6a, 0x59, - 0x67, 0x17, 0xa4, 0xc1, 0x4d, 0xb5, 0xa2, 0x74, 0x7a, 0x60, 0x1f, 0x1f, 0x83, 0xcc, 0xf6, 0x24, 0x19, 0x80, 0xea, - 0xcb, 0x86, 0x9f, 0xb0, 0x67, 0xea, 0x94, 0x59, 0x69, 0x5f, 0x3a, 0x75, 0x90, 0x3c, 0x18, 0xd6, 0x2d, 0x8d, 0x05, - 0x5d, 0x39, 0x34, 0xae, 0x86, 0x54, 0x90, 0x8b, 0x33, 0x52, 0xd9, 0xc6, 0x32, 0x82, 0xd5, 0x56, 0x7a, 0x44, 0x7a, - 0x85, 0x4d, 0x41, 0x80, 0x1e, 0xf2, 0xa3, 0x9e, 0xac, 0x0f, 0x73, 0x41, 0xb9, 0x9c, 0xfd, 0x3c, 0x67, 0x05, 0x17, - 0xac, 0x0b, 0xe3, 0x82, 0xb9, 0x0a, 0x22, 0xb6, 0x69, 0x1d, 0xd6, 0x6c, 0xc7, 0x55, 0xb0, 0xbd, 0x9b, 0xa1, 0x1e, - 0x2b, 0x90, 0x93, 0x6f, 0x66, 0x27, 0xb2, 0x27, 0xdc, 0xeb, 0xeb, 0x6f, 0xd4, 0x20, 0xd5, 0x52, 0x6a, 0x1b, 0xa8, - 0xb1, 0x26, 0xb6, 0x6a, 0x32, 0xb2, 0x5d, 0xa9, 0x50, 0xef, 0x75, 0x7a, 0x35, 0x3e, 0x80, 0x3d, 0xd7, 0xd6, 0x2c, - 0x5d, 0x19, 0xdb, 0xef, 0x15, 0x4d, 0xdf, 0x89, 0x91, 0xc9, 0x1a, 0xe5, 0xb7, 0x73, 0x8f, 0xda, 0xf1, 0xd0, 0x76, - 0xa9, 0xae, 0x12, 0x0c, 0xf3, 0xba, 0x60, 0x68, 0x42, 0x3d, 0xd3, 0x5d, 0x6c, 0xcd, 0x54, 0x3c, 0x54, 0x6b, 0xad, - 0x1c, 0x08, 0x16, 0x1e, 0x82, 0x71, 0xb2, 0xd2, 0x3f, 0x78, 0x1b, 0x4d, 0x19, 0x52, 0xd4, 0x5b, 0xd7, 0x40, 0x3a, - 0x10, 0xd0, 0xe4, 0xa8, 0xa9, 0xde, 0x98, 0x2b, 0xac, 0xa6, 0xfa, 0xfe, 0x8a, 0xc1, 0x8a, 0x00, 0xfb, 0xba, 0x5c, - 0xb1, 0x44, 0xa4, 0x37, 0x05, 0x97, 0x68, 0xfa, 0x88, 0x32, 0xb1, 0x26, 0xa4, 0xe0, 0x01, 0x79, 0x58, 0xfe, 0xc6, - 0xc2, 0xa9, 0x56, 0x0a, 0x47, 0x86, 0x32, 0x05, 0xe8, 0x4c, 0x4a, 0x00, 0xc4, 0x25, 0xfd, 0xad, 0x6d, 0x2c, 0x24, - 0xdb, 0x3e, 0xf2, 0x81, 0x3f, 0x4e, 0x22, 0xee, 0x74, 0xb6, 0xda, 0x2e, 0xf0, 0x21, 0x08, 0x71, 0xd0, 0x11, 0x60, - 0xde, 0x57, 0xa8, 0x70, 0xf2, 0x16, 0x5c, 0xe6, 0x83, 0x51, 0x34, 0x89, 0xc7, 0xdc, 0x49, 0x50, 0x89, 0xb8, 0x25, - 0x4b, 0x40, 0xc9, 0xe8, 0x7d, 0x05, 0xca, 0x82, 0x09, 0xe9, 0x22, 0xaa, 0x95, 0x40, 0x63, 0x0a, 0x52, 0x92, 0x52, - 0xa4, 0x05, 0x15, 0x04, 0x86, 0x50, 0xe9, 0x29, 0x8e, 0x02, 0xfd, 0x16, 0x0f, 0xc4, 0xa0, 0xc1, 0x92, 0x45, 0x19, - 0x0f, 0xe2, 0xe5, 0x42, 0x50, 0xc3, 0x3e, 0xcf, 0x5e, 0x67, 0x17, 0x2c, 0x7f, 0x16, 0x21, 0xec, 0x81, 0xe8, 0x5e, - 0x82, 0xa4, 0x27, 0x81, 0xce, 0x7b, 0x8a, 0x57, 0xce, 0x09, 0x69, 0x58, 0x88, 0x69, 0x8c, 0x8a, 0x10, 0xec, 0x16, - 0xa2, 0x7d, 0x8a, 0x5b, 0x8a, 0xf6, 0x1e, 0xaa, 0x12, 0xae, 0x79, 0x6b, 0xef, 0x75, 0x9d, 0xb7, 0x60, 0x84, 0x99, - 0xe2, 0xd6, 0xfa, 0x8e, 0x75, 0x3d, 0xa9, 0x9b, 0x1d, 0xc9, 0x5b, 0x86, 0x32, 0x03, 0xfd, 0x71, 0x7d, 0x5d, 0x19, - 0xe9, 0xa0, 0x4c, 0xb5, 0x34, 0x47, 0x08, 0xc4, 0x96, 0x70, 0x4b, 0x50, 0x46, 0x68, 0x78, 0xe5, 0x59, 0x92, 0x18, - 0xba, 0xc8, 0x8b, 0x7b, 0x4e, 0x43, 0x1d, 0x01, 0x14, 0xd3, 0x9a, 0x46, 0x1a, 0xb0, 0x40, 0x57, 0xa0, 0x52, 0x52, - 0xda, 0xc8, 0xab, 0xd6, 0x46, 0x40, 0x9c, 0x8e, 0x58, 0x2e, 0x1c, 0x34, 0xa9, 0x43, 0x61, 0xc2, 0x14, 0x18, 0x9a, - 0x8d, 0x40, 0xc2, 0x2b, 0x04, 0xc0, 0x3c, 0xf1, 0x27, 0x59, 0xc1, 0x75, 0x9d, 0x09, 0x7d, 0x7c, 0x7d, 0x1d, 0x0b, - 0x7f, 0x11, 0x19, 0x20, 0x67, 0xd3, 0xec, 0x9c, 0xad, 0x80, 0xba, 0xa7, 0x06, 0x33, 0x41, 0x36, 0x86, 0x01, 0x25, - 0x0a, 0xaa, 0x65, 0x96, 0xc4, 0x60, 0xe9, 0xeb, 0x06, 0x3e, 0x18, 0x74, 0xec, 0x12, 0x65, 0x84, 0xdb, 0xef, 0xf7, - 0xdb, 0x5e, 0xc7, 0x2d, 0x05, 0xc1, 0x17, 0x4b, 0x14, 0xbd, 0x41, 0x3f, 0x4a, 0x13, 0x7c, 0x95, 0x2c, 0x60, 0xae, - 0xa1, 0x14, 0x39, 0xe9, 0x26, 0xe6, 0x49, 0x41, 0xec, 0x7a, 0x23, 0x18, 0x94, 0x33, 0x25, 0xb8, 0xd1, 0xc4, 0x15, - 0xdb, 0xf6, 0x83, 0x26, 0x9b, 0x66, 0x27, 0xb5, 0xc3, 0xd4, 0xc2, 0xc8, 0x35, 0x2f, 0xb4, 0x07, 0x6c, 0x2e, 0x0f, - 0xd9, 0xf4, 0x58, 0x0d, 0xbc, 0x0e, 0x10, 0x0a, 0x4f, 0xd7, 0x59, 0x42, 0xa9, 0xea, 0x2c, 0x85, 0xb8, 0xde, 0x40, - 0x1f, 0x99, 0x04, 0x73, 0x15, 0x09, 0xf6, 0xa5, 0x40, 0x60, 0xe8, 0x91, 0x89, 0xf5, 0x7a, 0x06, 0xcb, 0x73, 0x1a, - 0x0d, 0x3f, 0x69, 0x70, 0x2b, 0xde, 0x6b, 0xb2, 0x81, 0xd3, 0x28, 0x09, 0x0d, 0x71, 0x65, 0xe2, 0xad, 0x24, 0x74, - 0x6d, 0xa3, 0x80, 0x43, 0xb6, 0xc4, 0xf6, 0xcd, 0x85, 0x6e, 0x72, 0xbb, 0x64, 0x0f, 0xe5, 0x3f, 0x55, 0x5c, 0xb2, - 0x9e, 0xe5, 0x98, 0x92, 0x06, 0x4c, 0x31, 0x1e, 0x2c, 0x4d, 0x03, 0x12, 0xe0, 0xbb, 0x72, 0x14, 0x17, 0xeb, 0x49, - 0xf0, 0xbb, 0x82, 0xf9, 0xdc, 0x98, 0xe9, 0x56, 0x48, 0xb5, 0x84, 0x93, 0x66, 0xb0, 0x06, 0x4d, 0x1a, 0x0f, 0x4a, - 0xd4, 0x7c, 0x8d, 0x86, 0x0a, 0x71, 0xfc, 0x99, 0xa8, 0x42, 0x13, 0x0c, 0xc1, 0xc8, 0xbd, 0x42, 0x32, 0x5c, 0xb6, - 0x2c, 0x5a, 0xa4, 0x4c, 0x8d, 0x49, 0xa5, 0x6a, 0x96, 0xcb, 0xc0, 0xc0, 0xa2, 0xdd, 0xea, 0x4b, 0x4b, 0x5c, 0x89, - 0xdc, 0x34, 0xd4, 0xc2, 0xa4, 0x50, 0xde, 0x84, 0x93, 0xa3, 0xdf, 0xa5, 0xac, 0x77, 0x13, 0x9f, 0x5c, 0xe1, 0x93, - 0xfb, 0x86, 0x0f, 0x65, 0xf2, 0x76, 0x31, 0x28, 0x82, 0xaf, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, - 0x74, 0x41, 0xa0, 0x48, 0x36, 0x49, 0x07, 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xe6, 0x8a, 0x0d, 0x52, 0xf3, - 0x4a, 0x33, 0x2f, 0x75, 0x1b, 0xf6, 0x7b, 0x59, 0x4a, 0x3a, 0x31, 0x41, 0x99, 0xd8, 0xbb, 0x89, 0x36, 0x5e, 0x1a, - 0x66, 0xc2, 0xfa, 0x15, 0xc6, 0x4e, 0x8d, 0x42, 0xa9, 0x14, 0x81, 0x38, 0x36, 0xbe, 0x56, 0x96, 0x41, 0xe6, 0xaf, - 0xb0, 0xa7, 0x00, 0x94, 0x04, 0x16, 0x5f, 0x53, 0xc9, 0x8b, 0xc2, 0x3a, 0x1d, 0xef, 0x11, 0x1d, 0x2b, 0x11, 0x5a, - 0x13, 0xf9, 0x5a, 0x9f, 0xc5, 0x7e, 0xcd, 0x25, 0x34, 0x29, 0x99, 0x0f, 0xf2, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, - 0x25, 0x83, 0x84, 0x1c, 0xd2, 0x65, 0xa2, 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x0a, 0x89, 0x96, 0x1e, 0x85, 0x11, 0x8a, - 0x0d, 0xb1, 0x16, 0x4b, 0x84, 0x6c, 0xda, 0x9b, 0xc4, 0x8a, 0xe8, 0x9c, 0xe6, 0x68, 0xc2, 0x99, 0x3a, 0xdd, 0x71, - 0x00, 0x1d, 0x10, 0xfb, 0x4b, 0xac, 0xb7, 0xd2, 0xec, 0x74, 0xfd, 0xca, 0xe1, 0xbb, 0xbe, 0x9e, 0x00, 0x3f, 0x48, - 0x83, 0x17, 0xd6, 0x6c, 0xa0, 0x64, 0xef, 0xde, 0x6b, 0x6c, 0x45, 0xf6, 0x67, 0x55, 0x52, 0x79, 0x0a, 0x35, 0xce, - 0xad, 0xaf, 0x53, 0x2d, 0xb4, 0xa8, 0x2a, 0xf6, 0x0d, 0xa9, 0xbe, 0xaf, 0x14, 0x76, 0x85, 0xf2, 0xbe, 0x1c, 0x3a, - 0x76, 0x5d, 0x37, 0xc8, 0xc9, 0x79, 0xb9, 0xb7, 0xca, 0x85, 0xbc, 0x7f, 0xdf, 0xf4, 0x99, 0xce, 0xf5, 0xf0, 0xcf, - 0x1c, 0x54, 0xce, 0xc5, 0x55, 0x4a, 0x16, 0xcc, 0x33, 0xa5, 0x8e, 0x96, 0x1c, 0xd0, 0x76, 0x0f, 0x3d, 0xed, 0xe8, - 0x22, 0x8a, 0xb9, 0xa5, 0x47, 0x11, 0x9e, 0x36, 0xca, 0x27, 0x69, 0x74, 0x00, 0x5e, 0x68, 0x42, 0x92, 0x13, 0x6e, - 0xda, 0xa2, 0xc5, 0x70, 0xc2, 0x30, 0x04, 0xae, 0xec, 0x09, 0x53, 0xf6, 0xdc, 0x43, 0xbc, 0xe5, 0xc0, 0xab, 0x61, - 0x2f, 0x9b, 0xdd, 0x6b, 0xe6, 0x3f, 0xac, 0x11, 0xc8, 0xb6, 0xa9, 0xaa, 0x2b, 0x1b, 0xef, 0x52, 0x44, 0x62, 0x84, - 0x6d, 0xd5, 0xd8, 0xd2, 0xd6, 0xef, 0x35, 0xdc, 0xeb, 0xca, 0x31, 0xaf, 0x29, 0xd5, 0x86, 0x1e, 0x56, 0x6e, 0x0e, - 0x37, 0x1d, 0x79, 0xb1, 0x82, 0x6e, 0x4f, 0x04, 0x85, 0xc0, 0x89, 0x50, 0xf6, 0xa0, 0xe6, 0x06, 0x22, 0x25, 0x53, - 0x5a, 0x35, 0x9b, 0x27, 0x23, 0x09, 0x2c, 0xb8, 0xb0, 0x4c, 0xf2, 0xd1, 0x45, 0x9c, 0x24, 0x55, 0xe9, 0xef, 0x2a, - 0xe0, 0xc5, 0xb0, 0xb7, 0x89, 0x76, 0x81, 0xd1, 0x5c, 0x81, 0xe0, 0x6a, 0x23, 0xec, 0xa3, 0xe3, 0x56, 0xeb, 0x2e, - 0x22, 0x8e, 0xcc, 0x8c, 0x46, 0x7c, 0x44, 0x1b, 0xb2, 0x64, 0x9a, 0xb5, 0xf7, 0x5e, 0x60, 0x48, 0xcd, 0xc0, 0x07, - 0xd5, 0x19, 0x15, 0xff, 0x2a, 0x7b, 0xea, 0x57, 0xa2, 0x77, 0xab, 0xea, 0x6a, 0x06, 0x54, 0x54, 0xe0, 0xc3, 0x0c, - 0xb1, 0xb4, 0x55, 0x20, 0x20, 0xd7, 0xc3, 0x3a, 0xdc, 0xad, 0x91, 0x06, 0x0b, 0x4a, 0x81, 0xb5, 0x56, 0x76, 0xaf, - 0x6f, 0x0b, 0xe6, 0x50, 0x28, 0x5c, 0xf4, 0x7f, 0x96, 0x4d, 0x67, 0x68, 0x99, 0x35, 0x98, 0x1a, 0x1a, 0x7c, 0x6c, - 0xd4, 0x97, 0x2b, 0xca, 0x6a, 0x7d, 0x68, 0x47, 0xd6, 0xf8, 0x49, 0x3b, 0xca, 0xe0, 0x50, 0xcd, 0x75, 0x51, 0xdd, - 0x6e, 0x6e, 0x8a, 0x98, 0x55, 0x3c, 0xee, 0x93, 0xde, 0xd6, 0xd6, 0xa4, 0xa7, 0x69, 0x40, 0x32, 0x49, 0x32, 0xbc, - 0xc9, 0x00, 0x65, 0x45, 0x9c, 0x45, 0xd9, 0x20, 0xdf, 0xa2, 0x2c, 0x71, 0xfd, 0x7e, 0xe8, 0xed, 0xd5, 0x3c, 0x6b, - 0x6f, 0x6f, 0xbd, 0x8b, 0x5c, 0xd5, 0x49, 0x0f, 0xf2, 0xf0, 0x08, 0x8a, 0x96, 0x6c, 0xca, 0x70, 0x31, 0xcd, 0x46, - 0x2c, 0xb0, 0xa1, 0x7b, 0x6a, 0x97, 0x72, 0xd3, 0x44, 0xc0, 0x3d, 0x11, 0x73, 0x16, 0x1f, 0xea, 0x91, 0xd4, 0x60, - 0x0f, 0x58, 0x40, 0x9b, 0x0b, 0x5f, 0x85, 0x67, 0x49, 0x76, 0x1a, 0x25, 0x07, 0x42, 0x81, 0xd7, 0x5a, 0x7e, 0x0b, - 0x2e, 0x23, 0x59, 0xac, 0x86, 0x92, 0xfa, 0x6a, 0xf0, 0x55, 0x70, 0x7b, 0x8f, 0xca, 0x5b, 0xb1, 0x3b, 0x7e, 0xdb, - 0xef, 0xd8, 0x2a, 0x22, 0xf6, 0x93, 0x39, 0x1d, 0x68, 0x9c, 0x02, 0x28, 0x73, 0x00, 0x9a, 0xac, 0xf0, 0x86, 0x2c, - 0xfc, 0x69, 0xf0, 0x93, 0x72, 0xa9, 0x33, 0x70, 0x21, 0xc0, 0xc9, 0x4f, 0x62, 0xde, 0xc2, 0xf3, 0x48, 0xdb, 0x5b, - 0x88, 0x0a, 0x8c, 0x2b, 0x52, 0x5c, 0xba, 0x54, 0xde, 0xa0, 0x77, 0x1c, 0x9e, 0x40, 0xb3, 0x8d, 0x8d, 0x85, 0xf3, - 0x26, 0xe2, 0x13, 0x3f, 0x8f, 0xd2, 0x51, 0x36, 0x75, 0xdc, 0x4d, 0xdb, 0x76, 0xfd, 0x82, 0x3c, 0x91, 0xcf, 0xdd, - 0x72, 0xe3, 0x04, 0xfc, 0x80, 0xd0, 0x1e, 0xd8, 0x9b, 0xc7, 0xde, 0x01, 0x0b, 0x4f, 0x76, 0x37, 0x16, 0x23, 0x56, - 0xf6, 0x4f, 0xbc, 0x4b, 0x1d, 0x73, 0xf7, 0xde, 0xa3, 0x94, 0x81, 0x5e, 0x61, 0xff, 0x52, 0x82, 0x01, 0xec, 0x46, - 0xf1, 0x77, 0x90, 0x72, 0x1f, 0xe9, 0x40, 0x44, 0xc6, 0x69, 0xaf, 0xaf, 0xed, 0x8c, 0x22, 0x06, 0xf6, 0x3d, 0xed, - 0xac, 0xde, 0xbf, 0x5f, 0xa9, 0xf9, 0xaa, 0xd4, 0x9b, 0xb3, 0xb0, 0xe6, 0xa9, 0x7b, 0x2f, 0xe9, 0x68, 0xa5, 0xbe, - 0x91, 0xe7, 0x8c, 0x94, 0xe6, 0xb2, 0x9d, 0xe0, 0x18, 0x5b, 0x7c, 0xf5, 0xb6, 0x3e, 0x14, 0x51, 0x0a, 0x3f, 0x06, - 0xeb, 0x25, 0x02, 0xf5, 0x0d, 0x0e, 0x8e, 0x77, 0x10, 0x6e, 0xed, 0x3a, 0x83, 0xc0, 0xb9, 0xd7, 0x6a, 0x5d, 0xff, - 0xb8, 0x75, 0xf8, 0xe7, 0xa8, 0xf5, 0xcb, 0x5e, 0xeb, 0x87, 0x23, 0xf7, 0xda, 0xf9, 0x71, 0x6b, 0x70, 0x28, 0xdf, - 0x0e, 0xff, 0xdc, 0xff, 0xb1, 0x38, 0xfa, 0x83, 0x28, 0xdc, 0x70, 0xdd, 0xad, 0x33, 0x6f, 0xc6, 0xc2, 0xad, 0x56, - 0xab, 0x0f, 0x4f, 0x67, 0xf0, 0x84, 0x7f, 0x2f, 0xe0, 0xcf, 0xf5, 0xa1, 0xf5, 0x9f, 0x7e, 0x4c, 0xff, 0xf3, 0x8f, - 0xf9, 0x11, 0x8e, 0x79, 0xf8, 0xe7, 0x1f, 0x0b, 0xfb, 0x41, 0x3f, 0xdc, 0x3a, 0xda, 0x74, 0x1d, 0x5d, 0xf3, 0x87, - 0xb0, 0x7a, 0x84, 0x56, 0x87, 0x7f, 0x96, 0x6f, 0xf6, 0x83, 0x93, 0xdd, 0x7e, 0x78, 0x74, 0xed, 0xd8, 0xd7, 0x0f, - 0xdc, 0x6b, 0xd7, 0xbd, 0xde, 0xc0, 0x79, 0xce, 0x61, 0xf4, 0x07, 0xf0, 0x77, 0x0c, 0x7f, 0x6d, 0xf8, 0x3b, 0x85, - 0xbf, 0x7f, 0x86, 0x6e, 0x22, 0xfe, 0x76, 0x4d, 0xb1, 0x90, 0x6b, 0x3c, 0xb0, 0x88, 0x60, 0x15, 0xdc, 0x8d, 0xad, - 0xd8, 0xdb, 0x20, 0xa2, 0xc1, 0x3e, 0xf4, 0x7d, 0x1f, 0xc3, 0xa4, 0xce, 0xe2, 0x78, 0x03, 0x16, 0x1d, 0x39, 0x67, - 0x23, 0xe0, 0x9e, 0x88, 0x1c, 0x14, 0x01, 0x13, 0x67, 0xab, 0x05, 0x1e, 0xae, 0x7a, 0xc3, 0x70, 0x83, 0x39, 0x60, - 0x14, 0xbc, 0x65, 0xf8, 0xd0, 0x75, 0xbd, 0x17, 0xf2, 0xcc, 0x10, 0xf7, 0xb9, 0x60, 0xad, 0x34, 0x13, 0x26, 0x8d, - 0xed, 0x7a, 0xb3, 0x15, 0x95, 0xb0, 0xad, 0xd3, 0x33, 0xa8, 0x3b, 0x15, 0x27, 0x8c, 0xdf, 0xb1, 0xe8, 0x13, 0x6e, - 0xc9, 0x37, 0xc6, 0x21, 0xf0, 0x92, 0x25, 0xdf, 0x34, 0x1a, 0x0d, 0x1b, 0x51, 0xb8, 0x63, 0x4f, 0x19, 0xcc, 0xb0, - 0x64, 0x22, 0x32, 0x52, 0x9a, 0xc2, 0xb2, 0x85, 0xc9, 0xdf, 0x47, 0x39, 0xdf, 0xa8, 0x0c, 0xdb, 0xb0, 0x66, 0xc9, - 0x36, 0x2d, 0xfd, 0x3b, 0x4c, 0x81, 0xa6, 0x25, 0x9d, 0x7f, 0x98, 0xe3, 0x87, 0x29, 0xa1, 0xf5, 0xd6, 0x61, 0xe0, - 0xa1, 0x17, 0x20, 0x77, 0x44, 0x3f, 0xe7, 0x3d, 0xaa, 0x31, 0xf8, 0x9f, 0x0c, 0x33, 0x78, 0x62, 0x3e, 0x0c, 0xd1, - 0x2c, 0x4a, 0x1d, 0xdc, 0x4a, 0x51, 0xdc, 0xbf, 0xc2, 0x9d, 0x91, 0x96, 0xde, 0x7e, 0xa8, 0x76, 0xcc, 0x41, 0xce, - 0xd8, 0x77, 0x51, 0xf2, 0x89, 0xe5, 0xce, 0xa5, 0xd7, 0xe9, 0x7e, 0x4e, 0x9d, 0x3d, 0xb4, 0xcd, 0x3e, 0x54, 0xc7, - 0x68, 0xda, 0x2c, 0x90, 0x47, 0x84, 0xad, 0x8e, 0x97, 0x63, 0x54, 0x0b, 0x49, 0x50, 0x78, 0x59, 0xd8, 0x25, 0x0e, - 0xb7, 0x77, 0x8b, 0xf3, 0xb3, 0xbe, 0x1d, 0xd8, 0x36, 0x58, 0xfc, 0x07, 0x14, 0xb6, 0x12, 0x86, 0x45, 0xbb, 0xc7, - 0x76, 0xe3, 0x1e, 0xdb, 0xdc, 0xac, 0x02, 0x4e, 0x78, 0x90, 0x4e, 0xdd, 0x13, 0x2f, 0xf2, 0x26, 0x21, 0x0c, 0x38, - 0x84, 0x66, 0xd8, 0xa5, 0x37, 0xdc, 0x8d, 0xe5, 0x34, 0x18, 0x0b, 0xf1, 0x93, 0xa8, 0xe0, 0xaf, 0x30, 0x1e, 0x11, - 0x0e, 0xd1, 0xd8, 0xf7, 0xd9, 0x25, 0x1b, 0x2a, 0x3b, 0x03, 0x08, 0x15, 0xb9, 0x3d, 0x77, 0x18, 0x1a, 0xcd, 0x60, - 0xee, 0x30, 0x3c, 0x18, 0xd8, 0xb0, 0x97, 0x60, 0x57, 0x86, 0xd1, 0x61, 0xe7, 0x68, 0x90, 0x86, 0x33, 0x16, 0x68, - 0xda, 0xca, 0xa2, 0xb3, 0x5a, 0x51, 0xf7, 0x68, 0xe0, 0x4c, 0x99, 0xcf, 0xc1, 0x16, 0x77, 0xf0, 0x0d, 0x23, 0x14, - 0x45, 0xf8, 0x81, 0x9d, 0xbd, 0xb8, 0x9c, 0x39, 0xf6, 0xee, 0x96, 0xbd, 0x89, 0xa5, 0x9e, 0x0d, 0xec, 0x05, 0x73, - 0x87, 0x17, 0xae, 0xd9, 0x79, 0xfb, 0x08, 0x41, 0xc5, 0x42, 0x9c, 0xfc, 0x62, 0x60, 0xf7, 0xc5, 0xd4, 0x6d, 0x18, - 0x34, 0x95, 0xcb, 0x8f, 0x2b, 0x7a, 0x40, 0xa8, 0xaa, 0xae, 0x0a, 0x3a, 0x28, 0xeb, 0x06, 0xce, 0xc4, 0x44, 0xa2, - 0x85, 0x93, 0x49, 0x2a, 0x80, 0xc3, 0x83, 0xcd, 0x60, 0x52, 0xa3, 0xdb, 0xf6, 0xd1, 0xe0, 0x22, 0x78, 0x60, 0x3f, - 0x50, 0x2f, 0x63, 0x40, 0x86, 0x89, 0xe9, 0xc7, 0xa0, 0x45, 0xf0, 0xef, 0x39, 0x03, 0x24, 0x2f, 0xa8, 0x68, 0x26, - 0x8b, 0xce, 0xb0, 0xe8, 0x20, 0x40, 0x50, 0xbd, 0x42, 0x5b, 0x7f, 0x62, 0x4d, 0x46, 0x21, 0xc1, 0x0e, 0xb6, 0xd0, - 0x21, 0xdb, 0xec, 0x1c, 0xe1, 0x79, 0x43, 0xce, 0x8b, 0xef, 0x62, 0x0e, 0x2a, 0x61, 0xab, 0x6f, 0xbb, 0x03, 0xdb, - 0xc2, 0xa5, 0xed, 0x65, 0x9b, 0xa1, 0xa0, 0x70, 0xbc, 0x79, 0xc0, 0x82, 0x49, 0x3f, 0x6c, 0x0f, 0x9c, 0x5c, 0x86, - 0x1b, 0xf1, 0xdc, 0x52, 0x48, 0xf0, 0xb6, 0x37, 0x01, 0x81, 0x8e, 0x9c, 0xbb, 0x61, 0x6f, 0xaa, 0x42, 0x28, 0x3a, - 0xde, 0x1c, 0xb9, 0x41, 0x0c, 0x7f, 0x9c, 0x16, 0x32, 0xcd, 0x44, 0xf7, 0x55, 0x9a, 0x19, 0x90, 0x18, 0x29, 0x8b, - 0x3c, 0x09, 0xb3, 0x4d, 0x07, 0x23, 0xb4, 0x20, 0x69, 0x77, 0x07, 0x00, 0xc3, 0xa6, 0xa3, 0x38, 0x6d, 0x4b, 0xb1, - 0x9a, 0xb2, 0xcf, 0x0f, 0xf5, 0x72, 0x0c, 0xd9, 0x60, 0xc8, 0xfc, 0x4a, 0xfb, 0x00, 0x58, 0x41, 0xe2, 0xe5, 0x47, - 0xea, 0xcc, 0xeb, 0x65, 0xed, 0x7c, 0x6b, 0xa1, 0x44, 0x11, 0xf7, 0x0c, 0x09, 0xc5, 0x4a, 0xed, 0x86, 0x09, 0x73, - 0x7b, 0x86, 0xc4, 0xd0, 0x2c, 0x1f, 0xb6, 0x81, 0xe9, 0x55, 0x80, 0x3d, 0x35, 0xb7, 0x45, 0x12, 0x56, 0xcd, 0xbd, - 0x43, 0x60, 0xed, 0x23, 0xe0, 0x21, 0xda, 0x46, 0x3d, 0x15, 0xcd, 0x67, 0x49, 0xf8, 0xb2, 0x71, 0x5c, 0x1c, 0xe1, - 0x89, 0xd0, 0xbe, 0x3f, 0x9c, 0xe7, 0x20, 0x0f, 0xf8, 0x5b, 0xb0, 0x0c, 0x42, 0xd9, 0x14, 0x1d, 0x3d, 0x3c, 0x02, - 0xf6, 0x08, 0xf1, 0x46, 0xd8, 0xdc, 0xa8, 0x46, 0x8b, 0x92, 0x8c, 0x17, 0x3a, 0x18, 0xee, 0x31, 0xe9, 0xda, 0xa3, - 0x60, 0x90, 0x27, 0xc6, 0x0e, 0x9e, 0xf9, 0xfb, 0x43, 0xac, 0xc6, 0x09, 0x0a, 0xb7, 0xa4, 0xdd, 0x56, 0x89, 0xbf, - 0x7d, 0x3f, 0x05, 0x09, 0x8e, 0x75, 0xe0, 0x67, 0xdd, 0xbf, 0x9f, 0x48, 0xa4, 0x76, 0xd3, 0x1e, 0x9d, 0x44, 0x60, - 0x3c, 0x38, 0xf7, 0x53, 0xa8, 0x46, 0x12, 0x51, 0x51, 0x8e, 0x16, 0xa8, 0x79, 0xaa, 0x56, 0xc1, 0x77, 0x68, 0x46, - 0xe0, 0x19, 0x86, 0xad, 0xc9, 0x4f, 0xd5, 0x8d, 0x45, 0x2c, 0xdf, 0x75, 0xe9, 0x68, 0x0b, 0x0f, 0x20, 0x05, 0xa3, - 0x09, 0x86, 0x71, 0x29, 0x28, 0x59, 0xf1, 0xdf, 0xb1, 0x11, 0x2b, 0x9f, 0x1c, 0x66, 0x9b, 0x9b, 0x47, 0xe2, 0xdc, - 0x82, 0x18, 0x87, 0x19, 0xd1, 0xd5, 0xb8, 0x02, 0xa0, 0x3e, 0x9d, 0x13, 0xd7, 0x03, 0xd3, 0x8a, 0x35, 0x5d, 0x8a, - 0x7d, 0x72, 0x98, 0x01, 0x28, 0xb8, 0xe5, 0x1c, 0xfa, 0x83, 0x3f, 0x1e, 0x81, 0x7b, 0xec, 0xff, 0xc1, 0xdd, 0x52, - 0x82, 0xa6, 0x27, 0xcf, 0x14, 0x17, 0x74, 0xc6, 0xda, 0xf1, 0x28, 0x36, 0x1a, 0x14, 0x5e, 0x0a, 0x18, 0x80, 0x36, - 0x07, 0x99, 0x50, 0x71, 0x10, 0x72, 0x54, 0x60, 0xfb, 0xb8, 0xf9, 0x19, 0xee, 0xec, 0xe7, 0x60, 0xe1, 0x0d, 0xf4, - 0xdb, 0x6b, 0x78, 0xfb, 0xa3, 0x7e, 0xfb, 0x89, 0x05, 0xbf, 0x94, 0x32, 0x74, 0x5f, 0x9b, 0xe2, 0x91, 0x9a, 0xa2, - 0x14, 0x4b, 0x64, 0xd0, 0x90, 0xbb, 0xf9, 0x52, 0xcc, 0x86, 0xb9, 0x25, 0x10, 0x43, 0x89, 0xae, 0xdc, 0xe7, 0xd1, - 0x19, 0x12, 0xd7, 0x35, 0x49, 0x61, 0xe4, 0x12, 0x98, 0x08, 0x57, 0x7c, 0x8b, 0xf4, 0x64, 0xfd, 0x36, 0xd8, 0xe0, - 0xb5, 0xbc, 0x03, 0xb4, 0xef, 0xd8, 0x74, 0xc6, 0xaf, 0xf6, 0x49, 0xd1, 0x07, 0x32, 0x6d, 0x40, 0x9c, 0x9d, 0xb7, - 0x7b, 0xf1, 0x2e, 0xeb, 0xc5, 0x20, 0xd5, 0x73, 0xc5, 0x62, 0xb8, 0x57, 0xbd, 0xf7, 0x18, 0xa5, 0x34, 0x99, 0xc9, - 0xab, 0xa1, 0xd7, 0x95, 0xe8, 0x6d, 0x6e, 0x02, 0x82, 0x3d, 0xa3, 0x2b, 0x13, 0x5d, 0xcb, 0x52, 0xd0, 0x04, 0x20, - 0x7a, 0x52, 0x67, 0x39, 0xe2, 0x38, 0xcc, 0x66, 0x83, 0xe2, 0x11, 0x73, 0x57, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, - 0xb0, 0x03, 0x98, 0x11, 0x97, 0xb7, 0x3a, 0x22, 0x3a, 0x2c, 0xfa, 0xeb, 0xf8, 0xf6, 0xb1, 0xc7, 0x37, 0x3b, 0x2e, - 0x68, 0x90, 0xda, 0x58, 0x8f, 0xab, 0xb1, 0xa0, 0x3e, 0x3c, 0xd6, 0x54, 0x2a, 0x8b, 0xcd, 0xcd, 0xb2, 0x7e, 0x54, - 0xab, 0x76, 0x70, 0xed, 0x34, 0xe5, 0xb2, 0x99, 0x0d, 0xc2, 0x81, 0x88, 0x09, 0x14, 0x68, 0x69, 0x65, 0xc5, 0x00, - 0x43, 0xca, 0x72, 0x94, 0x4f, 0x21, 0xf7, 0xe2, 0xb2, 0xd4, 0xa9, 0x2f, 0xcf, 0x64, 0xd0, 0x11, 0x4f, 0x3d, 0xc9, - 0x58, 0x01, 0x05, 0xeb, 0xa5, 0x5e, 0x42, 0x4b, 0x04, 0x98, 0xbf, 0x50, 0x39, 0x34, 0xc2, 0x02, 0x89, 0x42, 0xc3, - 0x2c, 0x51, 0xc6, 0x67, 0x11, 0xc6, 0xa0, 0xed, 0x9f, 0xd5, 0x62, 0x5f, 0x85, 0x32, 0x3a, 0x8a, 0xc3, 0xfc, 0x28, - 0xa0, 0xfa, 0xb9, 0x94, 0x60, 0x93, 0xf0, 0x23, 0xb0, 0x51, 0xe5, 0x78, 0x92, 0x20, 0x7c, 0x1e, 0xe7, 0x8c, 0x3c, - 0x85, 0x0d, 0x09, 0xb3, 0x34, 0x6d, 0x23, 0xd5, 0x2e, 0x32, 0x83, 0x50, 0x2e, 0xcc, 0x3f, 0x31, 0xce, 0x2e, 0xb2, - 0x70, 0xa9, 0x35, 0x98, 0x1f, 0xef, 0x4c, 0x80, 0xb2, 0xeb, 0xeb, 0x4c, 0xf8, 0xb8, 0x11, 0xd9, 0x1b, 0xba, 0x62, - 0x32, 0x50, 0x48, 0x05, 0x4e, 0x44, 0x16, 0x0f, 0x9d, 0xa1, 0xd0, 0x08, 0x07, 0x74, 0x8a, 0x9c, 0xbb, 0xc6, 0xa6, - 0xcf, 0x07, 0xda, 0x37, 0x4a, 0x43, 0x27, 0x01, 0x21, 0x20, 0x70, 0x37, 0xac, 0xa9, 0x74, 0x90, 0x06, 0x09, 0x95, - 0xa2, 0x9f, 0x03, 0xf8, 0x87, 0x91, 0xa4, 0x00, 0xd8, 0x0f, 0xd5, 0x48, 0x11, 0x65, 0x59, 0xe0, 0x02, 0xd0, 0x5c, - 0xfb, 0xb8, 0x12, 0xbe, 0x30, 0x50, 0x61, 0x7a, 0x9a, 0x95, 0x95, 0x42, 0x89, 0x3c, 0x5d, 0x91, 0xb2, 0x46, 0x32, - 0xf9, 0x1c, 0x1d, 0x3e, 0xe5, 0x5d, 0xbf, 0x95, 0x78, 0xe8, 0x82, 0xe7, 0xb0, 0xac, 0xea, 0xf9, 0x4d, 0xc8, 0xc8, - 0xb9, 0x06, 0x5d, 0x21, 0x85, 0xfe, 0x92, 0x93, 0xbc, 0xf7, 0xc6, 0xaf, 0x6a, 0xa9, 0x31, 0x94, 0x7d, 0x5c, 0xd5, - 0x0c, 0xcb, 0xcb, 0x59, 0x15, 0xa6, 0x20, 0xe0, 0x16, 0x2c, 0x09, 0x16, 0x52, 0x43, 0x80, 0x85, 0xed, 0x91, 0x56, - 0x0a, 0xf2, 0x52, 0x87, 0x77, 0x9e, 0x83, 0x15, 0x60, 0x1c, 0x6a, 0xa9, 0x64, 0x1a, 0x49, 0x7c, 0x99, 0xd4, 0x04, - 0x4c, 0xb9, 0x3f, 0x04, 0x3f, 0xb5, 0x79, 0xd2, 0x75, 0xe9, 0xfa, 0xf1, 0x14, 0x53, 0x7b, 0x08, 0xf4, 0xd8, 0xbb, - 0x07, 0xa6, 0x44, 0x5d, 0x87, 0x15, 0xc4, 0xa1, 0x59, 0x4d, 0xb3, 0x80, 0x19, 0xd3, 0x06, 0x2d, 0xd9, 0x06, 0x5b, - 0x2e, 0x07, 0xfb, 0x48, 0x6c, 0xcf, 0x6a, 0x05, 0x84, 0xae, 0x41, 0x03, 0x43, 0xee, 0x52, 0xa1, 0x85, 0x59, 0xaf, - 0x4b, 0x45, 0xb8, 0x3f, 0x07, 0x4c, 0x5a, 0xc1, 0x99, 0x97, 0xd1, 0xc0, 0xfb, 0xf1, 0x69, 0x82, 0x89, 0x2f, 0x88, - 0x15, 0xd8, 0xc1, 0x41, 0xa7, 0xd9, 0x14, 0x38, 0x15, 0x17, 0x29, 0x83, 0x65, 0x45, 0xa9, 0x0d, 0x7f, 0xa4, 0xc8, - 0xd6, 0x5d, 0x1e, 0xe9, 0x2e, 0xc4, 0x02, 0xd8, 0xe9, 0x17, 0x8c, 0x7c, 0xcb, 0x7a, 0x19, 0x30, 0x38, 0xd7, 0x1a, - 0x07, 0x81, 0xdf, 0xdc, 0x4c, 0x8e, 0xca, 0x94, 0xd8, 0xae, 0xc9, 0xea, 0x02, 0x72, 0x4c, 0x02, 0x6c, 0xe0, 0x0e, - 0xc2, 0x52, 0xd9, 0xe3, 0x45, 0x39, 0xc5, 0xe5, 0x52, 0x16, 0x72, 0x33, 0x1d, 0x8b, 0xe6, 0x73, 0x2b, 0xcd, 0xa6, - 0xe3, 0xad, 0xf8, 0xa2, 0xe0, 0x1f, 0x38, 0xb1, 0xb4, 0xea, 0x29, 0xb5, 0xc2, 0xa3, 0xcc, 0x2d, 0x59, 0xa7, 0xa4, - 0x56, 0xd7, 0x0d, 0x54, 0x23, 0x3c, 0x4d, 0xc3, 0x46, 0x20, 0xc4, 0x04, 0x17, 0xbf, 0x6d, 0x32, 0x31, 0xed, 0x2d, - 0x21, 0x75, 0x84, 0xdd, 0x43, 0x39, 0xc1, 0x5d, 0xcd, 0xb3, 0x2f, 0xc3, 0xd9, 0x7a, 0xe6, 0xde, 0x33, 0x98, 0xfb, - 0x69, 0xc8, 0x0c, 0x46, 0x8f, 0x65, 0xc2, 0x8f, 0x8c, 0x7d, 0xe4, 0xaa, 0xea, 0xd9, 0x59, 0x58, 0x89, 0x2c, 0xf1, - 0x64, 0x1c, 0x75, 0x18, 0xa7, 0xa2, 0x35, 0x41, 0x76, 0x7d, 0x5d, 0x98, 0x7b, 0x81, 0x82, 0xa6, 0x1e, 0xab, 0xc7, - 0x69, 0x2b, 0x76, 0x36, 0x22, 0x91, 0x7b, 0x6f, 0x6a, 0x91, 0xc8, 0x8a, 0xcf, 0x71, 0xa4, 0x35, 0x07, 0xb9, 0xcf, - 0xce, 0x96, 0x37, 0xa9, 0xd0, 0x2d, 0x1a, 0x6d, 0x63, 0x8f, 0xea, 0x03, 0x49, 0x3d, 0xa3, 0x02, 0xab, 0x1a, 0xfb, - 0xfe, 0xfd, 0x8e, 0x48, 0xb7, 0x54, 0x8a, 0x0d, 0x43, 0x5a, 0x21, 0x33, 0x46, 0xc1, 0xa0, 0xa4, 0xc8, 0x40, 0x8d, - 0xf2, 0x35, 0x82, 0x61, 0x8f, 0x1a, 0x80, 0xe2, 0x5c, 0x5d, 0xfd, 0xb4, 0x94, 0x6c, 0x21, 0x20, 0x01, 0xd9, 0x84, - 0x62, 0x8d, 0x98, 0x19, 0xf9, 0xe4, 0x23, 0x70, 0xde, 0x80, 0xa3, 0x63, 0x00, 0x7e, 0x81, 0xd8, 0xf4, 0x60, 0x62, - 0xdb, 0x44, 0x14, 0x7d, 0x36, 0xf0, 0x12, 0x80, 0x9d, 0x55, 0xa1, 0xd1, 0x0f, 0x55, 0x0a, 0x18, 0xb2, 0x81, 0x1b, - 0xf0, 0x2a, 0x2c, 0xb7, 0xf7, 0x12, 0xda, 0xc1, 0xeb, 0x0b, 0xd9, 0x7c, 0x03, 0xf3, 0x04, 0xab, 0xd8, 0x9d, 0x5f, - 0x59, 0xd6, 0xe2, 0xdc, 0xe9, 0xa0, 0x51, 0xaf, 0x28, 0x21, 0x6a, 0xf7, 0xb1, 0xf6, 0x25, 0x23, 0x18, 0xf1, 0xfd, - 0x0d, 0x65, 0x1d, 0xaa, 0x71, 0xcb, 0x3d, 0x8d, 0x16, 0x61, 0xba, 0x4c, 0x1a, 0x83, 0x92, 0x75, 0x3f, 0x19, 0x71, - 0x2f, 0xf7, 0x45, 0x2c, 0xb8, 0xc2, 0xd1, 0x08, 0x9b, 0x37, 0x90, 0xa4, 0xa7, 0x3d, 0x3a, 0x60, 0xdf, 0x68, 0xf6, - 0x02, 0xca, 0x7c, 0xac, 0x48, 0x25, 0x21, 0xa5, 0xd9, 0x0d, 0x91, 0x24, 0xac, 0x15, 0x79, 0xea, 0xbc, 0xef, 0x68, - 0x9f, 0x5b, 0x49, 0x04, 0x23, 0x38, 0x89, 0xd3, 0x95, 0x07, 0x4d, 0x01, 0xae, 0xa2, 0x23, 0xa6, 0x6f, 0x82, 0xf2, - 0x1b, 0xe4, 0xf6, 0x52, 0x72, 0x6d, 0xae, 0x61, 0x78, 0x86, 0x04, 0xab, 0x22, 0x11, 0x78, 0x44, 0x0d, 0x38, 0xe6, - 0xab, 0x3c, 0x0f, 0x30, 0xe1, 0x6b, 0x7b, 0x13, 0x00, 0xca, 0xc9, 0x55, 0x71, 0x96, 0x02, 0xdd, 0x80, 0xe5, 0xea, - 0x38, 0x35, 0x2a, 0x12, 0x17, 0x37, 0xa6, 0xab, 0x5b, 0xfa, 0x53, 0xb4, 0x9c, 0xc9, 0x10, 0xd3, 0x41, 0x10, 0x90, - 0xa9, 0x4f, 0x99, 0x23, 0x64, 0xae, 0xb0, 0x3e, 0x67, 0x4e, 0x6d, 0xea, 0x1e, 0xa7, 0x6e, 0x9e, 0xa4, 0x16, 0xab, - 0xd3, 0xa6, 0x94, 0x88, 0x49, 0x89, 0x79, 0x2a, 0x53, 0xb1, 0x95, 0xb8, 0x73, 0xeb, 0x1b, 0x2d, 0xa4, 0x8d, 0x76, - 0x2a, 0x73, 0xb0, 0xb5, 0xbc, 0x17, 0xa2, 0xfd, 0x25, 0x11, 0x9e, 0x95, 0xc8, 0x58, 0x8b, 0x39, 0x73, 0x4c, 0x04, - 0xab, 0x17, 0x53, 0x91, 0x7f, 0x70, 0x74, 0x9a, 0xbd, 0x41, 0x0f, 0x52, 0x6f, 0x20, 0x31, 0x6b, 0xe2, 0xbb, 0x90, - 0x86, 0x3a, 0x42, 0xa0, 0x32, 0xaa, 0x65, 0x3a, 0x4e, 0x2c, 0x15, 0x97, 0xe4, 0xab, 0xf7, 0xfa, 0x38, 0xdf, 0x78, - 0x6e, 0xac, 0x46, 0x10, 0x83, 0xb7, 0x90, 0x1f, 0x79, 0x52, 0x84, 0x03, 0xe1, 0xf2, 0xcd, 0xcd, 0x5e, 0xbe, 0xcb, - 0xaa, 0x10, 0x49, 0x05, 0x63, 0x8c, 0x19, 0xc5, 0xb8, 0x27, 0x6a, 0x6a, 0x31, 0x07, 0x54, 0x65, 0xeb, 0x30, 0xc7, - 0x03, 0x00, 0x68, 0x69, 0x4a, 0x2f, 0xb3, 0xad, 0x3a, 0xcf, 0x25, 0x7c, 0x8c, 0x3c, 0x14, 0xd9, 0xf8, 0xfd, 0x9a, - 0x0c, 0x14, 0x84, 0xfb, 0x5e, 0xc7, 0xc3, 0xc4, 0x38, 0x58, 0x45, 0x21, 0x0b, 0xf4, 0x06, 0xed, 0x55, 0x89, 0x50, - 0xdc, 0x9c, 0xac, 0xc7, 0x0d, 0x27, 0x15, 0x6c, 0xa1, 0x12, 0x96, 0x4a, 0x0b, 0xfc, 0x6a, 0x23, 0x34, 0x4f, 0x19, - 0xf7, 0xde, 0x54, 0x38, 0x83, 0xfe, 0xe0, 0xde, 0x32, 0xa3, 0xbe, 0x5f, 0x3a, 0x91, 0xa9, 0xc0, 0xc4, 0xcd, 0x2c, - 0xb5, 0xdf, 0x2f, 0xab, 0xb4, 0x9f, 0x57, 0xc8, 0x7d, 0x4e, 0x9a, 0xaf, 0x73, 0x07, 0xcd, 0x27, 0xc3, 0xfd, 0x4a, - 0xf9, 0xa1, 0x85, 0x51, 0x53, 0x7e, 0x79, 0x5d, 0xf9, 0x15, 0x9e, 0x0a, 0x6f, 0xf5, 0xbb, 0x28, 0x74, 0x51, 0x9f, - 0x83, 0x21, 0xa4, 0x1f, 0xc1, 0x35, 0x34, 0x78, 0x50, 0x24, 0x8b, 0xc5, 0xda, 0x05, 0x71, 0x7d, 0xcc, 0xa9, 0x76, - 0x28, 0x63, 0x8c, 0x78, 0x5a, 0x72, 0x90, 0x64, 0x70, 0x30, 0x7e, 0x03, 0x03, 0x62, 0x52, 0x12, 0xd2, 0x21, 0x74, - 0x56, 0x66, 0x22, 0x2a, 0x77, 0xf1, 0x76, 0xe3, 0xb2, 0xa6, 0x50, 0x84, 0x9d, 0x60, 0xa6, 0x52, 0x2a, 0x08, 0xa4, - 0xc9, 0x77, 0xaf, 0x53, 0x0b, 0x86, 0x82, 0x68, 0x30, 0x14, 0x90, 0xd7, 0x76, 0x3d, 0x68, 0xf2, 0x51, 0x1c, 0x3c, - 0xaf, 0x50, 0x23, 0x5e, 0x66, 0xf0, 0x35, 0x6c, 0xfe, 0x9a, 0x28, 0xc9, 0x43, 0x2e, 0x62, 0xaf, 0xe0, 0x13, 0x21, - 0x9b, 0xf2, 0xb0, 0x00, 0xfa, 0xa1, 0x5d, 0xd9, 0x4b, 0x77, 0x8b, 0xca, 0xa5, 0x45, 0x63, 0x2b, 0x51, 0xb3, 0xe6, - 0x87, 0xf1, 0x66, 0x7a, 0x04, 0x53, 0x53, 0x02, 0x01, 0x69, 0x2a, 0x27, 0xa9, 0xe6, 0x3d, 0x4c, 0x8f, 0x00, 0x24, - 0xd8, 0xfd, 0x04, 0x16, 0xfa, 0x4d, 0x89, 0x09, 0x16, 0x55, 0x63, 0xb7, 0x19, 0x68, 0xcd, 0x19, 0x69, 0xbe, 0x19, - 0x42, 0xb8, 0xa9, 0xac, 0x67, 0xcc, 0x0e, 0xb0, 0x6d, 0x77, 0xb3, 0x38, 0x4c, 0x37, 0x3b, 0x47, 0x86, 0xe0, 0xc2, - 0xe3, 0xff, 0xa4, 0xc4, 0x34, 0x90, 0x5c, 0xea, 0xc6, 0x4f, 0xa8, 0xc3, 0x3e, 0x91, 0x3a, 0x11, 0x03, 0x9a, 0xab, - 0xd1, 0x74, 0xee, 0x35, 0x47, 0xc9, 0x65, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0x8c, 0x89, 0x62, 0x9e, 0x13, - 0x00, 0xa3, 0xd8, 0xfc, 0x39, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4c, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0xcf, 0xe8, 0x48, - 0x45, 0x65, 0x73, 0x12, 0xf3, 0x6f, 0x4b, 0x30, 0x8d, 0x89, 0x0f, 0xf5, 0x5c, 0x47, 0xa1, 0x00, 0x5f, 0xd9, 0x50, - 0x6a, 0xb6, 0xd7, 0xbf, 0x75, 0xb6, 0x87, 0x72, 0x36, 0xc1, 0x02, 0x0d, 0xba, 0xac, 0xc1, 0x17, 0xb0, 0x0c, 0xee, - 0x48, 0x3f, 0x05, 0xdf, 0x4f, 0xeb, 0xe0, 0x33, 0xf6, 0xbf, 0x00, 0xb4, 0x2a, 0x30, 0xa0, 0xdc, 0x69, 0x1a, 0x56, - 0x42, 0x5c, 0xa2, 0xc2, 0xac, 0xe2, 0xfc, 0x71, 0x9d, 0xd7, 0x4d, 0xcb, 0x12, 0x83, 0xf2, 0x33, 0xd7, 0x70, 0xe3, - 0x7b, 0x8d, 0xfc, 0xf1, 0xbd, 0x97, 0xa0, 0xdb, 0x89, 0xb4, 0xf7, 0xef, 0xe7, 0xf7, 0xc8, 0x42, 0x03, 0x3f, 0x2c, - 0x9a, 0x41, 0x5b, 0xbc, 0x08, 0x90, 0xab, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x03, 0x0c, - 0xc1, 0x63, 0xa7, 0x32, 0xf9, 0x0c, 0x1b, 0x4d, 0xb1, 0x6b, 0x2e, 0x0c, 0x3e, 0x50, 0x95, 0x85, 0xe4, 0xc5, 0x3a, - 0xd9, 0x5e, 0x9c, 0xc3, 0xf3, 0xeb, 0xb8, 0x00, 0xea, 0x20, 0xfa, 0x9a, 0xca, 0x62, 0x03, 0xb9, 0xb8, 0x29, 0x6b, - 0xbd, 0xa2, 0xd1, 0xe8, 0xc6, 0x2e, 0xbc, 0xae, 0xc0, 0x27, 0x51, 0x3a, 0x4a, 0xc4, 0x24, 0x66, 0x52, 0xe5, 0x8a, - 0x5c, 0x1b, 0xdd, 0x4b, 0x5b, 0x34, 0x2f, 0x85, 0x04, 0xaf, 0x08, 0xdc, 0x10, 0xfa, 0x4a, 0x5f, 0xae, 0x36, 0x50, - 0xf0, 0xa8, 0xbd, 0xb9, 0x08, 0x26, 0x26, 0x1e, 0x37, 0xa4, 0xa6, 0x5f, 0x87, 0x53, 0x2b, 0x8b, 0x25, 0x87, 0x5f, - 0xe7, 0x8c, 0x35, 0x14, 0x00, 0xf1, 0xc9, 0xa3, 0xf5, 0x6e, 0xd2, 0x1b, 0xa5, 0x1d, 0x94, 0x46, 0x88, 0xef, 0x2a, - 0x7c, 0xdd, 0x85, 0xe2, 0x2b, 0x57, 0xdd, 0xfb, 0x3a, 0x66, 0xc6, 0x05, 0xa3, 0x97, 0x7c, 0x9a, 0x34, 0xae, 0xdd, - 0xd0, 0x5d, 0x9d, 0xef, 0xbd, 0x2f, 0x65, 0xde, 0xc2, 0x31, 0xb0, 0xc9, 0x31, 0x73, 0x5e, 0x7a, 0x6f, 0x8d, 0x13, - 0xe5, 0x1f, 0xcc, 0x23, 0x5e, 0x39, 0xcc, 0xaa, 0x93, 0xe4, 0x1f, 0x06, 0x3f, 0x04, 0xeb, 0x5b, 0x1a, 0x27, 0xc8, - 0x5d, 0x75, 0x82, 0x4c, 0x94, 0xdb, 0xd0, 0x1b, 0x6e, 0xef, 0xae, 0x02, 0x41, 0x9c, 0x8a, 0xe9, 0xa3, 0x72, 0x5c, - 0x3f, 0x5a, 0xa0, 0x52, 0x11, 0xf1, 0xb9, 0xca, 0x5d, 0x59, 0x9b, 0x1a, 0xea, 0x31, 0x9d, 0xcc, 0x42, 0xd3, 0xac, - 0xc8, 0xa5, 0x6c, 0x7a, 0x8c, 0x5c, 0xb3, 0x53, 0x6d, 0x7e, 0x77, 0xed, 0x21, 0x1d, 0xc7, 0xfb, 0x9e, 0xb5, 0x5a, - 0x70, 0xbf, 0xab, 0x28, 0xbc, 0xeb, 0xc5, 0x46, 0x2a, 0x43, 0xcd, 0x7a, 0x14, 0x7d, 0x1c, 0xb7, 0x99, 0xcb, 0xa3, - 0xec, 0xcf, 0x1a, 0x00, 0xa6, 0x23, 0x2c, 0xba, 0x9b, 0x9e, 0xb1, 0x27, 0xd0, 0xd3, 0x13, 0x19, 0x24, 0x7a, 0xa3, - 0xf3, 0x55, 0xab, 0xc4, 0xd2, 0x15, 0x04, 0x76, 0x6f, 0xc8, 0x58, 0x95, 0xb4, 0x5b, 0xae, 0x5f, 0xce, 0xf3, 0x79, - 0xce, 0x97, 0xf2, 0x7c, 0x6a, 0x16, 0xdd, 0xbd, 0xb6, 0x7b, 0x73, 0x6a, 0xa8, 0x98, 0x6b, 0x75, 0x93, 0xdf, 0x30, - 0x5d, 0x07, 0x43, 0x2d, 0x82, 0xcc, 0x6a, 0x57, 0xbd, 0x28, 0xcb, 0x8d, 0x7a, 0x26, 0xc7, 0x86, 0xf0, 0x4d, 0xa5, - 0x3b, 0x44, 0x37, 0x4c, 0xd5, 0x4c, 0xdf, 0x37, 0xb6, 0x85, 0x6c, 0xf3, 0xf2, 0x6a, 0x94, 0x03, 0xa5, 0xe5, 0xfe, - 0x32, 0x61, 0xf8, 0xfe, 0xfa, 0xfa, 0x7b, 0x21, 0xa7, 0xaa, 0x8e, 0xde, 0xe2, 0xb5, 0xee, 0x19, 0x6c, 0x94, 0xca, - 0x89, 0xb8, 0x60, 0xab, 0x07, 0x6f, 0xee, 0x5e, 0x01, 0xcb, 0x05, 0xec, 0xda, 0x0b, 0xe6, 0x34, 0x86, 0xaa, 0x36, - 0xf0, 0x97, 0xab, 0x07, 0x5b, 0xb5, 0x87, 0xbf, 0x1c, 0x7c, 0x19, 0xdc, 0xd8, 0xd8, 0xd8, 0xc6, 0xdb, 0xb5, 0x44, - 0x90, 0x37, 0x78, 0xa0, 0x8f, 0x57, 0x1f, 0x05, 0x2d, 0x57, 0x88, 0x6d, 0x36, 0x70, 0x28, 0x6c, 0x0d, 0xf2, 0x4d, - 0xca, 0xa4, 0xe1, 0xbc, 0xe0, 0xd9, 0x54, 0xce, 0x50, 0xc8, 0x6b, 0x3e, 0x0e, 0xda, 0x8e, 0xf0, 0xbf, 0xc0, 0xa9, - 0x1d, 0x2f, 0x2f, 0x3e, 0x41, 0x1f, 0xf0, 0x74, 0xa5, 0x34, 0xa5, 0x38, 0xa5, 0x0a, 0xea, 0x2c, 0xd7, 0x79, 0x30, - 0x52, 0x5c, 0x4c, 0x60, 0x71, 0xc1, 0x65, 0xb9, 0x71, 0x36, 0x72, 0xfa, 0x4b, 0xbc, 0xba, 0x48, 0x97, 0x8f, 0x44, - 0xb6, 0x6a, 0xe9, 0xfd, 0xac, 0x4f, 0xb7, 0xed, 0x29, 0xe3, 0x93, 0x6c, 0x44, 0x07, 0x33, 0x3e, 0x4e, 0x84, 0xd7, - 0x27, 0x46, 0xfa, 0x6e, 0x11, 0x98, 0x6e, 0x8e, 0x4d, 0x7e, 0x38, 0x5e, 0x6f, 0x36, 0x6b, 0xdc, 0xc1, 0x3b, 0xe7, - 0x93, 0xb3, 0x28, 0x31, 0xa2, 0xb2, 0xd0, 0xf0, 0x80, 0x56, 0x88, 0x9b, 0xf7, 0x4c, 0x60, 0x5c, 0x76, 0x45, 0x52, - 0xdb, 0x0d, 0x04, 0x2e, 0xf6, 0x38, 0x66, 0xc9, 0xc8, 0xf6, 0xa0, 0x3c, 0xd0, 0x17, 0xa3, 0xe9, 0x16, 0x30, 0x2d, - 0xaf, 0x9d, 0x5d, 0xa4, 0xb6, 0x57, 0x4d, 0x15, 0xc0, 0x2c, 0x59, 0x1e, 0x9f, 0x21, 0xeb, 0x7e, 0x0d, 0x5d, 0xc4, - 0x80, 0xb1, 0x71, 0x65, 0xce, 0x5d, 0xac, 0x5a, 0x11, 0xdf, 0x68, 0x22, 0x4d, 0xea, 0x43, 0xea, 0x7b, 0x14, 0xd6, - 0xea, 0x2a, 0x07, 0x09, 0xdc, 0x23, 0xef, 0x8e, 0xb8, 0xf4, 0xf4, 0x99, 0xc5, 0xb8, 0x4a, 0xdf, 0x52, 0xd7, 0xe2, - 0x9a, 0x61, 0xaf, 0x78, 0x00, 0xf6, 0x07, 0xc6, 0x2d, 0x62, 0x11, 0x6f, 0x67, 0xb5, 0x14, 0xd6, 0xc6, 0x1c, 0x68, - 0x6e, 0xb8, 0xc1, 0xcf, 0xac, 0x5a, 0x33, 0x30, 0xc3, 0x8c, 0x33, 0x92, 0x0f, 0xc6, 0xbd, 0xaa, 0xb1, 0x23, 0x57, - 0x01, 0x44, 0xdf, 0x82, 0x2e, 0xc9, 0xe1, 0x95, 0x2c, 0x57, 0x9d, 0x21, 0xbf, 0x82, 0x75, 0xd6, 0x8b, 0x13, 0x70, - 0x93, 0xa6, 0xac, 0xc4, 0xc4, 0x14, 0x71, 0xb9, 0x59, 0xc6, 0x3c, 0x4d, 0x9f, 0x45, 0x3b, 0x38, 0xb9, 0x91, 0xc0, - 0x11, 0xfb, 0xc6, 0x32, 0x34, 0x13, 0x36, 0x62, 0x22, 0x8d, 0x4a, 0x29, 0x61, 0x03, 0xb9, 0xd4, 0x92, 0xbf, 0xcc, - 0xe5, 0xd5, 0x97, 0xdb, 0x04, 0x07, 0xe4, 0x35, 0xb0, 0x1c, 0x1a, 0xc7, 0x2d, 0x03, 0x89, 0x58, 0x0c, 0x88, 0x51, - 0xab, 0x72, 0x39, 0x19, 0xd5, 0xc9, 0x7c, 0x85, 0x5c, 0xa8, 0xc8, 0x83, 0x5b, 0x02, 0x2f, 0x54, 0xe4, 0x98, 0x3a, - 0x98, 0x95, 0xda, 0x4d, 0x8b, 0x4d, 0x92, 0xf7, 0xcc, 0x80, 0xe4, 0xea, 0x6b, 0x78, 0x68, 0xfc, 0x32, 0xbc, 0xa1, - 0xe8, 0xe9, 0x18, 0x21, 0xa7, 0xa5, 0x31, 0x97, 0xfe, 0x5b, 0x79, 0x9f, 0x56, 0x02, 0xf6, 0x0a, 0xc4, 0x94, 0x81, - 0x4b, 0x6c, 0x5c, 0x90, 0x94, 0xd7, 0xf2, 0x94, 0xdd, 0xd7, 0x50, 0xbe, 0x4b, 0x26, 0x5d, 0xa5, 0xb2, 0xd6, 0x58, - 0x75, 0x3f, 0xcf, 0x59, 0x7e, 0xb5, 0xcf, 0x30, 0x37, 0x19, 0x0d, 0xb2, 0x25, 0x33, 0x9b, 0xf2, 0xab, 0xbd, 0x1b, - 0xbf, 0xf2, 0x50, 0xd2, 0xa1, 0x5a, 0xa5, 0x9b, 0x97, 0x6e, 0x38, 0xc6, 0x8d, 0x1b, 0x8e, 0x00, 0x36, 0x86, 0x9d, - 0x2a, 0x52, 0xeb, 0xfc, 0xf7, 0xa5, 0xf0, 0x93, 0xd8, 0x6b, 0x47, 0x7a, 0xd7, 0x1d, 0xad, 0x4c, 0x4f, 0xbf, 0x01, - 0x55, 0x23, 0x4b, 0xe8, 0x26, 0x54, 0x31, 0x19, 0x89, 0x12, 0xd3, 0x55, 0xca, 0xa3, 0xbe, 0x46, 0x9c, 0x83, 0xb8, - 0xa1, 0xfc, 0xc5, 0x3f, 0x85, 0x57, 0x27, 0x01, 0x1a, 0x51, 0x8b, 0x71, 0x96, 0xf2, 0xd6, 0x38, 0x9a, 0xc6, 0xc9, - 0x55, 0x30, 0x8f, 0x5b, 0xd3, 0x2c, 0xcd, 0x8a, 0x19, 0x70, 0xa5, 0x57, 0x5c, 0x81, 0x0d, 0x3f, 0x6d, 0xcd, 0x63, - 0xef, 0x25, 0x4b, 0xce, 0x19, 0x8f, 0x87, 0x91, 0x67, 0xef, 0xe5, 0x20, 0x1e, 0xac, 0xb7, 0x51, 0x9e, 0x67, 0x17, - 0xb6, 0xf7, 0x21, 0x3b, 0x05, 0xa6, 0xf5, 0xde, 0x5d, 0x5e, 0x9d, 0xb1, 0xd4, 0xfb, 0x78, 0x3a, 0x4f, 0xf9, 0xdc, - 0x2b, 0xa2, 0xb4, 0x68, 0x15, 0x2c, 0x8f, 0xc7, 0xa0, 0x26, 0x92, 0x2c, 0x6f, 0x61, 0xfe, 0xf3, 0x94, 0x05, 0x49, - 0x7c, 0x36, 0xe1, 0xd6, 0x28, 0xca, 0x3f, 0xf5, 0x5a, 0xad, 0x59, 0x1e, 0x4f, 0xa3, 0xfc, 0xaa, 0x45, 0x2d, 0x82, - 0xcf, 0xda, 0xdb, 0xd1, 0xe7, 0xe3, 0x87, 0x3d, 0x9e, 0x43, 0xdf, 0x18, 0xa9, 0x18, 0x80, 0xf0, 0xb1, 0xb6, 0x77, - 0xda, 0xd3, 0xe2, 0x9e, 0x38, 0x51, 0x8a, 0x52, 0x5e, 0x9e, 0x78, 0x57, 0x0c, 0xe0, 0xf6, 0x4f, 0x79, 0xea, 0x81, - 0x2f, 0xc7, 0xb3, 0x74, 0x31, 0x9c, 0xe7, 0x05, 0x0c, 0x30, 0xcb, 0xe2, 0x94, 0xb3, 0xbc, 0x77, 0x9a, 0xe5, 0x40, - 0xb6, 0x56, 0x1e, 0x8d, 0xe2, 0x79, 0x11, 0x3c, 0x9c, 0x5d, 0xf6, 0xd0, 0x56, 0x38, 0xcb, 0xb3, 0x79, 0x3a, 0x92, - 0x73, 0xc5, 0x29, 0x6c, 0x8c, 0x98, 0x9b, 0x15, 0xf4, 0x25, 0x14, 0x80, 0x2f, 0x65, 0x51, 0xde, 0x3a, 0xc3, 0xce, - 0x68, 0xe8, 0xb7, 0x47, 0xec, 0xcc, 0xcb, 0xcf, 0x4e, 0x23, 0xa7, 0xd3, 0x7d, 0xec, 0xa9, 0x7f, 0xfe, 0x8e, 0x0b, - 0x86, 0xfb, 0xca, 0xe2, 0x4e, 0xbb, 0xfd, 0x37, 0x6e, 0xaf, 0x31, 0x0b, 0x01, 0x14, 0x74, 0x66, 0x97, 0x56, 0x91, - 0x25, 0xb0, 0x3e, 0xab, 0x7a, 0xf6, 0x66, 0xe0, 0x37, 0xc5, 0xe9, 0x59, 0xd0, 0x9d, 0x5d, 0x96, 0x88, 0x5d, 0x20, - 0x12, 0x32, 0x25, 0x92, 0xf2, 0x6d, 0xf1, 0x5b, 0x21, 0x7e, 0xb2, 0x1a, 0xe2, 0xae, 0x82, 0xb8, 0xa2, 0x7a, 0x6b, - 0x04, 0xfb, 0x80, 0xc8, 0xdf, 0x29, 0x04, 0x20, 0x13, 0x70, 0x02, 0x73, 0x05, 0x07, 0xbd, 0xfc, 0x66, 0x30, 0xba, - 0xab, 0xc1, 0x78, 0x72, 0x1b, 0x18, 0x79, 0x3a, 0x5a, 0xd4, 0xd7, 0xb5, 0x03, 0xce, 0x69, 0x6f, 0xc2, 0x90, 0x9f, - 0x82, 0x2e, 0x3e, 0x5f, 0xc4, 0x23, 0x3e, 0x11, 0x8f, 0xc4, 0xce, 0x17, 0xa2, 0x6e, 0xa7, 0xdd, 0x16, 0xef, 0x05, - 0x28, 0xb4, 0xa0, 0xe3, 0x63, 0x03, 0x60, 0xa2, 0x2f, 0xd6, 0x7d, 0xc4, 0xe6, 0xbb, 0x5b, 0xbf, 0x54, 0xe3, 0x31, - 0x95, 0x37, 0x28, 0x54, 0x84, 0xfa, 0x66, 0x0b, 0x66, 0xbc, 0xe5, 0xfd, 0x8e, 0x3e, 0xa8, 0x1a, 0x7c, 0xc7, 0x48, - 0xeb, 0x05, 0xcc, 0x33, 0x73, 0x81, 0x7a, 0x69, 0x1f, 0x43, 0x52, 0xad, 0x96, 0x0b, 0x7a, 0x83, 0x63, 0x08, 0x89, - 0x0e, 0x04, 0x9d, 0x7c, 0x50, 0xd0, 0x37, 0x35, 0x32, 0x37, 0x28, 0x9c, 0xcc, 0x85, 0x2d, 0x9f, 0x69, 0xb9, 0x0e, - 0x4a, 0x1a, 0xbc, 0xec, 0x2f, 0x98, 0x6c, 0x00, 0xd2, 0xbb, 0x92, 0xb4, 0xbc, 0x3a, 0x7a, 0x52, 0x2e, 0x5f, 0x36, - 0x24, 0xca, 0x81, 0xaf, 0xcf, 0x27, 0xe8, 0x77, 0xeb, 0xab, 0xeb, 0x46, 0x4a, 0xcd, 0x96, 0xed, 0x0e, 0xb8, 0xce, - 0xca, 0xc2, 0xec, 0x33, 0x5e, 0xe2, 0x28, 0x5f, 0x81, 0x9c, 0xc5, 0xd0, 0xeb, 0xcf, 0xa1, 0x70, 0xd3, 0x94, 0x93, - 0xb6, 0x71, 0xd3, 0xf5, 0x7f, 0x58, 0xf1, 0x98, 0xb2, 0x9d, 0x55, 0x6c, 0x1c, 0x5c, 0x97, 0xe3, 0xa1, 0xb8, 0x76, - 0x58, 0x60, 0xb6, 0xf8, 0x6f, 0xf7, 0x24, 0x1c, 0x8d, 0x56, 0x91, 0xcd, 0xf3, 0x21, 0x26, 0xfd, 0xaf, 0x08, 0x31, - 0xd8, 0xa4, 0xe1, 0x6d, 0x8f, 0x6b, 0xc5, 0xc2, 0x30, 0x7f, 0xc2, 0xfc, 0xaa, 0x02, 0xa3, 0x53, 0x17, 0x71, 0xa9, - 0x41, 0x86, 0x55, 0x14, 0xd8, 0xa8, 0x2b, 0x47, 0x94, 0x60, 0x47, 0x17, 0x3e, 0xfd, 0x79, 0x1a, 0x83, 0x68, 0x3d, - 0x8e, 0x47, 0x74, 0xd1, 0x25, 0x1e, 0xd1, 0xc9, 0x47, 0x8b, 0x32, 0x9d, 0x30, 0x94, 0x0e, 0x05, 0x92, 0xe0, 0xf8, - 0x2c, 0x33, 0x67, 0xec, 0x96, 0x8d, 0xa7, 0x17, 0x86, 0x6e, 0x1e, 0x65, 0xd3, 0x28, 0x4e, 0x03, 0xfc, 0x20, 0x89, - 0xa7, 0x47, 0x0c, 0xb0, 0x8b, 0x07, 0x7f, 0x15, 0xed, 0x3b, 0xae, 0xff, 0x13, 0x08, 0x2e, 0xea, 0x5f, 0x4a, 0xc7, - 0x4f, 0xc3, 0xa5, 0xce, 0x95, 0xeb, 0xa5, 0x20, 0xec, 0xb8, 0xce, 0x6d, 0xa7, 0xc0, 0xca, 0x2e, 0xa3, 0x3f, 0x83, - 0x56, 0x27, 0xe8, 0xb8, 0xcb, 0x2b, 0x60, 0x5c, 0x0c, 0xa8, 0x56, 0x85, 0x4a, 0xe4, 0x1b, 0xcc, 0x21, 0xf9, 0xf3, - 0xfa, 0x5a, 0x7f, 0x3c, 0xa0, 0x71, 0x81, 0x56, 0xa4, 0xdf, 0xc8, 0x4b, 0x98, 0x84, 0x85, 0x7e, 0x16, 0x98, 0x56, - 0xef, 0x1a, 0x5b, 0x4f, 0x6e, 0x25, 0x8c, 0x39, 0x9d, 0xa5, 0x4e, 0x0d, 0x0d, 0x3a, 0xbe, 0x58, 0x33, 0x95, 0x5b, - 0x46, 0xc4, 0xdc, 0x4f, 0x49, 0xe6, 0xd4, 0xaf, 0x3f, 0xc5, 0x18, 0xb8, 0xaf, 0x65, 0x6d, 0x29, 0xf6, 0x1e, 0x9e, - 0xec, 0x0a, 0x21, 0x65, 0x11, 0xeb, 0x86, 0x36, 0x48, 0x0d, 0xdb, 0xfa, 0xe3, 0x10, 0xe8, 0xfc, 0x29, 0xb4, 0x37, - 0x16, 0x8e, 0xba, 0x0b, 0x90, 0xc3, 0x5c, 0x7b, 0x42, 0x51, 0xd3, 0x47, 0x04, 0xec, 0xfe, 0xc6, 0x82, 0x95, 0xbb, - 0x5b, 0xa2, 0x77, 0xff, 0xa4, 0x2c, 0x48, 0xa7, 0x9a, 0xb1, 0xbf, 0x6a, 0x0a, 0x51, 0x07, 0xc3, 0x52, 0xc6, 0x31, - 0x8e, 0x9b, 0x6b, 0x3b, 0x51, 0x04, 0xb9, 0x25, 0xe3, 0x16, 0x98, 0x61, 0x15, 0xe5, 0x20, 0x46, 0x74, 0x0e, 0x4d, - 0x21, 0xd2, 0x46, 0x7a, 0xcb, 0x50, 0x9c, 0x20, 0x04, 0x83, 0x8d, 0x45, 0x5c, 0x86, 0xf0, 0x94, 0x0e, 0xb3, 0x11, - 0xfb, 0xf8, 0xe1, 0x15, 0x5e, 0x93, 0xc8, 0x52, 0x94, 0xa7, 0x99, 0x5b, 0x9e, 0x80, 0x81, 0x85, 0x90, 0xe6, 0xea, - 0x2b, 0x35, 0x00, 0x8c, 0x88, 0x15, 0x59, 0x34, 0x2a, 0x82, 0xc2, 0x4b, 0xdb, 0x1a, 0x08, 0x08, 0xc1, 0x91, 0xc5, - 0x02, 0x30, 0x41, 0xa9, 0x17, 0x07, 0xfc, 0x44, 0xeb, 0x3e, 0x0c, 0xb4, 0xbb, 0x25, 0x1a, 0x01, 0xae, 0x39, 0xa2, - 0x51, 0xa1, 0x8a, 0x59, 0x45, 0x26, 0xba, 0xa3, 0xf8, 0x5c, 0x93, 0x93, 0x52, 0xac, 0xfb, 0xbb, 0x49, 0x74, 0xca, - 0x12, 0x18, 0x12, 0xf8, 0xaa, 0x0d, 0x23, 0x89, 0x57, 0x6b, 0x37, 0x4e, 0x67, 0x73, 0xf9, 0xb5, 0x30, 0x98, 0xb8, - 0x83, 0x07, 0xb8, 0x78, 0x99, 0x61, 0xa0, 0x4e, 0x24, 0x03, 0x39, 0x00, 0x80, 0x48, 0x87, 0x21, 0x08, 0x5d, 0xc5, - 0x2a, 0x50, 0x1a, 0x8f, 0x96, 0xcb, 0x60, 0x7f, 0xcf, 0xb0, 0x34, 0x85, 0xe7, 0x69, 0x9c, 0xe2, 0x63, 0x81, 0x8f, - 0xd1, 0x25, 0x3e, 0x66, 0xf0, 0xa8, 0x71, 0xcf, 0x4b, 0xfb, 0xaf, 0xba, 0x2a, 0x99, 0x5c, 0x01, 0x4b, 0x13, 0x20, - 0xbb, 0xbe, 0x06, 0xb5, 0xa5, 0x49, 0xb0, 0xbb, 0x05, 0xc4, 0x42, 0xee, 0x11, 0xdf, 0x8e, 0xe1, 0x26, 0x19, 0x59, - 0x31, 0x6b, 0x89, 0x72, 0x8b, 0x8c, 0x83, 0x10, 0x7c, 0xc7, 0xdc, 0x69, 0xd8, 0x40, 0x9e, 0xcc, 0x92, 0x79, 0x86, - 0x2f, 0xae, 0x6d, 0x89, 0x8f, 0x7b, 0x08, 0xa2, 0xd0, 0x23, 0x62, 0xa8, 0xcb, 0x98, 0xfc, 0x6c, 0x4f, 0x1c, 0xda, - 0x38, 0x0b, 0x98, 0xa1, 0xe8, 0x85, 0xf2, 0x28, 0x4e, 0x44, 0xe3, 0x15, 0xf8, 0x34, 0xd2, 0x1d, 0x09, 0x9d, 0xdd, - 0xad, 0x0a, 0x36, 0x00, 0x5e, 0x49, 0x04, 0x4e, 0x19, 0x37, 0xb6, 0x28, 0xa7, 0x14, 0x00, 0xb9, 0xcd, 0xab, 0x4f, - 0x3a, 0x01, 0x53, 0x80, 0x11, 0x3d, 0x3a, 0xa6, 0xd9, 0x06, 0x43, 0x20, 0x16, 0xcd, 0xd8, 0xd8, 0xba, 0xf6, 0x5f, - 0xfe, 0xf9, 0x1f, 0x6c, 0x4f, 0x80, 0x98, 0x8d, 0xc7, 0x20, 0xe5, 0xac, 0x75, 0x0d, 0xff, 0xd7, 0x3f, 0xfe, 0xdf, - 0xff, 0xf3, 0x5f, 0x75, 0xdb, 0x14, 0x9a, 0x9e, 0x04, 0xe2, 0x68, 0x41, 0x93, 0x94, 0x52, 0x3c, 0xed, 0x71, 0x94, - 0xae, 0x00, 0xe9, 0x10, 0xb3, 0x18, 0x19, 0x1b, 0x79, 0xb6, 0x05, 0x9a, 0x40, 0x3c, 0x1f, 0x27, 0xec, 0x9c, 0xc9, - 0x0f, 0xcb, 0xe8, 0x41, 0x74, 0xe5, 0x10, 0x2c, 0x18, 0x2e, 0xef, 0xbc, 0xca, 0x6d, 0xa0, 0x68, 0x29, 0x29, 0x5e, - 0x27, 0x98, 0x67, 0x1b, 0x83, 0x36, 0xe7, 0x68, 0xd7, 0x87, 0xf5, 0x40, 0xa5, 0xda, 0xb6, 0x80, 0x97, 0xcc, 0xde, - 0x95, 0x10, 0x37, 0xe1, 0x3a, 0xcd, 0xb1, 0x69, 0xca, 0x8a, 0x62, 0x15, 0x58, 0x40, 0x13, 0xcf, 0xae, 0x9a, 0xd8, - 0xb5, 0x0e, 0x00, 0x40, 0x77, 0x67, 0x47, 0x4c, 0x0b, 0x15, 0x6c, 0x3c, 0x86, 0x0d, 0x8e, 0xba, 0x2d, 0xe1, 0x18, - 0x84, 0x0f, 0xfb, 0xf6, 0x5b, 0x90, 0x25, 0x78, 0xa7, 0xc5, 0xd5, 0x9f, 0xf4, 0xa2, 0xe9, 0x95, 0xb0, 0x33, 0xe6, - 0x10, 0x9d, 0x8d, 0x61, 0xf4, 0x93, 0x81, 0x54, 0x36, 0xfc, 0xb4, 0x8a, 0x31, 0xd6, 0x32, 0xc2, 0xbf, 0xff, 0xcb, - 0x3f, 0xfe, 0x37, 0x18, 0x9b, 0xfa, 0xad, 0xe7, 0x02, 0x68, 0xf5, 0x3f, 0xa1, 0xd5, 0x3c, 0xbd, 0xa5, 0xdd, 0x5f, - 0xfe, 0xfe, 0xbf, 0x43, 0x33, 0xba, 0x28, 0x05, 0x7c, 0x42, 0x10, 0x0d, 0xd1, 0x36, 0xfd, 0x55, 0x20, 0xd5, 0x06, - 0x59, 0x3b, 0xd3, 0x3f, 0x21, 0xd8, 0x05, 0xcf, 0x66, 0x37, 0x82, 0x83, 0x50, 0x0f, 0x93, 0xac, 0x60, 0x1a, 0x1e, - 0xa1, 0x4f, 0x7e, 0x1d, 0x40, 0x34, 0xd7, 0x0c, 0x76, 0x6d, 0x61, 0xe9, 0x71, 0xc4, 0x0a, 0xad, 0xdc, 0x84, 0xf5, - 0x05, 0x2c, 0x18, 0x27, 0x74, 0x28, 0xdc, 0x03, 0x4b, 0x26, 0x9e, 0xe0, 0x81, 0x04, 0x9c, 0x5b, 0xff, 0xf8, 0xda, - 0xea, 0xc1, 0x34, 0xc3, 0x89, 0xb1, 0x44, 0x84, 0x4b, 0x8d, 0x00, 0x7f, 0x41, 0x08, 0x1f, 0xeb, 0xe7, 0xe8, 0x52, - 0x3f, 0xa3, 0xa0, 0x16, 0x13, 0x80, 0xbe, 0x9d, 0xa2, 0x31, 0x66, 0xce, 0x20, 0xb2, 0x33, 0x2a, 0xf7, 0xde, 0x48, - 0xf2, 0x11, 0xc2, 0xf8, 0x18, 0x73, 0x61, 0xf1, 0xe6, 0xd3, 0x3c, 0x67, 0xc7, 0x49, 0x76, 0x81, 0x31, 0x43, 0x22, - 0xd2, 0x9a, 0xfa, 0xf2, 0xdf, 0xfe, 0xd5, 0xf7, 0xff, 0xed, 0x5f, 0xd7, 0x34, 0x98, 0xc0, 0x9e, 0x00, 0x23, 0x9f, - 0x85, 0x9a, 0xce, 0x0d, 0xb4, 0x56, 0x0f, 0x8a, 0x78, 0xae, 0xae, 0x91, 0x88, 0x63, 0xa9, 0xc4, 0x5b, 0x3e, 0x12, - 0xda, 0x9a, 0x29, 0x6e, 0x9f, 0x05, 0x21, 0x5b, 0x33, 0x0d, 0x56, 0xdd, 0x32, 0xcf, 0x89, 0x1b, 0xdc, 0x40, 0x97, - 0x5f, 0x89, 0xf1, 0x6a, 0x30, 0x6e, 0x85, 0xc0, 0x03, 0x6d, 0x26, 0xf4, 0xdd, 0x33, 0xa1, 0xad, 0x02, 0xb1, 0x0c, - 0x52, 0x77, 0xd5, 0x00, 0xf2, 0xac, 0x03, 0x9a, 0x80, 0x9a, 0xc4, 0x95, 0xad, 0x40, 0xe6, 0xd6, 0x69, 0xde, 0x7f, - 0x83, 0x97, 0x1d, 0x91, 0x78, 0x64, 0x29, 0x14, 0x64, 0xd8, 0x30, 0x32, 0x6c, 0xa4, 0x46, 0x35, 0x6d, 0x0a, 0x74, - 0xfc, 0xb2, 0xd5, 0xb6, 0xc3, 0x31, 0x76, 0xaf, 0x69, 0x7f, 0x26, 0xb5, 0x7f, 0x2c, 0xed, 0x7d, 0xa9, 0xfd, 0xf1, - 0x93, 0x36, 0x0d, 0xed, 0x1f, 0xaf, 0xd5, 0xfe, 0x48, 0xb9, 0x01, 0x8e, 0x1c, 0xda, 0x9b, 0x18, 0xdd, 0x32, 0x6c, - 0x0d, 0xd4, 0xc4, 0x83, 0xe1, 0x84, 0x0d, 0x3f, 0x49, 0x33, 0x8b, 0x10, 0xc0, 0x40, 0x94, 0x36, 0x26, 0x05, 0x06, - 0x60, 0x32, 0x9c, 0x94, 0x7a, 0xd3, 0xe3, 0xa3, 0x31, 0x01, 0x73, 0x17, 0x63, 0x86, 0xa2, 0x1f, 0xd6, 0xec, 0x2b, - 0x56, 0x6e, 0xe1, 0x38, 0x62, 0xc3, 0x88, 0x67, 0xc0, 0x6c, 0x0b, 0x07, 0x3b, 0xf1, 0x16, 0x22, 0x58, 0x18, 0xd8, - 0xef, 0xdf, 0xed, 0x1f, 0xd8, 0xde, 0x69, 0x36, 0xba, 0x0a, 0x6c, 0x70, 0xc6, 0xc0, 0x9a, 0x72, 0x7d, 0x3e, 0x61, - 0xa9, 0xa3, 0x3c, 0x9f, 0x2c, 0x61, 0xe0, 0x00, 0x9e, 0x89, 0x6f, 0x5b, 0x34, 0x0f, 0x3a, 0x80, 0xb0, 0xf4, 0xf1, - 0xcb, 0xfe, 0x2e, 0x17, 0xdf, 0x85, 0xe5, 0x39, 0x3e, 0xf6, 0x31, 0xd5, 0x63, 0x77, 0x0b, 0x1e, 0xf0, 0x65, 0x1f, - 0xf5, 0x1e, 0xbd, 0x6d, 0x2c, 0x96, 0xdc, 0x86, 0x01, 0x0e, 0x31, 0xe9, 0x0b, 0x14, 0x0a, 0x6a, 0x75, 0x12, 0x20, - 0x62, 0xf0, 0x08, 0x63, 0x6d, 0xa9, 0x71, 0x11, 0x42, 0xd5, 0x5f, 0x3b, 0x2e, 0x95, 0xdd, 0x4a, 0xf3, 0x8e, 0xb0, - 0x01, 0x39, 0x2e, 0xd8, 0x7b, 0xa4, 0x4b, 0x84, 0xa9, 0x43, 0x45, 0xeb, 0x20, 0xd0, 0x35, 0x95, 0xb9, 0x22, 0x3a, - 0x18, 0xc0, 0x90, 0x99, 0x2b, 0x00, 0x81, 0xbf, 0x84, 0xf6, 0x89, 0xf9, 0xfd, 0x37, 0xf1, 0xa9, 0x26, 0x4d, 0x9c, - 0xc3, 0x3f, 0x79, 0x57, 0xcc, 0xbb, 0x3a, 0xa1, 0x96, 0x2a, 0xd8, 0x80, 0x51, 0x30, 0x0c, 0xca, 0xb4, 0x55, 0x54, - 0x09, 0xec, 0xb4, 0x24, 0x9a, 0x15, 0x2c, 0x50, 0x0f, 0x32, 0xee, 0x80, 0xe1, 0x8b, 0xe5, 0x40, 0x8f, 0x69, 0xcf, - 0x95, 0x7c, 0xb2, 0x30, 0x03, 0x13, 0x8f, 0xda, 0xed, 0x1e, 0x5e, 0xaa, 0x68, 0x45, 0x60, 0x1d, 0xa4, 0x41, 0xc2, - 0xc6, 0xbc, 0xe4, 0x78, 0x6b, 0x7f, 0xa1, 0x22, 0x41, 0x7e, 0x77, 0x27, 0x67, 0x53, 0xcb, 0xc7, 0xff, 0xbf, 0x6d, - 0xec, 0x51, 0x90, 0xf2, 0x49, 0x8b, 0xae, 0xf1, 0xe0, 0x15, 0x49, 0x80, 0xc8, 0x7c, 0x5f, 0x18, 0x13, 0x0d, 0x19, - 0x46, 0xc9, 0x4a, 0x0e, 0xce, 0x37, 0x88, 0x9b, 0xdc, 0x6c, 0x07, 0x72, 0x7a, 0x29, 0x54, 0xb6, 0x1c, 0xac, 0xd9, - 0x76, 0xa5, 0x7f, 0xb4, 0xdc, 0x58, 0x45, 0xbc, 0xea, 0x6f, 0x4b, 0x14, 0x32, 0x62, 0x73, 0xa5, 0x50, 0x51, 0x0b, - 0xd1, 0xc3, 0xc4, 0x69, 0x39, 0x6a, 0x77, 0xab, 0xc5, 0x5c, 0x92, 0xb8, 0x38, 0x24, 0x71, 0x41, 0xe2, 0xef, 0x68, - 0x21, 0xe6, 0x1e, 0x46, 0xc9, 0xd0, 0x41, 0x00, 0xac, 0x96, 0xf5, 0x04, 0xa8, 0xe9, 0xaa, 0xc8, 0x91, 0xff, 0x18, - 0x89, 0x5b, 0x0a, 0x61, 0xb9, 0x82, 0x4a, 0x27, 0x47, 0x65, 0xd9, 0x63, 0xcc, 0x39, 0xfc, 0x20, 0x2f, 0x81, 0x88, - 0xbb, 0xbf, 0xfa, 0xfb, 0x89, 0xed, 0xd2, 0x3d, 0xf2, 0x7e, 0x36, 0x3e, 0x4a, 0x67, 0x2b, 0x66, 0xb7, 0x3d, 0x58, - 0x06, 0xb3, 0xa7, 0xfc, 0x84, 0xe4, 0x4d, 0x7d, 0x4d, 0x36, 0xa7, 0xfe, 0x3f, 0x87, 0x38, 0xc2, 0x1b, 0xc7, 0x46, - 0x13, 0x9d, 0x46, 0xbe, 0x6a, 0x11, 0x7f, 0xda, 0xd8, 0x55, 0x1c, 0x81, 0x7c, 0xbd, 0x2e, 0x92, 0xf5, 0xcd, 0xed, - 0x91, 0xac, 0xe2, 0x8e, 0x91, 0xac, 0x6f, 0x7e, 0xe7, 0x48, 0xd6, 0xd7, 0x66, 0x24, 0x0b, 0x05, 0xf4, 0xab, 0x5f, - 0x13, 0x6d, 0xca, 0xb3, 0x8b, 0x22, 0xec, 0xc8, 0xcc, 0x09, 0x90, 0x75, 0x18, 0x76, 0xfa, 0xeb, 0x47, 0x98, 0x60, - 0xa2, 0x46, 0x7c, 0x89, 0x02, 0x4a, 0x22, 0xd9, 0x13, 0xd4, 0x8a, 0x0c, 0xe7, 0xb4, 0x75, 0x56, 0x65, 0xeb, 0xa1, - 0xba, 0x46, 0x06, 0xae, 0xaf, 0xab, 0x43, 0x6d, 0x5d, 0x15, 0xf0, 0x09, 0xe8, 0x3b, 0xb0, 0xba, 0x63, 0x77, 0x53, - 0xa5, 0xf3, 0x99, 0x23, 0xf4, 0xd4, 0x29, 0x8d, 0x60, 0xa2, 0x85, 0xfd, 0x5f, 0x0e, 0x3b, 0xbd, 0xed, 0xce, 0x14, - 0x7a, 0x83, 0x02, 0x87, 0xb7, 0x76, 0x6f, 0x7b, 0x1b, 0xdf, 0x2e, 0xd4, 0x5b, 0x17, 0xdf, 0x62, 0xf5, 0xb6, 0x83, - 0x6f, 0x43, 0xf5, 0xf6, 0x08, 0xdf, 0x46, 0xea, 0xed, 0x31, 0xbe, 0x9d, 0xdb, 0xe5, 0x21, 0xd3, 0xc0, 0x3d, 0x06, - 0xbe, 0x22, 0x6f, 0x26, 0x50, 0x65, 0xb0, 0xe9, 0xf1, 0xc3, 0x08, 0xd1, 0x59, 0x10, 0x7b, 0xc2, 0xbb, 0x0c, 0x72, - 0xef, 0x02, 0x34, 0x4e, 0x40, 0xd9, 0x86, 0xcf, 0xf1, 0x3b, 0x1c, 0xe0, 0x24, 0x1d, 0xc4, 0x53, 0xa6, 0x3e, 0x48, - 0xac, 0xb0, 0x06, 0x03, 0xf6, 0xb0, 0x7d, 0x54, 0xf6, 0xf4, 0x3a, 0x89, 0x78, 0x96, 0xca, 0xe6, 0xa0, 0x95, 0xab, - 0xea, 0xc4, 0x74, 0x2d, 0xbd, 0xc2, 0x6b, 0xf4, 0x97, 0x11, 0x8f, 0x18, 0x83, 0x61, 0xd6, 0xba, 0x04, 0x0f, 0x76, - 0xa5, 0x4e, 0x43, 0x88, 0xb4, 0x4e, 0x23, 0x9c, 0xf4, 0xdb, 0x41, 0x74, 0xa6, 0x9f, 0xdf, 0x80, 0xa5, 0x1d, 0x9d, - 0xc9, 0x96, 0xeb, 0x75, 0x18, 0x81, 0x68, 0xea, 0x2f, 0x05, 0x04, 0x99, 0x62, 0xb0, 0x34, 0xe8, 0x49, 0x4b, 0xfd, - 0x85, 0xd4, 0xa9, 0x6b, 0x34, 0x9a, 0xbe, 0x5e, 0x04, 0x14, 0xad, 0x0a, 0x76, 0xc1, 0xe0, 0xa7, 0x52, 0x41, 0x61, - 0xa8, 0xc0, 0x02, 0x51, 0xbd, 0x46, 0x95, 0xe9, 0x60, 0xc3, 0x5a, 0x85, 0x66, 0x29, 0x5d, 0x66, 0x9e, 0xee, 0xe8, - 0xa3, 0x9d, 0x65, 0xf1, 0xfa, 0x59, 0x67, 0x88, 0xff, 0x49, 0xe1, 0xfd, 0xd9, 0x78, 0x3c, 0xbe, 0x51, 0xb7, 0x7d, - 0x36, 0x1a, 0xb3, 0x2e, 0xdb, 0xe9, 0x61, 0xe4, 0xbf, 0x25, 0xc5, 0x69, 0xa7, 0x24, 0xda, 0x2d, 0xee, 0xd6, 0x18, - 0x25, 0x2f, 0xa8, 0xbb, 0xbb, 0x2b, 0xc1, 0x12, 0xa8, 0xb2, 0x00, 0xe1, 0x7f, 0x16, 0xa7, 0x41, 0xbb, 0xf4, 0xcf, - 0xa5, 0xd6, 0xf8, 0xec, 0xc9, 0x93, 0x27, 0xa5, 0x3f, 0x52, 0x6f, 0xed, 0xd1, 0xa8, 0xf4, 0x87, 0x0b, 0x8d, 0x46, - 0xbb, 0x3d, 0x1e, 0x97, 0x7e, 0xac, 0x0a, 0xb6, 0xbb, 0xc3, 0xd1, 0x76, 0xb7, 0xf4, 0x2f, 0x8c, 0x16, 0xa5, 0xcf, - 0xe4, 0x5b, 0xce, 0x46, 0xb5, 0xe3, 0x83, 0xc7, 0x6d, 0xa8, 0x14, 0x8c, 0xb6, 0x40, 0xef, 0x52, 0x3c, 0x06, 0xd1, - 0x9c, 0x67, 0x60, 0xd8, 0x95, 0xbd, 0x02, 0xe4, 0xf3, 0x58, 0x4a, 0x78, 0xf1, 0xbd, 0x5f, 0x94, 0xea, 0xaf, 0x4c, - 0xa9, 0x8e, 0xcc, 0x4c, 0xd2, 0xbc, 0x20, 0x6d, 0xd0, 0xac, 0x46, 0xce, 0xa2, 0xea, 0x57, 0x61, 0x51, 0x09, 0x7b, - 0x94, 0x36, 0xd8, 0x52, 0xc8, 0xf8, 0x1f, 0xd6, 0xc9, 0xf8, 0xef, 0x6f, 0x97, 0xf1, 0xa7, 0x77, 0x13, 0xf1, 0xdf, - 0xff, 0xce, 0x22, 0xfe, 0x07, 0x53, 0xc4, 0x0b, 0x21, 0xb6, 0x07, 0xa6, 0x33, 0xd9, 0xcc, 0xa7, 0xd9, 0x65, 0x0b, - 0xb7, 0x44, 0x6e, 0x93, 0xf4, 0x9c, 0xde, 0x49, 0xf8, 0xaf, 0xc8, 0x07, 0x53, 0x83, 0x19, 0x1f, 0x0f, 0xe6, 0xd9, - 0xd9, 0x59, 0xc2, 0x94, 0x8c, 0x37, 0x2a, 0xc8, 0x1c, 0x7f, 0x97, 0x86, 0xf6, 0x3b, 0xf4, 0x8c, 0xab, 0x92, 0xf1, - 0x18, 0x8a, 0xc6, 0x63, 0x5b, 0xe5, 0x4b, 0x83, 0x3c, 0xa3, 0x56, 0x6f, 0x6b, 0x25, 0xd4, 0xea, 0x8b, 0x2f, 0xcc, - 0x32, 0xb3, 0x40, 0x86, 0xf4, 0x4c, 0x63, 0x44, 0xd6, 0x8c, 0xe2, 0x02, 0xf7, 0x60, 0xf5, 0xb1, 0x63, 0xb4, 0x77, - 0xa6, 0xa0, 0x54, 0xe2, 0x21, 0x9e, 0x8b, 0x34, 0x3f, 0x2c, 0x23, 0x72, 0xdb, 0x97, 0x91, 0xab, 0xce, 0xbf, 0x8d, - 0x6f, 0x18, 0x56, 0x67, 0xde, 0xb0, 0xf8, 0x32, 0xbf, 0xe5, 0xe9, 0xd5, 0xab, 0x91, 0xb3, 0x87, 0x97, 0x7f, 0x8b, - 0x77, 0x69, 0x23, 0x6f, 0x50, 0x80, 0x1d, 0x86, 0x26, 0xa6, 0xa5, 0x20, 0x58, 0x75, 0x81, 0xa2, 0xaa, 0xec, 0x19, - 0x9d, 0x64, 0x7a, 0x19, 0x0e, 0x39, 0xa8, 0x91, 0x25, 0x30, 0x07, 0x93, 0xba, 0x90, 0x3e, 0x66, 0x2f, 0x92, 0x6e, - 0xce, 0xe5, 0x57, 0xcf, 0xe9, 0x70, 0x66, 0x21, 0xf5, 0x87, 0x4c, 0xc7, 0xa8, 0x7a, 0xd2, 0x79, 0x08, 0xcd, 0x30, - 0x2a, 0xd5, 0x19, 0x08, 0x10, 0x6e, 0x86, 0x9f, 0x68, 0x12, 0x43, 0xa8, 0x83, 0x82, 0x8a, 0x7a, 0xd7, 0xd7, 0xe6, - 0x97, 0x42, 0x6b, 0x5f, 0x95, 0x6c, 0xf0, 0x00, 0xc7, 0x4f, 0xfc, 0xa2, 0x36, 0xc8, 0xe6, 0xdc, 0xc1, 0x33, 0x80, - 0x05, 0x1e, 0x31, 0x78, 0x3b, 0xed, 0x36, 0xa8, 0x18, 0x5f, 0x7c, 0x07, 0xca, 0xd1, 0x9d, 0x05, 0xbe, 0x6c, 0xdd, - 0xb9, 0xc4, 0xd2, 0x77, 0xd9, 0x2a, 0x12, 0xdf, 0xbf, 0x2f, 0x11, 0x35, 0xee, 0x0e, 0xa9, 0x45, 0x6c, 0xbe, 0xfb, - 0xca, 0x77, 0x34, 0x08, 0xeb, 0xae, 0xe2, 0x60, 0x99, 0x5b, 0x5b, 0x2f, 0xc4, 0xb6, 0xc2, 0xaa, 0x59, 0x06, 0xe7, - 0x16, 0x9d, 0x59, 0x5c, 0x18, 0x01, 0xfc, 0xda, 0x36, 0x28, 0x55, 0x04, 0x5f, 0x84, 0xe1, 0xf7, 0xd0, 0xc5, 0x15, - 0x8e, 0xb7, 0x02, 0xba, 0xe1, 0xf2, 0x56, 0x90, 0xa3, 0x33, 0xac, 0x19, 0x5d, 0x55, 0xa9, 0x82, 0xd2, 0x3c, 0x82, - 0x31, 0x90, 0xa1, 0x48, 0x3a, 0xac, 0x71, 0x2a, 0xf4, 0x16, 0x4c, 0x43, 0x02, 0x58, 0xfb, 0x75, 0xe8, 0xd6, 0xd8, - 0x0a, 0x6c, 0x21, 0x2d, 0x40, 0xe9, 0x61, 0x87, 0xbe, 0x55, 0x03, 0x3d, 0x5d, 0x0e, 0xc0, 0xdf, 0xe8, 0xe4, 0x9d, - 0xf8, 0xc5, 0x85, 0x07, 0xff, 0xac, 0x3f, 0x2c, 0x40, 0xca, 0x9f, 0x7e, 0x8a, 0x39, 0xd8, 0xd4, 0xb3, 0x16, 0x86, - 0x5f, 0x28, 0x4e, 0x2b, 0xd5, 0x21, 0x1d, 0x45, 0x8b, 0x2b, 0x63, 0xbd, 0x79, 0x81, 0xbe, 0x20, 0x39, 0x3d, 0x41, - 0x9a, 0xa5, 0xac, 0x57, 0x4f, 0x39, 0x30, 0xfd, 0x0e, 0x45, 0xac, 0xa3, 0x45, 0x86, 0xbe, 0x23, 0xbf, 0x02, 0xdf, - 0x51, 0xa8, 0xd1, 0xb6, 0x72, 0x3a, 0xda, 0x2b, 0xdb, 0x07, 0x92, 0xb6, 0x9b, 0x64, 0x2d, 0xe4, 0xcb, 0xce, 0xd5, - 0x3a, 0xe7, 0xe8, 0xb6, 0x03, 0x78, 0x0c, 0x0a, 0xab, 0x7f, 0x46, 0xe6, 0x42, 0xb3, 0x98, 0x0e, 0xe0, 0xef, 0x02, - 0x59, 0x10, 0x8d, 0xf1, 0x0b, 0x8b, 0x77, 0x69, 0x79, 0x4a, 0xd9, 0xaf, 0x0b, 0x54, 0xeb, 0x41, 0xe7, 0x09, 0x78, - 0x7b, 0x77, 0x1e, 0xfe, 0x66, 0xf4, 0x4b, 0x49, 0x23, 0x75, 0x89, 0xd9, 0xb6, 0x7b, 0x28, 0x2f, 0x92, 0xe8, 0x0a, - 0x9c, 0x4e, 0xb2, 0x31, 0x4e, 0x31, 0x7a, 0xdc, 0x9b, 0x65, 0x32, 0x93, 0x24, 0x67, 0x09, 0xfd, 0x8c, 0x89, 0x5c, - 0x8a, 0xed, 0x47, 0xb3, 0x4b, 0xb5, 0x1a, 0x9d, 0x46, 0x86, 0xc8, 0xef, 0x9a, 0x08, 0xb2, 0x3e, 0xf3, 0xa4, 0x9e, - 0xcc, 0xb0, 0x03, 0x30, 0x08, 0xc3, 0xa6, 0x95, 0x0b, 0xa8, 0xda, 0x50, 0x62, 0xa4, 0xc2, 0x54, 0x03, 0x59, 0xfe, - 0x36, 0xa8, 0xca, 0xa8, 0x60, 0x3d, 0xfc, 0xd4, 0x65, 0x0c, 0xae, 0xad, 0x34, 0x9e, 0xa6, 0xf1, 0x68, 0x94, 0xb0, - 0x9e, 0xb2, 0x8f, 0xac, 0xce, 0x23, 0xcc, 0x24, 0x31, 0x97, 0xac, 0xbe, 0x2a, 0x06, 0xf1, 0x34, 0x9d, 0xa2, 0x53, - 0xb0, 0xd7, 0xf0, 0x7b, 0x95, 0x2b, 0xc9, 0x29, 0x53, 0x2c, 0xda, 0x15, 0xf1, 0xe8, 0xb9, 0x8e, 0xcb, 0x0e, 0x18, - 0x8b, 0xb4, 0xe0, 0xed, 0x1e, 0xcf, 0x66, 0x41, 0x6b, 0xbb, 0x8e, 0x08, 0x56, 0x69, 0x14, 0xbc, 0x15, 0x68, 0x79, - 0x68, 0x1d, 0x08, 0x2d, 0x67, 0xf9, 0x1d, 0x59, 0x46, 0x03, 0xe0, 0x37, 0x11, 0x75, 0x51, 0x59, 0x47, 0xe6, 0xaf, - 0xb3, 0x5b, 0x3e, 0x5f, 0xbd, 0x5b, 0x3e, 0x57, 0xbb, 0xe5, 0x66, 0x8e, 0xfd, 0x6c, 0xdc, 0xc1, 0xff, 0x7a, 0x15, - 0x42, 0xb0, 0x2a, 0x40, 0x0e, 0x0b, 0xed, 0xe2, 0x56, 0x17, 0xfe, 0x8f, 0x86, 0x6e, 0x7b, 0xf8, 0x9f, 0x0f, 0x16, - 0x60, 0xdb, 0xc2, 0x42, 0xfc, 0xd7, 0xae, 0x55, 0x75, 0x1e, 0x62, 0x1d, 0xf6, 0xda, 0x59, 0xae, 0xeb, 0xde, 0xbc, - 0x69, 0x41, 0x5e, 0x71, 0x27, 0x50, 0xc2, 0x18, 0x5c, 0xb5, 0xe8, 0xf4, 0x14, 0x4a, 0xc7, 0xd9, 0x70, 0x5e, 0xfc, - 0xad, 0x84, 0x5f, 0x12, 0xf1, 0xc6, 0x2d, 0xdd, 0x18, 0x47, 0x75, 0x15, 0x69, 0x49, 0x6a, 0x84, 0x85, 0x5e, 0xa7, - 0xa0, 0x00, 0xc6, 0x64, 0x4e, 0xd7, 0x7f, 0xb8, 0x62, 0x13, 0xfc, 0x7f, 0x59, 0x9b, 0x95, 0xc8, 0xfc, 0x47, 0x89, - 0x71, 0x23, 0x11, 0x7e, 0x15, 0x0d, 0xcc, 0x35, 0x6c, 0x3f, 0x59, 0x0d, 0xee, 0xa1, 0x9a, 0xe9, 0x48, 0x29, 0x05, - 0xa9, 0x77, 0xc0, 0x0b, 0x88, 0xe6, 0x09, 0xbf, 0x79, 0xd4, 0x75, 0x9c, 0xb1, 0x34, 0xea, 0x0d, 0x02, 0xbd, 0x6a, - 0x7b, 0x47, 0x29, 0xfd, 0xd9, 0xe7, 0x0f, 0xf1, 0x3f, 0x11, 0x38, 0x3b, 0xad, 0x7c, 0x23, 0x11, 0x1b, 0x40, 0xdf, - 0x68, 0x5a, 0x73, 0x7e, 0x84, 0x06, 0x27, 0xff, 0xe7, 0xae, 0xad, 0xd1, 0x58, 0xbf, 0x53, 0x73, 0x69, 0x95, 0xfe, - 0xaa, 0xd6, 0xbf, 0x6e, 0xf0, 0x3b, 0xb6, 0x1d, 0x0a, 0x87, 0xa0, 0xde, 0x56, 0xc6, 0x03, 0x97, 0x1a, 0x2b, 0x8a, - 0xdf, 0xb5, 0x7d, 0x65, 0x12, 0x53, 0x8f, 0x69, 0x78, 0xaa, 0x9d, 0x48, 0x79, 0x78, 0x8f, 0x3d, 0x84, 0x1f, 0xf9, - 0x25, 0x0b, 0x1f, 0xe0, 0xd7, 0xd8, 0xac, 0xcb, 0x69, 0x92, 0x82, 0x59, 0x35, 0xe1, 0x7c, 0x16, 0x6c, 0x6d, 0x5d, - 0x5c, 0x5c, 0xf8, 0x17, 0xdb, 0x7e, 0x96, 0x9f, 0x6d, 0x75, 0xdb, 0xed, 0x36, 0x7e, 0x44, 0xcb, 0xb6, 0xce, 0x63, - 0x76, 0xf1, 0x14, 0xdc, 0x0f, 0xfb, 0xb1, 0xf5, 0xc4, 0x7a, 0xbc, 0x6d, 0xed, 0x3c, 0xb2, 0x2d, 0x52, 0x00, 0x50, - 0xb2, 0x6d, 0x5b, 0x42, 0x01, 0x84, 0x36, 0x14, 0xf7, 0x77, 0xcf, 0x94, 0x0d, 0x87, 0x97, 0x14, 0x84, 0x85, 0x04, - 0xfe, 0x5b, 0xf6, 0x89, 0xd5, 0xb7, 0xba, 0x28, 0x6b, 0x49, 0x35, 0xa2, 0x5e, 0x71, 0xbf, 0x0f, 0xa3, 0x59, 0x40, - 0x6c, 0x64, 0x16, 0x62, 0x98, 0x4c, 0x94, 0xd2, 0x14, 0x68, 0x97, 0x9e, 0xc2, 0x13, 0x66, 0xb5, 0x59, 0xf0, 0xfc, - 0xa6, 0xfb, 0x18, 0x74, 0xdc, 0x79, 0xeb, 0xe1, 0xb0, 0xdd, 0xea, 0x58, 0x9d, 0x56, 0xd7, 0x7f, 0x6c, 0x75, 0xc5, - 0xff, 0x83, 0x8c, 0xdc, 0xb6, 0x3a, 0xf0, 0xb4, 0x6d, 0xc1, 0xfb, 0xf9, 0x43, 0x91, 0x5b, 0x12, 0xd9, 0x5b, 0xfd, - 0x5d, 0xfc, 0x4d, 0x29, 0x40, 0xea, 0x73, 0x5b, 0xfc, 0x0a, 0x9e, 0xfd, 0x99, 0x59, 0xda, 0x79, 0xb2, 0xb2, 0xb8, - 0xfb, 0x78, 0x65, 0xf1, 0xf6, 0xa3, 0x95, 0xc5, 0x0f, 0x77, 0xea, 0xc5, 0x5b, 0x67, 0xa2, 0x4a, 0xcb, 0x85, 0xd0, - 0x9e, 0x46, 0xc0, 0x28, 0x97, 0x4e, 0x07, 0xe0, 0x6c, 0x5b, 0x2d, 0xfc, 0xf3, 0xb8, 0xeb, 0xea, 0x5e, 0xa7, 0xd8, - 0x4b, 0x63, 0xf9, 0xf8, 0x09, 0x60, 0xf9, 0xb2, 0xfb, 0x68, 0x88, 0xed, 0x08, 0x51, 0xf8, 0x77, 0xbe, 0xfd, 0x64, - 0x08, 0x1a, 0xc1, 0xc2, 0x7f, 0xf0, 0xdf, 0x64, 0xa7, 0x3b, 0x14, 0x2f, 0x6d, 0xac, 0xff, 0xb6, 0xf3, 0xb8, 0x80, - 0xa6, 0xf8, 0xdf, 0x2f, 0xda, 0x84, 0x46, 0x03, 0xde, 0x1c, 0xf7, 0x21, 0xd0, 0xe8, 0xc9, 0xa4, 0xeb, 0x7f, 0x7e, - 0xfe, 0xd8, 0x7f, 0x32, 0xe9, 0x3c, 0xfe, 0x56, 0xbc, 0x25, 0x40, 0xc1, 0xcf, 0xf1, 0xdf, 0xb7, 0xdb, 0xed, 0x49, - 0xab, 0xe3, 0x3f, 0x39, 0xdf, 0xf6, 0xb7, 0x93, 0xd6, 0x23, 0xff, 0x09, 0xfe, 0xab, 0x86, 0x9b, 0x64, 0x53, 0x66, - 0x5b, 0xb8, 0xde, 0x0d, 0xbf, 0xd7, 0x9c, 0xa3, 0xfb, 0xd0, 0xda, 0x79, 0xf8, 0xf2, 0x09, 0xac, 0xd1, 0xa4, 0xd3, - 0x85, 0xff, 0x5f, 0xf7, 0xf8, 0x2d, 0x12, 0x5e, 0x0e, 0x1c, 0x31, 0x4c, 0x2f, 0x56, 0x84, 0xa3, 0x0f, 0xba, 0x3d, - 0xf0, 0xfe, 0xb4, 0x2e, 0x00, 0xc2, 0xf8, 0xad, 0x01, 0x10, 0xce, 0xef, 0x16, 0x01, 0xa1, 0x5f, 0x1b, 0xf8, 0x1d, - 0x23, 0x20, 0x7f, 0x6a, 0x06, 0xb9, 0x2f, 0xd9, 0x52, 0xa0, 0xa3, 0xe9, 0xac, 0xbd, 0x65, 0xce, 0xe1, 0x97, 0xf8, - 0xe3, 0x06, 0x65, 0x0f, 0x5a, 0x73, 0x6e, 0xc6, 0x83, 0x32, 0xdc, 0xc8, 0x97, 0xf2, 0xe2, 0x43, 0xc1, 0xd7, 0x10, - 0x24, 0xbe, 0x9d, 0x20, 0xdf, 0xde, 0x8d, 0x1e, 0xf1, 0xef, 0x4c, 0x8f, 0x82, 0x1b, 0xf4, 0xa8, 0x45, 0xdc, 0x29, - 0x62, 0x40, 0x8e, 0xfe, 0x3e, 0xbd, 0x3b, 0x9c, 0xbe, 0xc5, 0xb6, 0xc5, 0xb0, 0xa8, 0xb0, 0x45, 0xce, 0xe6, 0xd3, - 0x5f, 0x73, 0x44, 0x20, 0xd2, 0xcd, 0x43, 0x5b, 0x46, 0x61, 0x66, 0xf8, 0xd1, 0x62, 0xf5, 0x72, 0x2e, 0xae, 0x34, - 0x85, 0x74, 0x1f, 0x71, 0x47, 0x47, 0x70, 0xf0, 0x06, 0x40, 0xb8, 0xc8, 0x78, 0x84, 0xbf, 0x8a, 0x05, 0xe4, 0xa6, - 0xdf, 0xcf, 0x8a, 0x79, 0xc2, 0x30, 0x9d, 0x66, 0x28, 0x3e, 0x20, 0x0b, 0x8f, 0xf2, 0xae, 0x21, 0xa6, 0xb0, 0x7f, - 0x83, 0xe9, 0xf7, 0xea, 0xec, 0x60, 0x8a, 0x71, 0x84, 0x37, 0x6c, 0x14, 0x47, 0x8e, 0xed, 0xcc, 0x60, 0x23, 0xc3, - 0x2c, 0xad, 0x5a, 0xee, 0x3b, 0xa5, 0xbd, 0xbb, 0xb6, 0xfa, 0x69, 0xa6, 0x1c, 0x3f, 0x75, 0x17, 0x1e, 0xca, 0xb8, - 0xa3, 0x2d, 0x1d, 0x03, 0x18, 0x5f, 0x95, 0xe4, 0xa8, 0x03, 0x2a, 0x63, 0xc2, 0x16, 0xd6, 0x44, 0xc7, 0xef, 0x82, - 0x77, 0x41, 0xc5, 0xf8, 0xe9, 0xb0, 0xef, 0x9d, 0xd6, 0x36, 0x58, 0x3b, 0x46, 0x37, 0x3d, 0xd0, 0x91, 0xfe, 0xa5, - 0x1f, 0xfd, 0x6b, 0x74, 0xf5, 0x0b, 0x03, 0xb6, 0xe0, 0x88, 0xcf, 0x04, 0xee, 0xb6, 0xf8, 0x44, 0x83, 0x48, 0x28, - 0xc1, 0x0b, 0x73, 0x50, 0xe6, 0x98, 0xbf, 0x4a, 0x26, 0x3e, 0x4d, 0x26, 0x7e, 0x80, 0xb0, 0xac, 0x9a, 0x70, 0x77, - 0x41, 0x67, 0x23, 0xf8, 0x23, 0x9a, 0x98, 0x68, 0x8a, 0xa1, 0xf2, 0xd0, 0xa0, 0x29, 0xbe, 0xbb, 0x35, 0x22, 0x73, - 0x4f, 0x03, 0x44, 0x04, 0x0e, 0xe5, 0xdf, 0xaa, 0x58, 0x3d, 0xc8, 0xa0, 0x16, 0x38, 0xfa, 0xf8, 0xb3, 0x2f, 0xf4, - 0x67, 0x29, 0x64, 0x26, 0x02, 0x21, 0x8d, 0xd2, 0x6a, 0xa8, 0x2a, 0x34, 0x56, 0x3c, 0xbd, 0x3a, 0x90, 0xdf, 0x3c, - 0xb0, 0x31, 0x4a, 0x4d, 0xa7, 0x13, 0xd5, 0xf7, 0xd6, 0x36, 0x41, 0x35, 0xd2, 0xaf, 0xa0, 0x52, 0x82, 0x01, 0x6a, - 0x3f, 0xbc, 0x72, 0x60, 0x49, 0x2f, 0x29, 0xb4, 0x85, 0xee, 0x1b, 0xb1, 0xf3, 0x78, 0x28, 0x55, 0x98, 0x67, 0xc9, - 0xab, 0x52, 0x2d, 0x5a, 0x9a, 0xb0, 0xe3, 0x89, 0x38, 0x01, 0xbc, 0xa0, 0x06, 0x0f, 0xd3, 0xcc, 0xee, 0x3f, 0xe8, - 0xad, 0x23, 0x3e, 0xfe, 0x24, 0xeb, 0x21, 0xf8, 0xa5, 0x7f, 0x1b, 0x3e, 0xc0, 0x1f, 0x65, 0x7d, 0x70, 0x64, 0xbb, - 0x3e, 0x29, 0x80, 0x07, 0xd5, 0x2f, 0xb3, 0xa2, 0xf4, 0xdb, 0x04, 0x5d, 0xed, 0xdd, 0x55, 0x69, 0x4b, 0x05, 0xdd, - 0xdd, 0xa9, 0x14, 0x34, 0x3c, 0x1b, 0x12, 0x19, 0x94, 0x45, 0xd7, 0xdf, 0x31, 0xc4, 0xfe, 0x79, 0x0b, 0xff, 0xd6, - 0x04, 0xff, 0x43, 0x68, 0xa0, 0x24, 0xff, 0x6b, 0x68, 0xbe, 0x2d, 0x94, 0x0c, 0xf4, 0xfb, 0x81, 0xc4, 0xb2, 0x10, - 0xc9, 0xf5, 0x6d, 0xb0, 0xe2, 0xc0, 0x4c, 0x24, 0x63, 0xd8, 0x9e, 0x11, 0x5b, 0x13, 0xbb, 0x52, 0x46, 0x8e, 0x9e, - 0x43, 0x5f, 0x47, 0x7f, 0xc6, 0x7c, 0x55, 0x9d, 0x57, 0x93, 0x12, 0x2b, 0xa6, 0xc0, 0x7d, 0xdd, 0x38, 0x94, 0xeb, - 0x89, 0x3c, 0x6f, 0xfd, 0x1d, 0x94, 0xf5, 0x0c, 0x2d, 0x13, 0xc2, 0x5d, 0x43, 0x44, 0x30, 0xfa, 0xd4, 0x2a, 0x4d, - 0xf2, 0x6a, 0x54, 0x36, 0xe7, 0x07, 0xb3, 0x06, 0x7f, 0x97, 0xb2, 0xba, 0xe5, 0x23, 0xaf, 0xef, 0x62, 0xca, 0xc5, - 0x28, 0xce, 0xe9, 0x56, 0xb8, 0x02, 0xbd, 0x16, 0x78, 0xad, 0xa8, 0x44, 0x52, 0x82, 0x15, 0x03, 0x1b, 0x8b, 0xec, - 0x40, 0x26, 0x06, 0x9a, 0xdf, 0x1a, 0x37, 0xaf, 0xed, 0x8e, 0x44, 0x4e, 0x20, 0xfe, 0x16, 0x83, 0x2d, 0xe8, 0x63, - 0x83, 0xb4, 0x5d, 0xbb, 0x4b, 0xc8, 0x06, 0x43, 0x5c, 0xab, 0x1f, 0xd7, 0x32, 0x05, 0x90, 0x6d, 0x12, 0x5a, 0x8f, - 0x4b, 0x24, 0x74, 0x25, 0x9d, 0x4e, 0x59, 0xc4, 0xfd, 0x28, 0xa5, 0xfc, 0x2d, 0xc7, 0x10, 0x53, 0x5e, 0x87, 0x6d, - 0xbb, 0x25, 0xc8, 0x46, 0xe3, 0xd7, 0xc7, 0xe4, 0xee, 0x86, 0x42, 0xfd, 0xe5, 0xab, 0x7a, 0x2e, 0xf6, 0xa4, 0xdb, - 0x7f, 0x77, 0xb0, 0x67, 0x89, 0x4d, 0xb9, 0xbb, 0x05, 0xaf, 0xbb, 0xe4, 0xc1, 0x8b, 0x54, 0x96, 0x50, 0xa4, 0xb2, - 0x58, 0x22, 0x01, 0x4e, 0xe4, 0x2e, 0x6f, 0x09, 0xb4, 0x6d, 0x8b, 0xa5, 0x43, 0x11, 0x7a, 0x9c, 0x82, 0x97, 0x13, - 0xe3, 0xf7, 0xe9, 0xb6, 0xb0, 0x6b, 0x0b, 0x17, 0xcc, 0x56, 0x59, 0x41, 0xca, 0xae, 0xe1, 0xa9, 0x0a, 0x54, 0x82, - 0x35, 0xc2, 0x54, 0x82, 0x90, 0x1c, 0x4a, 0xe7, 0x25, 0x2f, 0xb7, 0x2e, 0xe6, 0xa7, 0x53, 0x90, 0x93, 0x2a, 0xa9, - 0xe7, 0xa3, 0xec, 0xb0, 0x4b, 0x53, 0xf5, 0x4f, 0x4a, 0x19, 0x49, 0x55, 0xdf, 0x0e, 0x6f, 0xfc, 0xce, 0xaa, 0xc0, - 0x5e, 0xea, 0x05, 0xcc, 0x49, 0x99, 0x6c, 0x1b, 0x39, 0x29, 0x46, 0x5d, 0x09, 0xa8, 0x6f, 0xf7, 0x4f, 0x82, 0x99, - 0x1c, 0xef, 0x75, 0xb6, 0xf4, 0x9b, 0xad, 0x5a, 0x4e, 0x0e, 0x28, 0xbf, 0x5c, 0xdc, 0xeb, 0x90, 0x00, 0xc3, 0x0a, - 0x02, 0x4c, 0xd2, 0x04, 0xb0, 0xe8, 0xe8, 0xdb, 0xde, 0x69, 0xab, 0xb4, 0x5d, 0x28, 0xc3, 0x0d, 0x29, 0xba, 0x18, - 0x93, 0xd4, 0xc2, 0xbf, 0x93, 0x4e, 0x7f, 0x37, 0x92, 0xc6, 0x25, 0x0a, 0x8f, 0x02, 0xa4, 0x07, 0x74, 0x46, 0x0b, - 0xce, 0x8f, 0xb3, 0xad, 0x0b, 0x76, 0xda, 0x8a, 0x66, 0x71, 0x15, 0x6b, 0x45, 0x53, 0x43, 0x4f, 0x99, 0x55, 0x33, - 0xe1, 0x63, 0xd4, 0x40, 0x92, 0x04, 0x77, 0x29, 0x03, 0xb9, 0x64, 0xa1, 0x03, 0x0b, 0x01, 0x85, 0x49, 0xae, 0xab, - 0x80, 0xaf, 0xd4, 0xb8, 0xa5, 0xdd, 0xff, 0xcb, 0x3f, 0xff, 0x6f, 0x19, 0xc3, 0x05, 0xaa, 0x74, 0xd4, 0x58, 0x0d, - 0x42, 0x97, 0xbb, 0x98, 0x02, 0x55, 0x9d, 0xf2, 0xb2, 0xcb, 0xd6, 0x59, 0x1e, 0x8f, 0x5a, 0x93, 0x28, 0x19, 0x03, - 0x60, 0x6b, 0x09, 0x64, 0x26, 0x48, 0x48, 0xa8, 0xeb, 0x45, 0xc8, 0x82, 0xbf, 0x29, 0x11, 0x5b, 0x25, 0xc0, 0xd3, - 0x6e, 0x35, 0xd3, 0xb2, 0xab, 0x0d, 0x55, 0x4b, 0xcd, 0x56, 0x3f, 0x5c, 0xa6, 0x84, 0x5a, 0x2d, 0x2f, 0x1b, 0x5a, - 0xea, 0xc3, 0xa8, 0x7f, 0xff, 0x97, 0x7f, 0xf8, 0x1f, 0xea, 0x15, 0xcf, 0x98, 0xfe, 0xf2, 0x4f, 0x7f, 0x87, 0x29, - 0xd0, 0x96, 0x3e, 0x87, 0x22, 0x39, 0x61, 0x55, 0x87, 0x50, 0x42, 0x60, 0x58, 0x95, 0xd3, 0x57, 0xcf, 0xdf, 0xde, - 0xa7, 0x09, 0x69, 0xb3, 0x49, 0xe8, 0x68, 0xd3, 0x96, 0x15, 0x8f, 0xd4, 0x48, 0x4e, 0xbc, 0x08, 0x95, 0x48, 0xef, - 0x3b, 0x25, 0x47, 0xf9, 0x7a, 0x35, 0x16, 0x2a, 0x42, 0x88, 0x25, 0x65, 0x55, 0x6e, 0x61, 0xe8, 0x7e, 0x81, 0xaf, - 0x41, 0xd7, 0x28, 0xa6, 0xc5, 0xab, 0xf5, 0xe9, 0xfd, 0x34, 0x07, 0xf8, 0xc7, 0x48, 0x71, 0x11, 0x87, 0xa4, 0x63, - 0xe9, 0x16, 0xda, 0x7c, 0xc9, 0x55, 0x49, 0xa3, 0x08, 0x47, 0xf1, 0xe1, 0x93, 0xbf, 0x29, 0xff, 0x38, 0x45, 0xcb, - 0xca, 0x72, 0xa6, 0xd1, 0xa5, 0x74, 0x1f, 0x1f, 0xb5, 0xdb, 0xb3, 0x4b, 0x77, 0x51, 0xcd, 0xe0, 0xad, 0x9b, 0x8c, - 0x62, 0x97, 0xe6, 0x80, 0x74, 0x9e, 0xad, 0xc3, 0xa4, 0xe0, 0x31, 0xb5, 0x31, 0xaa, 0x56, 0x96, 0x7f, 0x58, 0x50, - 0xa4, 0x2e, 0xfe, 0x05, 0xcf, 0x9d, 0x65, 0x50, 0x13, 0x4a, 0x0c, 0x2c, 0x16, 0x46, 0xaf, 0xae, 0xe8, 0x35, 0xe9, - 0x2c, 0xa7, 0x0d, 0x99, 0xe7, 0xe6, 0xe6, 0x89, 0xf7, 0x43, 0x3c, 0xc3, 0x9e, 0x74, 0xbc, 0x49, 0x77, 0xa1, 0x87, - 0xe7, 0x3c, 0x9b, 0x9a, 0x07, 0xe5, 0x2c, 0x62, 0x43, 0x36, 0x56, 0xc1, 0x60, 0x59, 0x2f, 0x0e, 0xc1, 0xcb, 0xc9, - 0xf6, 0x8a, 0xb9, 0x24, 0x48, 0x74, 0x40, 0x0e, 0xf0, 0x7c, 0x86, 0x1b, 0x10, 0xe8, 0x9f, 0x45, 0x3c, 0x20, 0x7e, - 0xed, 0x99, 0xc7, 0xed, 0x11, 0x4a, 0x99, 0x6c, 0x61, 0xc0, 0xd3, 0x13, 0x4d, 0x31, 0x2c, 0x5b, 0x4f, 0xdb, 0x2a, - 0x7d, 0xea, 0x6e, 0x0e, 0x25, 0xa2, 0x3a, 0xdf, 0xca, 0x53, 0xec, 0xa7, 0xb5, 0x70, 0x88, 0x54, 0x31, 0x5d, 0xd7, - 0x5b, 0x59, 0x2f, 0x34, 0xb5, 0xa8, 0xfd, 0x16, 0x0c, 0x30, 0x02, 0xd3, 0x6e, 0xb6, 0xa2, 0x42, 0x6c, 0xf5, 0x34, - 0xfc, 0x56, 0xbb, 0x3e, 0xd1, 0x6c, 0x46, 0x0d, 0x5d, 0x60, 0x62, 0x32, 0x58, 0x51, 0x76, 0x50, 0x86, 0x86, 0x48, - 0x88, 0x90, 0x6d, 0xe4, 0x46, 0x10, 0x4f, 0x32, 0x55, 0x02, 0x7f, 0x72, 0xa2, 0xff, 0xff, 0x00, 0x69, 0x5b, 0x88, - 0x58, 0x18, 0x7f, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xc5, 0x7d, 0xd9, 0x76, 0xe3, 0xc6, 0x92, 0xe0, 0xf3, + 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x10, 0x4a, 0xad, 0x02, 0xae, 0x40, 0x88, 0xa4, 0x6a, 0x33, 0x28, 0x90, 0x57, 0xb5, + 0xd8, 0x55, 0x76, 0x6d, 0x2e, 0xa9, 0xec, 0x6b, 0xcb, 0xb4, 0x04, 0x91, 0x49, 0x11, 0x2e, 0x10, 0xa0, 0x81, 0xa4, + 0x16, 0x53, 0xe8, 0x33, 0x4f, 0xf3, 0xd4, 0xe7, 0xcc, 0xd6, 0x0f, 0xfd, 0x30, 0x7d, 0xba, 0x1f, 0xe6, 0x23, 0xe6, + 0xb9, 0x3f, 0xe5, 0xfe, 0xc0, 0xf4, 0x27, 0x4c, 0x44, 0xe4, 0x82, 0x04, 0x17, 0x49, 0x5e, 0xba, 0xe7, 0xd8, 0x2a, + 0x12, 0xb9, 0x46, 0x44, 0x46, 0xc6, 0x96, 0x91, 0xe0, 0xde, 0xc6, 0x30, 0x1b, 0xf0, 0xab, 0x29, 0xb3, 0xc6, 0x7c, + 0x92, 0x74, 0xf7, 0xe4, 0xbf, 0x2c, 0x1a, 0x76, 0xf7, 0x92, 0x38, 0xfd, 0x64, 0xe5, 0x2c, 0x09, 0xe3, 0x41, 0x96, + 0x5a, 0xe3, 0x9c, 0x8d, 0xc2, 0x61, 0xc4, 0xa3, 0x20, 0x9e, 0x44, 0x67, 0xcc, 0xda, 0xe9, 0xee, 0x4d, 0x18, 0x8f, + 0xac, 0xc1, 0x38, 0xca, 0x0b, 0xc6, 0xc3, 0x8f, 0x87, 0x9f, 0x37, 0x9e, 0x74, 0xf7, 0x8a, 0x41, 0x1e, 0x4f, 0xb9, + 0x85, 0x43, 0x86, 0x93, 0x6c, 0x38, 0x4b, 0x58, 0xf7, 0x3c, 0xca, 0xad, 0x17, 0x3c, 0x7c, 0x77, 0xfa, 0x13, 0x1b, + 0x70, 0x7f, 0xc8, 0x46, 0x71, 0xca, 0xde, 0xe7, 0xd9, 0x94, 0xe5, 0xfc, 0xca, 0x3b, 0x58, 0x5d, 0x11, 0xb3, 0xc2, + 0x7b, 0xa6, 0xab, 0xce, 0x18, 0x7f, 0x77, 0x91, 0xaa, 0x3e, 0xcf, 0x99, 0x98, 0x24, 0xcb, 0x0b, 0xaf, 0x58, 0xd3, + 0xe6, 0xe0, 0x6a, 0x72, 0x9a, 0x25, 0x85, 0xf7, 0x49, 0xd7, 0x4f, 0xf3, 0x8c, 0x67, 0x08, 0x96, 0x3f, 0x8e, 0x0a, + 0xa3, 0xa5, 0xf7, 0x6e, 0x45, 0x93, 0xa9, 0xac, 0x7c, 0x55, 0xbc, 0x48, 0x67, 0x13, 0x96, 0x47, 0xa7, 0x09, 0xf3, + 0x72, 0x1e, 0x3a, 0xdc, 0x63, 0x5e, 0xec, 0x86, 0x5d, 0x66, 0xc5, 0xa9, 0xc5, 0x7b, 0x2f, 0x38, 0x95, 0xcc, 0x99, + 0x6e, 0x15, 0x6c, 0x34, 0x3d, 0x20, 0xd7, 0x28, 0x3e, 0x9b, 0xe9, 0xe7, 0x8b, 0x3c, 0xe6, 0xea, 0xfb, 0x79, 0x94, + 0xcc, 0x58, 0x10, 0x97, 0x6e, 0xc0, 0x8f, 0x58, 0x3f, 0x8c, 0xbd, 0x4f, 0x34, 0x28, 0x0c, 0x39, 0x1f, 0x65, 0xb9, + 0x83, 0xb4, 0x8a, 0x71, 0x6c, 0x76, 0x7d, 0xed, 0xb0, 0x70, 0x5e, 0xba, 0xee, 0x27, 0xee, 0x0f, 0xa2, 0x24, 0x71, + 0x70, 0xe2, 0xad, 0xad, 0x1c, 0x67, 0x8c, 0x3d, 0x76, 0x14, 0xf7, 0xdd, 0x4e, 0x3c, 0x72, 0x0a, 0xee, 0x56, 0xfd, + 0xb2, 0x91, 0x55, 0x70, 0x87, 0xb9, 0xee, 0xbb, 0xf5, 0x7d, 0x72, 0xc6, 0x67, 0x39, 0xc0, 0x5e, 0x7a, 0xef, 0xd4, + 0xcc, 0x07, 0x58, 0xff, 0x8c, 0x3a, 0x76, 0x00, 0xf6, 0x82, 0x5b, 0x9f, 0x87, 0x17, 0x71, 0x3a, 0xcc, 0x2e, 0xfc, + 0x83, 0x71, 0x04, 0x1f, 0x1f, 0xb2, 0x8c, 0x6f, 0x6d, 0x39, 0xe7, 0x59, 0x3c, 0xb4, 0x9a, 0x61, 0x68, 0x56, 0x5e, + 0x3d, 0x3b, 0x38, 0xb8, 0xbe, 0x5e, 0x28, 0xf0, 0xd3, 0x88, 0xc7, 0xe7, 0x4c, 0x74, 0x06, 0x00, 0x6c, 0xf8, 0x9c, + 0x72, 0x36, 0x3c, 0xe0, 0x57, 0x09, 0x94, 0x32, 0xc6, 0x0b, 0x1b, 0x70, 0x7c, 0x9e, 0x0d, 0x80, 0x6c, 0xa9, 0x41, + 0x78, 0x68, 0x9a, 0xb3, 0x69, 0x12, 0x0d, 0x18, 0xd6, 0xc3, 0x48, 0x55, 0x8f, 0xaa, 0x91, 0xf7, 0x6d, 0x28, 0x96, + 0xd7, 0x71, 0xbd, 0x94, 0x87, 0x29, 0xbb, 0xb0, 0xde, 0x44, 0xd3, 0xce, 0x20, 0x89, 0x8a, 0xc2, 0xca, 0xf8, 0x9c, + 0x50, 0xc8, 0x67, 0x03, 0x60, 0x10, 0x42, 0x70, 0x0e, 0x64, 0xe2, 0xe3, 0xb8, 0xf0, 0x8f, 0x37, 0x07, 0x45, 0xf1, + 0x81, 0x15, 0xb3, 0x84, 0x6f, 0x86, 0xb0, 0x16, 0x6c, 0x23, 0x0c, 0xbf, 0x75, 0xf9, 0x38, 0xcf, 0x2e, 0xac, 0x17, + 0x79, 0x0e, 0xcd, 0x6d, 0x98, 0x52, 0x34, 0xb0, 0xe2, 0xc2, 0x4a, 0x33, 0x6e, 0xe9, 0xc1, 0x70, 0x01, 0x7d, 0xeb, + 0x63, 0xc1, 0xac, 0x93, 0x59, 0x5a, 0x44, 0x23, 0x06, 0x4d, 0x4f, 0xac, 0x2c, 0xb7, 0x4e, 0x60, 0xd0, 0x13, 0x58, + 0xb2, 0x82, 0xc3, 0xae, 0xf1, 0x6d, 0xb7, 0x43, 0x73, 0x41, 0xe1, 0x21, 0xbb, 0xe4, 0x21, 0x2f, 0x81, 0x31, 0x61, + 0x55, 0x14, 0x1a, 0x8e, 0x3b, 0x4f, 0xa0, 0x00, 0xc0, 0x26, 0x96, 0x75, 0xcc, 0xc6, 0x7a, 0x71, 0x3e, 0xdf, 0xda, + 0xd2, 0xb4, 0x46, 0xc2, 0x43, 0xdb, 0x62, 0xa1, 0xad, 0x27, 0x10, 0xaf, 0x91, 0xc8, 0xf5, 0xb8, 0x2f, 0xc9, 0x77, + 0x70, 0x95, 0x0e, 0xea, 0x63, 0x43, 0x65, 0xc9, 0xb3, 0x03, 0x9e, 0xc7, 0xe9, 0x19, 0x00, 0xa1, 0xd8, 0xc0, 0x68, + 0x52, 0x96, 0x62, 0xf1, 0xdf, 0x03, 0xd4, 0x61, 0x17, 0x47, 0xcf, 0xb8, 0x63, 0x17, 0xd4, 0xc3, 0x06, 0x40, 0x80, + 0xf4, 0xc0, 0x60, 0xbc, 0xc7, 0x03, 0xbe, 0x6d, 0xdb, 0xde, 0xb7, 0xae, 0x77, 0x81, 0x1c, 0xe4, 0xfb, 0x3e, 0xb1, + 0xaf, 0xe8, 0x1c, 0x87, 0x2d, 0x04, 0xda, 0x4f, 0x58, 0x7a, 0xc6, 0xc7, 0x3d, 0x7e, 0xd4, 0xec, 0x07, 0x0c, 0xa0, + 0x1a, 0xce, 0x06, 0xcc, 0x41, 0x7e, 0xf4, 0x0a, 0xdc, 0x3e, 0xdb, 0x0e, 0x4c, 0x81, 0x0b, 0xb3, 0x41, 0x38, 0xd6, + 0x96, 0xc6, 0x55, 0xb0, 0x29, 0xc0, 0x90, 0xcf, 0x6d, 0xd8, 0x61, 0xa7, 0x2c, 0x37, 0xe0, 0xd0, 0xcd, 0x3a, 0xb5, + 0x15, 0x9c, 0xc1, 0x0a, 0x41, 0x3f, 0x6b, 0x34, 0x4b, 0x07, 0x3c, 0x06, 0xc1, 0x65, 0x6f, 0x03, 0xb8, 0x62, 0xe5, + 0xf4, 0xc2, 0xd9, 0x6e, 0xe9, 0x3a, 0xb1, 0xbb, 0xcd, 0x8f, 0x8a, 0xed, 0x56, 0xdf, 0x43, 0x28, 0x35, 0xf1, 0x25, + 0xe2, 0x31, 0x20, 0x58, 0x7a, 0x1f, 0xb9, 0xde, 0x9e, 0x9f, 0xf7, 0xb8, 0xbf, 0xcc, 0xc7, 0x21, 0xf3, 0x27, 0xd1, + 0x14, 0xb1, 0xe1, 0xc4, 0x03, 0x51, 0x3a, 0x40, 0xe8, 0x6a, 0xeb, 0x82, 0x14, 0xf3, 0x2b, 0x16, 0x70, 0x81, 0x20, + 0xb0, 0x67, 0x5f, 0x44, 0x83, 0x31, 0x6c, 0xf1, 0x8a, 0x70, 0x43, 0xb5, 0x1d, 0x06, 0x39, 0x8b, 0x38, 0x7b, 0x91, + 0x30, 0x7c, 0xc2, 0x15, 0x80, 0x9e, 0xb6, 0xeb, 0x15, 0x6a, 0xdf, 0x25, 0x31, 0x7f, 0x9b, 0xc1, 0x3c, 0x1d, 0xc1, + 0x24, 0xc0, 0xc5, 0xc5, 0xd6, 0x56, 0x8c, 0x2c, 0xb2, 0xcf, 0x61, 0xb5, 0x4e, 0x67, 0x9c, 0x01, 0xbd, 0xb0, 0x85, + 0x0d, 0xd4, 0xf6, 0x62, 0x9f, 0x03, 0x11, 0x9f, 0x65, 0x29, 0x87, 0xe1, 0x00, 0x5e, 0xcd, 0x41, 0x7e, 0x34, 0x9d, + 0xb2, 0x74, 0xf8, 0x6c, 0x1c, 0x27, 0x43, 0xa0, 0x46, 0x09, 0xf8, 0x26, 0x3c, 0x04, 0x3c, 0x01, 0x99, 0xe0, 0x66, + 0x8c, 0x68, 0xf9, 0x90, 0x91, 0x59, 0x68, 0xdb, 0x1d, 0x94, 0x40, 0x12, 0x0b, 0x94, 0x41, 0xb4, 0x70, 0x1f, 0x40, + 0xf4, 0x17, 0x2e, 0xdb, 0x0e, 0x63, 0xbd, 0x8c, 0x92, 0xc0, 0xef, 0x51, 0xd2, 0x00, 0xfd, 0x81, 0x10, 0xbc, 0x83, + 0x82, 0xeb, 0x4b, 0x29, 0x75, 0x22, 0xae, 0x30, 0x04, 0x02, 0x0c, 0x50, 0x82, 0x48, 0x1a, 0xbc, 0xcf, 0x92, 0xab, + 0x51, 0x9c, 0x24, 0x07, 0xb3, 0xe9, 0x34, 0xcb, 0xb9, 0xf7, 0x55, 0x38, 0xe7, 0x59, 0x85, 0x2b, 0x6d, 0xf2, 0xe2, + 0x22, 0xe6, 0x48, 0x50, 0x77, 0x3e, 0x88, 0x60, 0xa9, 0x9f, 0x66, 0x59, 0xc2, 0xa2, 0x14, 0xd0, 0xe0, 0x3d, 0xdb, + 0x0e, 0xd2, 0x59, 0x92, 0x74, 0x4e, 0x61, 0xd8, 0x4f, 0x1d, 0xaa, 0x16, 0x12, 0x3f, 0xa0, 0xef, 0xfb, 0x79, 0x1e, + 0x5d, 0x41, 0x43, 0x6c, 0x03, 0xec, 0x05, 0xab, 0xf5, 0xe5, 0xc1, 0xbb, 0xb7, 0xbe, 0x60, 0xfc, 0x78, 0x74, 0x05, + 0x80, 0x96, 0x95, 0xd4, 0x1c, 0xe5, 0xd9, 0x64, 0x61, 0x6a, 0xa4, 0x43, 0x1c, 0xf2, 0xce, 0x1a, 0x10, 0x62, 0x1a, + 0x19, 0x56, 0x89, 0x9b, 0x10, 0xbc, 0x25, 0x7e, 0x96, 0x95, 0xb8, 0x07, 0x7a, 0xf8, 0x25, 0x10, 0xc5, 0x30, 0xe5, + 0x2d, 0xd0, 0xe6, 0x57, 0xf3, 0x38, 0x24, 0x38, 0xa7, 0xa8, 0x7f, 0x11, 0xc6, 0x41, 0x04, 0xb3, 0xcf, 0xc5, 0x80, + 0xa5, 0x82, 0x38, 0x2e, 0x4b, 0x6f, 0xac, 0x99, 0x18, 0x25, 0x1e, 0x0a, 0x14, 0x16, 0x86, 0xa0, 0x60, 0x38, 0x3c, + 0xb8, 0xde, 0xd7, 0xe1, 0x3c, 0x52, 0xf8, 0xa0, 0x86, 0xc2, 0xfd, 0x15, 0x08, 0x39, 0x81, 0x9a, 0xec, 0x1c, 0xf4, + 0x20, 0xc0, 0xf9, 0x95, 0x07, 0xfa, 0x3f, 0x41, 0x28, 0x36, 0x5a, 0x1e, 0x68, 0xd0, 0x67, 0xe3, 0x28, 0x3d, 0x63, + 0xc3, 0x60, 0xcc, 0x4b, 0x29, 0x79, 0xf7, 0x2d, 0x58, 0x63, 0x60, 0xa7, 0xc2, 0x7a, 0x79, 0xf8, 0xe6, 0xb5, 0x5c, + 0xb9, 0x9a, 0x30, 0x86, 0x45, 0x9a, 0x81, 0x5a, 0x05, 0xb1, 0x2d, 0xc5, 0xf1, 0x0b, 0x2d, 0xbd, 0x45, 0x49, 0x5c, + 0x7c, 0x9c, 0x82, 0x89, 0xc1, 0xde, 0xc3, 0x30, 0x30, 0x7d, 0x08, 0x53, 0x51, 0x39, 0xcc, 0x27, 0x2a, 0x86, 0xba, + 0x08, 0x3a, 0x0b, 0x4c, 0xc5, 0x63, 0xe6, 0xb8, 0x25, 0xb0, 0x2a, 0x8f, 0x07, 0x56, 0x34, 0x1c, 0xbe, 0x4a, 0x63, + 0x1e, 0x47, 0x49, 0xfc, 0x0b, 0x51, 0x72, 0x8e, 0x3c, 0xc6, 0x3a, 0x72, 0x11, 0x00, 0x77, 0xea, 0x91, 0xb8, 0x4a, + 0xc8, 0x6e, 0x10, 0x31, 0x84, 0xb4, 0x4c, 0xc2, 0xa3, 0xbe, 0x04, 0x2f, 0xf1, 0xa7, 0xb3, 0x62, 0x8c, 0x84, 0x95, + 0x03, 0xa3, 0x20, 0xcf, 0x4e, 0x0b, 0x96, 0x9f, 0xb3, 0xa1, 0xe6, 0x80, 0x02, 0xb0, 0xa2, 0xe6, 0x60, 0xbc, 0xd0, + 0x8c, 0x8e, 0xd2, 0xa1, 0x1c, 0x86, 0xea, 0x98, 0x62, 0x96, 0x49, 0x66, 0xd6, 0x16, 0x8e, 0x96, 0x02, 0x8e, 0x30, + 0x2a, 0xa4, 0x24, 0x28, 0x42, 0x85, 0xe1, 0x18, 0xa4, 0x10, 0x73, 0x6b, 0xdb, 0x5c, 0x69, 0xb2, 0x17, 0x33, 0x52, + 0x09, 0x05, 0x74, 0x84, 0x8d, 0x4c, 0x90, 0x16, 0x2e, 0xec, 0x2a, 0x90, 0xf2, 0x12, 0x5c, 0x21, 0x45, 0x94, 0x99, + 0x83, 0x0c, 0x10, 0x7e, 0x4d, 0xba, 0x90, 0xf9, 0xd8, 0x82, 0x21, 0x1b, 0xf8, 0x7a, 0xe5, 0x81, 0xb0, 0x12, 0xef, + 0x0a, 0x11, 0x6f, 0x0d, 0xd8, 0xa4, 0x8b, 0x00, 0x30, 0x6f, 0x83, 0xf9, 0x69, 0xb6, 0x3f, 0x18, 0xb0, 0xa2, 0xc8, + 0xf2, 0xad, 0xad, 0x0d, 0x6a, 0xbf, 0xce, 0xd0, 0x02, 0x4a, 0xba, 0x5a, 0xd6, 0xd9, 0x05, 0x69, 0x70, 0x53, 0xad, + 0x28, 0x9d, 0x1e, 0xd8, 0xc7, 0xc7, 0x20, 0xb3, 0x3d, 0x49, 0x06, 0xa0, 0xfa, 0xb2, 0xe1, 0x27, 0xec, 0x99, 0x3a, + 0x65, 0x56, 0xda, 0x97, 0x4e, 0x1d, 0x24, 0x0f, 0x86, 0x75, 0x4b, 0x63, 0x41, 0x57, 0x0e, 0x8d, 0xab, 0x21, 0x15, + 0xe4, 0xfc, 0x8c, 0x54, 0xb6, 0xb1, 0x8c, 0x60, 0xb5, 0x95, 0x1e, 0x91, 0x5e, 0x61, 0x93, 0x13, 0xa0, 0x47, 0xbc, + 0xdf, 0x91, 0xf5, 0x61, 0x21, 0x28, 0x97, 0xb3, 0x9f, 0x67, 0xac, 0xe0, 0x82, 0x75, 0x61, 0xdc, 0x1c, 0xc6, 0x2d, + 0x97, 0xac, 0xc3, 0x9a, 0xed, 0xb8, 0x0a, 0xb6, 0x77, 0x53, 0xd4, 0x63, 0x05, 0x72, 0xf2, 0xcd, 0xec, 0x44, 0xf6, + 0x84, 0x7b, 0x7d, 0xfd, 0xb5, 0x1a, 0xa4, 0x5a, 0x4a, 0x6d, 0x03, 0x2d, 0xac, 0x89, 0xad, 0x9a, 0x0c, 0x6d, 0x57, + 0x2a, 0xd4, 0x8d, 0x56, 0xa7, 0xc6, 0x07, 0xb0, 0xe7, 0x9a, 0x9a, 0xa5, 0x2b, 0x63, 0xfb, 0xbd, 0xa2, 0xe9, 0x3b, + 0x31, 0x32, 0x59, 0xa3, 0xfc, 0x76, 0xee, 0x51, 0x3b, 0x1e, 0xda, 0x2e, 0xd5, 0x55, 0x82, 0x61, 0x56, 0x17, 0x0c, + 0x8b, 0x50, 0x4f, 0x75, 0x17, 0x5b, 0x33, 0x15, 0x0f, 0xd5, 0x5a, 0x2b, 0x07, 0x82, 0x85, 0x47, 0x60, 0x9c, 0xac, + 0xf4, 0x0f, 0xde, 0x46, 0x13, 0x86, 0x14, 0xf5, 0xd6, 0x35, 0x90, 0x0e, 0x04, 0x34, 0xe9, 0x2f, 0xaa, 0x37, 0xe6, + 0x0a, 0xab, 0xa9, 0xbe, 0xbf, 0x62, 0xb0, 0x22, 0xc0, 0xbe, 0x2e, 0x57, 0x2c, 0x11, 0xe9, 0x4d, 0xc9, 0xce, 0x8a, + 0x3e, 0xa2, 0x4c, 0xac, 0x09, 0x29, 0x78, 0x40, 0x1e, 0x96, 0x7f, 0x61, 0xe1, 0x54, 0x2b, 0x85, 0x23, 0x43, 0x99, + 0x02, 0x74, 0x26, 0x25, 0x00, 0xe2, 0x92, 0x3e, 0x6b, 0x1b, 0x0b, 0xc9, 0x76, 0x80, 0x7c, 0xe0, 0x8f, 0x92, 0x88, + 0x3b, 0xad, 0x9d, 0xa6, 0x0b, 0x7c, 0x08, 0x42, 0x1c, 0x74, 0x04, 0x98, 0xf7, 0x15, 0x2a, 0x1c, 0x51, 0x89, 0x5d, + 0xe6, 0x83, 0x51, 0x34, 0x8e, 0x47, 0xdc, 0x49, 0x90, 0x79, 0xdc, 0x92, 0x25, 0xa0, 0x64, 0xf4, 0xbe, 0x02, 0x65, + 0xc1, 0x84, 0x74, 0x11, 0xd5, 0x4a, 0xa0, 0x31, 0x05, 0x29, 0x49, 0x29, 0xd2, 0x82, 0x0a, 0x02, 0x43, 0xa8, 0x74, + 0x14, 0x47, 0x81, 0x7e, 0x8b, 0x7b, 0x62, 0xd0, 0x60, 0xc9, 0xa2, 0x8c, 0x7b, 0xf1, 0x72, 0x21, 0xa8, 0x61, 0x9f, + 0x67, 0xaf, 0xb3, 0x0b, 0x96, 0x3f, 0x8b, 0x10, 0xf6, 0x40, 0x74, 0x2f, 0x41, 0xd2, 0x93, 0x40, 0xe7, 0x1d, 0xc5, + 0x2b, 0xe7, 0x84, 0x34, 0x2c, 0xc4, 0x24, 0x46, 0x45, 0x08, 0x76, 0x0b, 0xd1, 0x3e, 0xc5, 0x2d, 0x45, 0x7b, 0x0f, + 0x55, 0x09, 0xd7, 0xbc, 0xb5, 0xff, 0xba, 0xce, 0x5b, 0x30, 0xc2, 0x54, 0x71, 0x6b, 0x7d, 0xc7, 0x82, 0x7b, 0x21, + 0x74, 0xb3, 0x23, 0x79, 0xcb, 0x50, 0x66, 0xa0, 0x3f, 0xae, 0xaf, 0x2b, 0x23, 0x1d, 0x94, 0xa9, 0x96, 0xe6, 0x08, + 0x81, 0xd8, 0x12, 0x6e, 0x09, 0xca, 0x08, 0x0d, 0xaf, 0x3c, 0x4b, 0x12, 0x43, 0x17, 0x79, 0x71, 0xc7, 0x59, 0x50, + 0x47, 0x00, 0xc5, 0xa4, 0xa6, 0x91, 0x7a, 0x2c, 0xd0, 0x15, 0xa8, 0x94, 0x94, 0x36, 0xf2, 0xaa, 0xb5, 0x11, 0x10, + 0xa7, 0x43, 0x96, 0x0b, 0x07, 0x4d, 0xea, 0x50, 0x98, 0x30, 0x05, 0x86, 0x66, 0x43, 0xf4, 0x1c, 0x24, 0x02, 0x60, + 0x9e, 0xf8, 0xe3, 0xac, 0xe0, 0xba, 0xce, 0x84, 0x3e, 0xbe, 0xbe, 0x8e, 0x85, 0xbf, 0x88, 0x0c, 0x90, 0xb3, 0x49, + 0x76, 0xce, 0x56, 0x40, 0xdd, 0x51, 0x83, 0x99, 0x20, 0x1b, 0xc3, 0x80, 0x12, 0x05, 0xd5, 0x32, 0x4d, 0x62, 0xb0, + 0xf4, 0x75, 0x03, 0x1f, 0x0c, 0x3a, 0x76, 0x89, 0x32, 0xc2, 0xed, 0x76, 0xbb, 0x4d, 0xaf, 0xe5, 0x96, 0x82, 0xe0, + 0xf3, 0x25, 0x8a, 0xde, 0xa0, 0x1f, 0xa5, 0x09, 0xbe, 0x4a, 0x16, 0x30, 0xd7, 0x50, 0x8a, 0xc2, 0x4f, 0x62, 0x9e, + 0x14, 0xc4, 0xae, 0x37, 0x84, 0x41, 0x39, 0x53, 0x82, 0x1b, 0x4d, 0x5c, 0xb1, 0x6d, 0x3f, 0x68, 0xb2, 0x69, 0x76, + 0x52, 0x3b, 0x4c, 0x2d, 0x8c, 0x5c, 0xf3, 0x42, 0x7b, 0xc0, 0xe6, 0xf2, 0x90, 0x4d, 0x8f, 0xd5, 0xc0, 0xeb, 0x00, + 0xa1, 0xf0, 0x74, 0x9d, 0x25, 0x94, 0xaa, 0xce, 0x52, 0x88, 0xeb, 0x0d, 0xf4, 0x51, 0x81, 0xb9, 0x8a, 0x04, 0x07, + 0x52, 0x20, 0x30, 0xf4, 0xc8, 0xc4, 0x7a, 0x3d, 0x83, 0xe5, 0x39, 0x8d, 0x06, 0x9f, 0x34, 0xb8, 0x15, 0xef, 0x2d, + 0xb2, 0x81, 0xb3, 0x50, 0x12, 0x1a, 0xe2, 0xca, 0xc4, 0x5b, 0x49, 0xe8, 0xda, 0x46, 0x01, 0x87, 0x6c, 0x89, 0xed, + 0x17, 0x17, 0x7a, 0x91, 0xdb, 0x25, 0x7b, 0x28, 0xff, 0xa9, 0xe2, 0x92, 0xf5, 0x2c, 0xc7, 0x94, 0x34, 0x60, 0x8a, + 0xf1, 0x60, 0x69, 0x16, 0x20, 0x01, 0xbe, 0x2b, 0x87, 0x71, 0xb1, 0x9e, 0x04, 0x7f, 0x28, 0x98, 0xcf, 0x8d, 0x99, + 0x6e, 0x85, 0x54, 0x4b, 0x38, 0x69, 0x06, 0x6b, 0xd0, 0xa4, 0xf1, 0xa0, 0x44, 0xcd, 0x57, 0x68, 0xa8, 0x10, 0xc7, + 0x9f, 0x89, 0x2a, 0x34, 0xc1, 0x10, 0x8c, 0xc2, 0xcb, 0x25, 0xc3, 0xa5, 0xcb, 0xa2, 0x45, 0xca, 0xd4, 0x98, 0x54, + 0xaa, 0x66, 0xb9, 0x14, 0x0c, 0x2c, 0xda, 0xad, 0xbe, 0xb4, 0xc4, 0x95, 0xc8, 0xcd, 0x42, 0x2d, 0x4c, 0x72, 0xe5, + 0x4d, 0x38, 0x05, 0xfa, 0x5d, 0xca, 0x7a, 0x37, 0xf1, 0x29, 0x14, 0x3e, 0x85, 0x6f, 0xf8, 0x50, 0x26, 0x6f, 0xe7, + 0x3d, 0x30, 0xf7, 0x6b, 0x95, 0x68, 0x9f, 0xfa, 0x28, 0x98, 0x5d, 0x2d, 0x74, 0x41, 0xa0, 0x48, 0x36, 0xc9, 0x7a, + 0x92, 0xdf, 0x50, 0x6c, 0x54, 0x9e, 0x51, 0xea, 0x8a, 0x0d, 0x52, 0xf3, 0x4a, 0x53, 0x2f, 0x73, 0x17, 0xec, 0xf7, + 0xb2, 0x94, 0x74, 0x62, 0x82, 0x32, 0xb1, 0x77, 0x13, 0x6d, 0xbc, 0x2c, 0x4c, 0x85, 0xf5, 0x2b, 0x8c, 0x9d, 0x1a, + 0x85, 0x32, 0x29, 0x02, 0x71, 0x6c, 0x7c, 0xac, 0x2c, 0x83, 0xd4, 0x5f, 0x61, 0x4f, 0x01, 0x28, 0x09, 0x2c, 0xbe, + 0xa6, 0x92, 0x17, 0x85, 0x75, 0x3a, 0x6e, 0x10, 0x1d, 0x2b, 0x11, 0x5a, 0x13, 0xf9, 0x5a, 0x9f, 0xc5, 0x7e, 0xcd, + 0x25, 0x34, 0x29, 0x59, 0xf4, 0x8a, 0xc0, 0x56, 0x81, 0x88, 0x4a, 0xb7, 0x25, 0xbd, 0x84, 0x1c, 0xd2, 0x65, 0xa2, + 0xd7, 0x46, 0x32, 0x68, 0x9d, 0x09, 0x89, 0x96, 0xf5, 0xc3, 0x08, 0xc5, 0x86, 0x58, 0x8b, 0x25, 0x42, 0x2e, 0xda, + 0x9b, 0xc4, 0x8a, 0xe8, 0x9c, 0x16, 0x68, 0xc2, 0x99, 0x3a, 0xdd, 0x71, 0x00, 0x1d, 0x10, 0xfb, 0x4b, 0xac, 0xb7, + 0xd2, 0xec, 0x74, 0xfd, 0xca, 0xe1, 0xbb, 0xbe, 0x1e, 0x73, 0xd7, 0x91, 0x06, 0x2f, 0xac, 0x59, 0x4f, 0xc9, 0xde, + 0xfd, 0xd7, 0xd8, 0x8a, 0xec, 0xcf, 0xaa, 0xa4, 0xf2, 0x14, 0x6a, 0x9c, 0x5b, 0x5f, 0xa7, 0x5a, 0x68, 0x51, 0x55, + 0x1c, 0x18, 0x52, 0xfd, 0x40, 0x29, 0xec, 0x0a, 0xe5, 0x03, 0x39, 0x74, 0xec, 0xba, 0x6e, 0x50, 0x90, 0xf3, 0xb2, + 0xb1, 0xca, 0x85, 0xdc, 0xda, 0x32, 0x7d, 0xa6, 0x73, 0x3d, 0xfc, 0x33, 0x07, 0x95, 0x73, 0x71, 0x95, 0x92, 0x05, + 0xf3, 0x4c, 0xa9, 0xa3, 0x25, 0x07, 0xb4, 0xd9, 0x41, 0x4f, 0x3b, 0xba, 0x88, 0x62, 0x6e, 0xe9, 0x51, 0x84, 0xa7, + 0x8d, 0xf2, 0x49, 0x1a, 0x1d, 0x80, 0x17, 0x9a, 0x90, 0xe4, 0x84, 0x9b, 0xb6, 0x68, 0x31, 0x18, 0x33, 0x0c, 0x81, + 0x2b, 0x7b, 0xc2, 0x94, 0x3d, 0x1b, 0x88, 0xb7, 0x1c, 0x78, 0x35, 0xec, 0xe5, 0x62, 0xf7, 0x9a, 0xf9, 0x0f, 0x6b, + 0x04, 0xb2, 0x6d, 0xa2, 0xea, 0xca, 0x85, 0x67, 0x29, 0x22, 0x31, 0xc2, 0xb6, 0x6a, 0x6c, 0x69, 0xeb, 0x77, 0x16, + 0xdc, 0xeb, 0xca, 0x31, 0xaf, 0x29, 0xd5, 0x05, 0x3d, 0xac, 0xdc, 0x1c, 0x6e, 0x3a, 0xf2, 0x62, 0x05, 0xdd, 0x8e, + 0x08, 0x0a, 0x81, 0x13, 0xa1, 0xec, 0x41, 0xcd, 0x0d, 0x44, 0x4a, 0xa6, 0xb4, 0x6a, 0x36, 0x4b, 0x86, 0x12, 0x58, + 0x70, 0x61, 0x99, 0xe4, 0xa3, 0x8b, 0x38, 0x49, 0xaa, 0xd2, 0x3f, 0x54, 0xc0, 0x8b, 0x61, 0x6f, 0x13, 0xed, 0x02, + 0xa3, 0x99, 0x02, 0xc1, 0xd5, 0x46, 0xd8, 0x47, 0xc7, 0xad, 0xd6, 0x5d, 0x44, 0x1c, 0x99, 0x19, 0x8d, 0xf8, 0x88, + 0x36, 0x64, 0xc9, 0x34, 0x6b, 0xef, 0xbf, 0xc0, 0x90, 0x9a, 0x81, 0x0f, 0xaa, 0x33, 0x2a, 0xfe, 0x55, 0xf6, 0xd4, + 0xaf, 0x44, 0xef, 0x56, 0xd5, 0xb5, 0x18, 0x50, 0x51, 0x81, 0x0f, 0x33, 0xc4, 0xd2, 0x54, 0x81, 0x80, 0x5c, 0x0f, + 0xeb, 0x70, 0xb7, 0x46, 0x1a, 0x2c, 0x28, 0x05, 0xd6, 0x5a, 0xd9, 0xbd, 0xbe, 0x2d, 0x98, 0x43, 0xa1, 0x70, 0xd1, + 0xff, 0x59, 0x36, 0x99, 0xa2, 0x65, 0xb6, 0xc0, 0xd4, 0xd0, 0xe0, 0xe3, 0x42, 0x7d, 0xb9, 0xa2, 0xac, 0xd6, 0x87, + 0x76, 0x64, 0x8d, 0x9f, 0xb4, 0xa3, 0x0c, 0x0e, 0xd5, 0x4c, 0x17, 0xd5, 0xed, 0xe6, 0x45, 0x11, 0xb3, 0x8a, 0xc7, + 0x7d, 0xd2, 0xdb, 0xda, 0x9a, 0xf4, 0x34, 0x0d, 0x48, 0x26, 0x49, 0x86, 0x37, 0x19, 0xa0, 0xac, 0x88, 0x33, 0x2f, + 0x17, 0xc8, 0x37, 0x2f, 0x4b, 0x5c, 0xbf, 0xef, 0x3b, 0xfb, 0x35, 0xcf, 0xda, 0xdb, 0x5f, 0xef, 0x22, 0x57, 0x75, + 0xd2, 0x83, 0x3c, 0xea, 0x43, 0xd1, 0x92, 0x4d, 0x19, 0xce, 0x27, 0xd9, 0x90, 0x05, 0x36, 0x74, 0x4f, 0xed, 0x52, + 0x6e, 0x9a, 0x08, 0x36, 0x07, 0xf8, 0x7f, 0xf3, 0x0f, 0xf5, 0x48, 0x6a, 0xb0, 0x0f, 0x2c, 0xa0, 0xcd, 0x85, 0x2f, + 0xc3, 0xb3, 0x24, 0x3b, 0x8d, 0x92, 0x43, 0xa1, 0xc0, 0x6b, 0x2d, 0xbf, 0x01, 0x97, 0x91, 0x2c, 0x56, 0x43, 0x49, + 0x7d, 0xd9, 0xfb, 0x32, 0xb8, 0xbd, 0x47, 0xe5, 0xad, 0xd8, 0x2d, 0xbf, 0xe9, 0xb7, 0x6c, 0x15, 0x11, 0xfb, 0xc9, + 0x9c, 0x0e, 0x34, 0x4e, 0x01, 0x94, 0x39, 0x04, 0x4d, 0x56, 0x78, 0x03, 0x1e, 0xfe, 0xd4, 0xfb, 0x49, 0xb9, 0xd4, + 0x19, 0xb8, 0x10, 0xe0, 0xe4, 0x27, 0x31, 0x6f, 0xe0, 0x79, 0xa4, 0xed, 0xcd, 0x45, 0x05, 0xc6, 0x15, 0x29, 0x2e, + 0x5d, 0x2a, 0x6f, 0xd0, 0x3b, 0x0e, 0x4f, 0xa0, 0xd9, 0xe6, 0xe6, 0xdc, 0x79, 0x13, 0xf1, 0xb1, 0x9f, 0x47, 0xe9, + 0x30, 0x9b, 0x38, 0xee, 0xb6, 0x6d, 0xbb, 0x7e, 0x41, 0x9e, 0xc8, 0x67, 0x6e, 0xb9, 0x79, 0xe2, 0x0d, 0x79, 0x68, + 0xf7, 0xec, 0xed, 0x63, 0xef, 0x90, 0x87, 0x27, 0x7b, 0x9b, 0xf3, 0x21, 0x2f, 0xbb, 0x27, 0xde, 0xa5, 0x8e, 0xb9, + 0x7b, 0xef, 0x51, 0xca, 0x40, 0xaf, 0xb0, 0x7b, 0x29, 0xc1, 0x00, 0x76, 0xa3, 0xf8, 0x3b, 0x48, 0xb9, 0x8f, 0x74, + 0x20, 0x22, 0xe3, 0xb4, 0xd7, 0xd7, 0x76, 0x46, 0x11, 0x03, 0x7b, 0x43, 0x3b, 0xab, 0x5b, 0x5b, 0x95, 0x9a, 0xaf, + 0x4a, 0xbd, 0x19, 0x0f, 0x6b, 0x9e, 0xba, 0xf7, 0x92, 0x8e, 0x56, 0xea, 0x1b, 0x79, 0x26, 0x82, 0x36, 0xcb, 0x76, + 0x82, 0x63, 0x6c, 0xf1, 0xd5, 0xdb, 0xfa, 0x48, 0x44, 0x29, 0xfc, 0x18, 0xac, 0x97, 0x08, 0xd4, 0x37, 0x38, 0x38, + 0xde, 0x61, 0xb8, 0xb3, 0xe7, 0xf4, 0x02, 0x67, 0xa3, 0xd1, 0xb8, 0xfe, 0x61, 0xe7, 0xe8, 0xc7, 0xa8, 0xf1, 0xcb, + 0x7e, 0xe3, 0xfb, 0xbe, 0x7b, 0xed, 0xfc, 0xb0, 0xd3, 0x3b, 0x92, 0x4f, 0x47, 0x3f, 0x76, 0x7f, 0x28, 0xfa, 0x7f, + 0x12, 0x85, 0x9b, 0xae, 0xbb, 0x73, 0xe6, 0x4d, 0x79, 0xb8, 0xd3, 0x68, 0x74, 0xe1, 0xdb, 0x19, 0x7c, 0xc3, 0xcf, + 0x53, 0xf8, 0xb8, 0x3e, 0xb2, 0xfe, 0xc3, 0x0f, 0xe9, 0x7f, 0xfc, 0x21, 0xef, 0xe3, 0x98, 0x47, 0x3f, 0xfe, 0x50, + 0xd8, 0xf7, 0xbb, 0xe1, 0x4e, 0x7f, 0xdb, 0x75, 0x74, 0xcd, 0x9f, 0xc2, 0xea, 0x2b, 0xb4, 0x3a, 0xfa, 0x51, 0x3e, + 0xd9, 0xf7, 0x4f, 0xf6, 0xba, 0x61, 0xff, 0xda, 0xb1, 0xaf, 0xef, 0xbb, 0xd7, 0xae, 0x7b, 0xbd, 0x89, 0xf3, 0x9c, + 0xc3, 0xe8, 0xf7, 0xe1, 0x73, 0x04, 0x9f, 0x36, 0x7c, 0x6e, 0xc2, 0xe7, 0x8f, 0xd0, 0x4d, 0xc4, 0xdf, 0xae, 0x29, + 0x16, 0x72, 0x8d, 0x07, 0x16, 0x11, 0xac, 0x82, 0xbb, 0xb9, 0x13, 0x7b, 0x13, 0x22, 0x1a, 0xec, 0x43, 0xdf, 0xf7, + 0x31, 0x4c, 0xea, 0xcc, 0x8f, 0x37, 0x61, 0xd1, 0x91, 0x73, 0x36, 0x03, 0xee, 0x89, 0xc8, 0x41, 0x11, 0x30, 0x71, + 0xb6, 0x5a, 0xe0, 0xe1, 0xaa, 0x37, 0x0c, 0x27, 0xdc, 0x01, 0xa3, 0xe0, 0x03, 0xc7, 0x2f, 0x6d, 0xd7, 0x7b, 0x21, + 0xcf, 0x0c, 0x71, 0x9f, 0x0b, 0xd6, 0x4a, 0x33, 0x61, 0xd2, 0xd8, 0xae, 0x37, 0x5d, 0x51, 0x09, 0xdb, 0x3a, 0x3d, + 0x83, 0xba, 0x63, 0x11, 0xa3, 0xfe, 0x96, 0x45, 0x9f, 0x70, 0x4b, 0xbe, 0x35, 0x0e, 0x81, 0x97, 0x2c, 0xf9, 0x45, + 0xa3, 0xd1, 0xb0, 0x11, 0x85, 0x3b, 0xf6, 0x94, 0xc1, 0x0c, 0x4b, 0x26, 0x22, 0x23, 0xa5, 0x29, 0x2c, 0x5b, 0x98, + 0xfc, 0x7d, 0x94, 0xf3, 0xcd, 0xca, 0xb0, 0x0d, 0xeb, 0x96, 0xec, 0x82, 0xa5, 0x7f, 0x87, 0x29, 0xd0, 0xb4, 0xa4, + 0xf3, 0x0f, 0x73, 0xfc, 0x30, 0x23, 0xb4, 0x3e, 0x38, 0x0c, 0x3c, 0xf4, 0x02, 0xe4, 0x8e, 0xe8, 0xe7, 0xbc, 0x47, + 0x35, 0x06, 0xff, 0xcb, 0x30, 0x83, 0x27, 0xe6, 0xc3, 0x10, 0xcd, 0xbc, 0xd4, 0xc1, 0xad, 0x0c, 0xc5, 0xfd, 0x2b, + 0xdc, 0x19, 0x59, 0xe9, 0x1d, 0x84, 0x6a, 0xc7, 0x1c, 0xe6, 0x8c, 0x7d, 0x1b, 0x25, 0x9f, 0x58, 0xee, 0x5c, 0x7a, + 0xad, 0xf6, 0x67, 0xd4, 0xd9, 0x43, 0xdb, 0xec, 0x4d, 0x75, 0x8c, 0xa6, 0xcd, 0x02, 0x79, 0x44, 0xd8, 0x68, 0x79, + 0x28, 0x31, 0x88, 0x04, 0xb9, 0x97, 0x86, 0x6d, 0xe2, 0x70, 0x7b, 0xaf, 0x38, 0x3f, 0xeb, 0xda, 0x81, 0x6d, 0x83, + 0xc5, 0x7f, 0x48, 0x61, 0x2b, 0x61, 0x58, 0x34, 0x3b, 0x6c, 0x2f, 0xee, 0xb0, 0xed, 0xed, 0x2a, 0xe0, 0x84, 0x07, + 0xe9, 0xd4, 0x3d, 0xf1, 0x22, 0x6f, 0x1c, 0xc2, 0x80, 0x03, 0x68, 0x86, 0x5d, 0x3a, 0x83, 0xbd, 0x58, 0x4e, 0x03, + 0xb2, 0x3e, 0xf3, 0x93, 0xa8, 0xe0, 0xaf, 0x30, 0x1e, 0x11, 0x0e, 0xc0, 0xd8, 0xcf, 0x7c, 0x76, 0xc9, 0x06, 0xca, + 0xce, 0x00, 0x42, 0x45, 0x6e, 0xc7, 0x1d, 0x84, 0x46, 0x33, 0x98, 0x3b, 0x0c, 0x0f, 0x7b, 0x36, 0xec, 0x25, 0xd8, + 0x95, 0x61, 0x74, 0xd4, 0xea, 0xf7, 0xb2, 0x70, 0xca, 0x03, 0x4d, 0x5b, 0x59, 0x74, 0x56, 0x2b, 0x6a, 0xf7, 0x7b, + 0xce, 0x26, 0x18, 0xe9, 0x60, 0x8b, 0x3b, 0xf8, 0x84, 0x11, 0x8a, 0x3c, 0xfc, 0xc0, 0xce, 0x5e, 0x5c, 0x4e, 0x1d, + 0x7b, 0x6f, 0xc7, 0xde, 0xc6, 0x52, 0xcf, 0x06, 0xf6, 0x02, 0x0a, 0x86, 0xa7, 0xae, 0xd9, 0x79, 0xb7, 0x8f, 0xa0, + 0x62, 0x21, 0x4e, 0x7e, 0xda, 0xb3, 0xbb, 0x62, 0xea, 0x26, 0x0c, 0x9a, 0xc9, 0xe5, 0xc7, 0x15, 0x3d, 0x24, 0x54, + 0x55, 0x57, 0x05, 0x1d, 0x94, 0xb5, 0x03, 0x67, 0x6c, 0x22, 0xd1, 0xc0, 0xc9, 0x24, 0x15, 0xc0, 0xe1, 0xc1, 0x66, + 0x30, 0xa9, 0xd1, 0x6d, 0xb7, 0xdf, 0x3b, 0x0d, 0xee, 0xdb, 0xf7, 0xd5, 0xc3, 0x08, 0x90, 0xe1, 0x62, 0xfa, 0x11, + 0x48, 0x3b, 0xfc, 0x3c, 0xe7, 0x80, 0xe4, 0x29, 0x15, 0x4d, 0x65, 0xd1, 0x19, 0x16, 0x1d, 0x06, 0x08, 0xaa, 0x97, + 0x6b, 0xeb, 0x4f, 0xac, 0xc9, 0x30, 0x24, 0xd8, 0xc1, 0x16, 0x3a, 0x62, 0xdb, 0xad, 0x3e, 0x9e, 0x37, 0xe4, 0xbc, + 0xf8, 0x36, 0xe6, 0xa0, 0x12, 0x76, 0xba, 0xb6, 0xdb, 0xb3, 0x2d, 0x5c, 0xda, 0x4e, 0xba, 0x1d, 0x0a, 0x0a, 0xc7, + 0xdb, 0x87, 0x3c, 0x18, 0x77, 0xc3, 0x66, 0xcf, 0x29, 0x64, 0xb8, 0x11, 0xcf, 0x2d, 0x85, 0x04, 0x6f, 0x7a, 0x63, + 0x10, 0xe8, 0xc8, 0xb9, 0x9b, 0xf6, 0xb6, 0x2a, 0x84, 0xa2, 0xe3, 0xed, 0xa1, 0x1b, 0xc4, 0xf0, 0xe1, 0x34, 0x90, + 0x69, 0xc6, 0xba, 0xaf, 0xd2, 0xcc, 0xcc, 0x0d, 0x86, 0xca, 0x22, 0x4f, 0xc2, 0x74, 0xdb, 0xc1, 0x08, 0x2d, 0x48, + 0xda, 0xbd, 0x1e, 0xc0, 0xb0, 0xed, 0x28, 0x4e, 0xdb, 0x51, 0xac, 0xa6, 0xec, 0xf3, 0x23, 0xbd, 0x1c, 0x03, 0xde, + 0x1b, 0xa8, 0xf3, 0x58, 0xd4, 0x3e, 0x00, 0x56, 0x90, 0x78, 0x45, 0x5f, 0x9d, 0x79, 0xbd, 0xac, 0x9d, 0x6f, 0xcd, + 0x95, 0x28, 0xe2, 0x9e, 0x21, 0xa1, 0x58, 0xa9, 0xdd, 0x30, 0x61, 0x6e, 0x4f, 0x91, 0x18, 0x9a, 0xe5, 0x43, 0xd8, + 0x63, 0xa1, 0x0a, 0xb0, 0x67, 0xe6, 0xb6, 0x48, 0xc2, 0xaa, 0xb9, 0x77, 0x04, 0xac, 0xdd, 0x0f, 0xdf, 0x08, 0x77, + 0xaa, 0xa3, 0xa2, 0xf9, 0x2c, 0x09, 0x5f, 0x2e, 0x1c, 0x17, 0x47, 0x78, 0x22, 0x74, 0xe0, 0x0f, 0x66, 0x39, 0xc8, + 0x03, 0xfe, 0x16, 0x2c, 0x83, 0x50, 0x36, 0x45, 0x47, 0x0f, 0x8f, 0x80, 0x3d, 0x42, 0x7c, 0x21, 0x6c, 0x6e, 0x54, + 0xa3, 0x45, 0x49, 0xc6, 0x0b, 0x1d, 0x0c, 0x77, 0x98, 0x74, 0xed, 0x51, 0x30, 0xc8, 0x13, 0x63, 0x07, 0xcf, 0xfc, + 0xfd, 0x01, 0x56, 0xe3, 0x04, 0x85, 0x5b, 0xd2, 0x6e, 0xab, 0xc4, 0xdf, 0x81, 0x9f, 0x82, 0x04, 0xc7, 0x3a, 0xf0, + 0xb3, 0xb6, 0xb6, 0x12, 0x89, 0xd4, 0x5e, 0xd6, 0xa1, 0x93, 0x08, 0x8c, 0x07, 0x17, 0x7e, 0x0a, 0xd5, 0x48, 0x22, + 0x2a, 0x22, 0x0b, 0xd4, 0x3c, 0x55, 0xab, 0xe0, 0x3b, 0x32, 0x23, 0xf0, 0x8c, 0x92, 0x5c, 0xd0, 0x50, 0xd4, 0x8d, + 0x45, 0x2c, 0xdf, 0x75, 0xe9, 0x68, 0x0b, 0x0f, 0x20, 0x05, 0xa3, 0x09, 0x86, 0x71, 0x29, 0x28, 0x59, 0xf1, 0xdf, + 0xb1, 0x11, 0x2b, 0x1f, 0x1f, 0xa5, 0xdb, 0xdb, 0x7d, 0x71, 0x6e, 0x41, 0x8c, 0xc3, 0x8c, 0xe8, 0x6a, 0x5c, 0x01, + 0x50, 0x9f, 0xce, 0x89, 0xeb, 0x81, 0x69, 0xc5, 0x9a, 0x2e, 0xc5, 0x3e, 0x39, 0xcc, 0x00, 0x14, 0xdc, 0x71, 0x8e, + 0xfc, 0xde, 0x9f, 0xfb, 0xe0, 0x1e, 0xfb, 0x7f, 0x72, 0x77, 0x94, 0xa0, 0xe9, 0xc8, 0x33, 0xc5, 0x39, 0x9d, 0xb1, + 0xb6, 0x3c, 0x8a, 0x8d, 0x06, 0x20, 0xf5, 0x00, 0x03, 0xd0, 0xe6, 0x20, 0x13, 0x2a, 0x0e, 0x42, 0x8e, 0x0a, 0x6c, + 0x1f, 0x37, 0x3f, 0xc3, 0x9d, 0xfd, 0x9c, 0x07, 0x60, 0xc1, 0xa8, 0xa7, 0xd7, 0xf0, 0xf4, 0x67, 0xfd, 0xf4, 0x13, + 0x0f, 0x7e, 0x29, 0x65, 0xe8, 0xbe, 0x36, 0xc5, 0x23, 0x35, 0x45, 0x29, 0x96, 0xc8, 0xa0, 0x21, 0x77, 0x97, 0x63, + 0x36, 0xcc, 0x2d, 0x81, 0x18, 0x4a, 0x74, 0x81, 0x8d, 0x16, 0x9d, 0x21, 0x71, 0x5d, 0x93, 0x14, 0x46, 0x2e, 0x81, + 0x89, 0x70, 0xc5, 0xb7, 0x48, 0x4f, 0xd6, 0x6d, 0xba, 0xf3, 0x5a, 0x5b, 0xb2, 0xef, 0xd8, 0x64, 0xca, 0xaf, 0x0e, + 0x48, 0xd1, 0x07, 0x32, 0x6d, 0x40, 0x9c, 0x9d, 0x37, 0x3b, 0xf1, 0x1e, 0xeb, 0xc4, 0x20, 0xd5, 0x0b, 0xc5, 0x62, + 0xb8, 0x57, 0xbd, 0xf7, 0x18, 0xa5, 0x34, 0x99, 0xc9, 0xab, 0xa1, 0xd7, 0x96, 0xe8, 0x6d, 0x6f, 0x03, 0x82, 0x1d, + 0xa3, 0x2b, 0x13, 0x5d, 0xcb, 0x52, 0xd0, 0x04, 0x20, 0x7a, 0x52, 0x67, 0x39, 0xe2, 0x38, 0xcc, 0x66, 0x83, 0xe2, + 0x21, 0x77, 0x57, 0x8e, 0x8a, 0x63, 0x62, 0x77, 0x99, 0xb0, 0x03, 0x98, 0x11, 0x97, 0x37, 0x5a, 0x22, 0x3a, 0x2c, + 0xfa, 0xeb, 0xf8, 0xf6, 0xb1, 0xc7, 0xb7, 0x5b, 0x2e, 0x68, 0x90, 0xda, 0x58, 0x8f, 0xab, 0xb1, 0xa0, 0x3e, 0x3c, + 0xd6, 0x54, 0x2a, 0xf3, 0xed, 0xed, 0xb2, 0x7e, 0x54, 0xab, 0x76, 0x70, 0xed, 0x34, 0xe5, 0x72, 0x31, 0x1b, 0x84, + 0x03, 0x11, 0x13, 0x28, 0xd0, 0xd2, 0xca, 0x8a, 0x01, 0x86, 0x94, 0xe5, 0x28, 0x9f, 0x42, 0xee, 0xc5, 0x65, 0xa9, + 0x53, 0x5f, 0x9e, 0xc9, 0xa0, 0x23, 0x9e, 0x7a, 0x92, 0xb1, 0x02, 0xac, 0xe6, 0x65, 0x5e, 0x42, 0x4b, 0x04, 0x98, + 0xbf, 0x50, 0x39, 0x34, 0xc2, 0x02, 0x89, 0x42, 0xc3, 0x2c, 0x51, 0xc6, 0x67, 0x1e, 0xc6, 0xa0, 0xed, 0x9f, 0xd5, + 0x62, 0x5f, 0xb9, 0x32, 0x3a, 0xf2, 0xa3, 0xa2, 0x1f, 0x50, 0xfd, 0x4c, 0x4a, 0xb0, 0x71, 0xf8, 0x11, 0xd8, 0xa8, + 0x72, 0x3c, 0x49, 0x10, 0x3e, 0x8f, 0x73, 0x46, 0x9e, 0xc2, 0xa6, 0x84, 0x59, 0x9a, 0xb6, 0x91, 0x6a, 0x17, 0x99, + 0x41, 0x28, 0x17, 0xe6, 0x1f, 0x1b, 0x67, 0x17, 0x69, 0xb8, 0xd4, 0x1a, 0xcc, 0x8f, 0x77, 0x26, 0x40, 0xe9, 0xf5, + 0x75, 0x2a, 0x7c, 0xdc, 0x88, 0xec, 0x0d, 0x5d, 0x31, 0xee, 0x29, 0xa4, 0x02, 0x27, 0x22, 0x8b, 0x87, 0xce, 0x50, + 0x68, 0x84, 0x43, 0x3a, 0x45, 0x2e, 0x5c, 0x63, 0xd3, 0x17, 0x3d, 0xed, 0x1b, 0x65, 0xa1, 0x93, 0x80, 0x10, 0x10, + 0xb8, 0x1b, 0xd6, 0x54, 0xd6, 0xcb, 0x82, 0x84, 0x4a, 0xd1, 0xcf, 0x01, 0xfc, 0xc3, 0x48, 0x52, 0x00, 0xec, 0x87, + 0x6a, 0xa4, 0x88, 0xb2, 0x2c, 0x70, 0x01, 0x68, 0xae, 0x03, 0x5c, 0x09, 0x5f, 0x18, 0xa8, 0x30, 0x3d, 0xcd, 0xca, + 0x4a, 0xa1, 0x44, 0x9e, 0xae, 0x48, 0x59, 0x23, 0x99, 0x7c, 0x8e, 0x0e, 0x9f, 0xf2, 0xae, 0xdf, 0x4a, 0x3c, 0x74, + 0xc1, 0x73, 0x58, 0x56, 0xf5, 0xfd, 0x4d, 0xc8, 0xc8, 0xb9, 0x06, 0x5d, 0x21, 0x85, 0xfe, 0x92, 0x93, 0xbc, 0xff, + 0xc6, 0xaf, 0x6a, 0xa9, 0x31, 0x94, 0x7d, 0x5c, 0xd5, 0x0c, 0xcb, 0xcb, 0x69, 0x15, 0xa6, 0x20, 0xe0, 0xe6, 0x2c, + 0x09, 0xe6, 0x52, 0x43, 0x80, 0x85, 0xed, 0x91, 0x56, 0x0a, 0x8a, 0x52, 0x87, 0x77, 0x9e, 0x83, 0x15, 0x60, 0x1c, + 0x6a, 0xa9, 0x64, 0x1a, 0x49, 0x7c, 0xa9, 0x44, 0x81, 0x29, 0x0f, 0x06, 0xe0, 0xa7, 0x2e, 0x9e, 0x74, 0x5d, 0xba, + 0x7e, 0x3c, 0xc1, 0xd4, 0x1e, 0x02, 0x3d, 0xf6, 0x36, 0xc0, 0x94, 0xa8, 0xeb, 0xb0, 0x9c, 0x38, 0x34, 0xad, 0x69, + 0x16, 0x30, 0x63, 0x9a, 0xa0, 0x25, 0x9b, 0x60, 0xcb, 0x15, 0x60, 0x1f, 0x89, 0xed, 0x59, 0xad, 0x80, 0xd0, 0x35, + 0x68, 0x60, 0xc8, 0x5d, 0x2a, 0xb4, 0x30, 0xeb, 0xb4, 0xa9, 0x08, 0xf7, 0x67, 0x8f, 0x49, 0x2b, 0x38, 0xf5, 0x52, + 0x1a, 0xf8, 0x20, 0x3e, 0x4d, 0x30, 0xf1, 0x05, 0xb1, 0x02, 0x3b, 0x38, 0x68, 0x2d, 0x36, 0x05, 0x4e, 0xc5, 0x45, + 0x4a, 0x61, 0x59, 0x51, 0x6a, 0xc3, 0x87, 0x14, 0xd9, 0xba, 0xcb, 0x23, 0xdd, 0x85, 0x58, 0x00, 0x3b, 0xfd, 0xc2, + 0xa1, 0x83, 0xac, 0x97, 0x01, 0x83, 0x73, 0xad, 0x71, 0x10, 0xf8, 0xed, 0xed, 0xa4, 0x5f, 0x66, 0x48, 0xb9, 0x25, + 0x56, 0x17, 0x90, 0xe3, 0x76, 0x58, 0xc0, 0x1d, 0x84, 0xa5, 0xb2, 0xc7, 0xf3, 0x72, 0x82, 0xcb, 0xa5, 0x2c, 0xe4, + 0xc5, 0x74, 0x2c, 0x9a, 0xcf, 0xad, 0x34, 0x9b, 0x8e, 0xb7, 0xe2, 0x83, 0x82, 0xbf, 0xe7, 0xc4, 0xd2, 0xaa, 0xa7, + 0xd4, 0x0a, 0x8f, 0x32, 0xb7, 0x64, 0x9d, 0x92, 0x5a, 0x6d, 0x37, 0x50, 0x8d, 0xf0, 0x34, 0x0d, 0x1b, 0x81, 0x10, + 0x13, 0x5c, 0xfc, 0x61, 0x91, 0x89, 0x69, 0x6f, 0x09, 0xa9, 0x23, 0xec, 0x1e, 0xca, 0x09, 0x6e, 0x6b, 0x9e, 0x7d, + 0x19, 0x4e, 0xd7, 0x33, 0xf7, 0xbe, 0xc1, 0xdc, 0x4f, 0x43, 0x66, 0x30, 0x7a, 0x2c, 0x13, 0x7e, 0x64, 0xec, 0xa3, + 0x50, 0x55, 0xcf, 0xce, 0xc2, 0x4a, 0x64, 0x89, 0x6f, 0xc6, 0x51, 0x87, 0x71, 0x2a, 0x5a, 0x13, 0x64, 0xd7, 0xd7, + 0xb9, 0xb9, 0x17, 0x28, 0x68, 0xea, 0xb1, 0x7a, 0x9c, 0xb6, 0x62, 0x67, 0x23, 0x12, 0xb9, 0xff, 0xa6, 0x16, 0x89, + 0xac, 0xf8, 0x1c, 0x47, 0x5a, 0x73, 0x90, 0xfb, 0xec, 0x6c, 0x79, 0x93, 0x0a, 0xdd, 0xa2, 0xd1, 0x36, 0xf6, 0xa8, + 0x3e, 0x90, 0xd4, 0x33, 0x2a, 0xb0, 0xaa, 0xb1, 0xb7, 0xb6, 0x5a, 0x22, 0xdd, 0x52, 0x29, 0x36, 0x0c, 0x69, 0x85, + 0xcc, 0x18, 0x05, 0x83, 0x92, 0x22, 0x03, 0x35, 0xca, 0xd7, 0x08, 0x86, 0x7d, 0x6a, 0x00, 0x8a, 0x73, 0x75, 0xf5, + 0xd3, 0x52, 0xb2, 0x85, 0x80, 0x04, 0x64, 0x13, 0x8a, 0x35, 0x62, 0x66, 0xe4, 0x93, 0x8f, 0xc0, 0x79, 0x3d, 0x8e, + 0x8e, 0x01, 0xc8, 0x60, 0xb1, 0xe9, 0xc1, 0xc4, 0xb6, 0x89, 0x28, 0xfa, 0x6c, 0xe0, 0x25, 0x00, 0x3b, 0xad, 0x42, + 0xa3, 0x1f, 0xaa, 0x14, 0x30, 0x64, 0x03, 0x37, 0xe0, 0x55, 0x58, 0x6e, 0xff, 0x25, 0xb4, 0x83, 0xc7, 0x17, 0xb2, + 0xf9, 0x26, 0xe6, 0x09, 0x56, 0xb1, 0x3b, 0xbf, 0xb2, 0xac, 0xc5, 0xb9, 0xd3, 0xe1, 0x42, 0xbd, 0xa2, 0x84, 0xa8, + 0x3d, 0xc0, 0xda, 0x97, 0x9c, 0x60, 0xc4, 0xe7, 0x37, 0x94, 0x75, 0xa8, 0xc6, 0x2d, 0xf7, 0x35, 0x5a, 0x84, 0xe9, + 0x32, 0x69, 0x0c, 0x4a, 0xd6, 0xfd, 0x64, 0xc4, 0xbd, 0x3c, 0x10, 0xb1, 0xe0, 0x0a, 0x47, 0x23, 0x6c, 0xbe, 0x80, + 0x24, 0x7d, 0xdb, 0xa7, 0x03, 0xf6, 0xcd, 0xc5, 0x5e, 0x40, 0x99, 0x8f, 0x15, 0xa9, 0x24, 0xa4, 0x34, 0xbb, 0x21, + 0x92, 0x84, 0xb5, 0x22, 0x4f, 0x9d, 0x0f, 0x1c, 0xed, 0x73, 0x2b, 0x89, 0x60, 0x04, 0x27, 0x71, 0xba, 0xf2, 0x70, + 0x51, 0x80, 0xab, 0xe8, 0x88, 0xe9, 0x9b, 0xa0, 0xfc, 0x06, 0xb9, 0xbd, 0x94, 0x5c, 0x5b, 0x68, 0x18, 0x9e, 0x21, + 0xc1, 0xaa, 0x48, 0x04, 0x3a, 0x0a, 0x80, 0xe3, 0x4a, 0xcf, 0x03, 0x4c, 0xf8, 0xda, 0xde, 0x04, 0x80, 0x44, 0x56, + 0x90, 0xb3, 0x14, 0xe8, 0x06, 0x2c, 0x57, 0xc7, 0xa9, 0x51, 0x91, 0xb8, 0xb8, 0x31, 0x5d, 0xdd, 0xd2, 0x9f, 0xa0, + 0xe5, 0x4c, 0x86, 0x98, 0x0e, 0x82, 0x80, 0x4c, 0x7d, 0xca, 0x9d, 0x9c, 0xa6, 0x13, 0xd6, 0xe7, 0xd4, 0xa9, 0x4d, + 0xdd, 0xe1, 0xd4, 0xcd, 0x93, 0xd4, 0x62, 0x75, 0xda, 0x94, 0x12, 0x31, 0x29, 0x31, 0x8f, 0x65, 0x2a, 0xb6, 0x12, + 0x77, 0x6e, 0x7d, 0xa3, 0x85, 0xb4, 0xd1, 0x8e, 0x65, 0x0e, 0xb6, 0x96, 0xf7, 0x42, 0xb4, 0xbf, 0x24, 0xc2, 0xb3, + 0x12, 0x19, 0x6b, 0x3e, 0xe3, 0x8e, 0x89, 0x60, 0xf5, 0x60, 0x2a, 0xf2, 0x0f, 0x8e, 0x4e, 0xb3, 0x37, 0xe8, 0x41, + 0xea, 0x0d, 0x24, 0x66, 0x4d, 0x7c, 0xe7, 0xd2, 0x50, 0x47, 0x08, 0x54, 0x46, 0xb5, 0x4c, 0xc7, 0x89, 0xa5, 0xe2, + 0x92, 0x7c, 0xf5, 0x5e, 0x1f, 0xe7, 0x1b, 0xdf, 0x17, 0x56, 0x23, 0x88, 0xc1, 0x5b, 0x28, 0xfa, 0x9e, 0x14, 0xe1, + 0x39, 0x2c, 0xcf, 0xf6, 0x76, 0xa7, 0xd8, 0x63, 0x55, 0x88, 0xa4, 0x82, 0x31, 0xc6, 0x8c, 0x62, 0xdc, 0x13, 0x35, + 0xb5, 0x88, 0xc4, 0x96, 0xad, 0xc3, 0x02, 0x0f, 0x00, 0xa0, 0xa5, 0x29, 0xbd, 0xcc, 0xb6, 0xea, 0x3c, 0x97, 0xf0, + 0x31, 0xf2, 0x50, 0x64, 0xe3, 0xf7, 0x6b, 0x32, 0x50, 0x10, 0xee, 0x8d, 0x96, 0x87, 0x89, 0x71, 0xb0, 0x8a, 0x42, + 0x16, 0xe8, 0x0d, 0xda, 0xa9, 0x12, 0xa1, 0xb8, 0x39, 0x59, 0x87, 0x1b, 0x4e, 0x2a, 0xd8, 0x42, 0x25, 0x2c, 0x95, + 0x16, 0xf8, 0xd5, 0x46, 0x58, 0x3c, 0x65, 0xdc, 0x7f, 0x53, 0xe1, 0x0c, 0xfa, 0x83, 0x7b, 0xcb, 0x8c, 0xfa, 0x7e, + 0xe9, 0x44, 0xa6, 0x02, 0x13, 0x37, 0xb3, 0xd4, 0x7e, 0xbf, 0xac, 0xd2, 0x7e, 0x5e, 0x2e, 0xf7, 0x39, 0x69, 0xbe, + 0xd6, 0x1d, 0x34, 0x9f, 0x0c, 0xf7, 0x2b, 0xe5, 0x87, 0x16, 0x46, 0x4d, 0xf9, 0xd5, 0x97, 0x34, 0xcc, 0x3d, 0x15, + 0xde, 0xea, 0xb6, 0x51, 0xe8, 0xa2, 0x3e, 0x07, 0x43, 0x48, 0x7f, 0x05, 0xd7, 0xd0, 0xe0, 0x41, 0x91, 0x2c, 0x16, + 0x6b, 0x17, 0xc4, 0xf5, 0x31, 0xa7, 0xda, 0xa1, 0x8c, 0x31, 0xe2, 0x69, 0xc9, 0x41, 0x92, 0xc1, 0xc1, 0xf8, 0x0d, + 0x0c, 0x88, 0x49, 0x49, 0x48, 0x87, 0xd0, 0x59, 0x99, 0x89, 0xa8, 0xdc, 0xc5, 0xdb, 0x8d, 0xcb, 0x9a, 0x42, 0x11, + 0x76, 0x82, 0x99, 0x4a, 0xa9, 0x20, 0x90, 0x26, 0xdf, 0x46, 0xab, 0x16, 0x0c, 0x05, 0xd1, 0x60, 0x28, 0x20, 0x0f, + 0xd3, 0x55, 0xc2, 0x8d, 0x8f, 0xe2, 0xe0, 0x79, 0x85, 0x1a, 0xf1, 0x52, 0x83, 0xaf, 0x61, 0xf3, 0xd7, 0x44, 0x49, + 0x11, 0x72, 0x11, 0x7b, 0x05, 0x9f, 0x08, 0xd9, 0x94, 0x87, 0x39, 0xd0, 0x0f, 0xed, 0xca, 0x4e, 0xb6, 0x97, 0x57, + 0x2e, 0x2d, 0x1a, 0x5b, 0x89, 0x9a, 0xb5, 0x38, 0x8a, 0xb7, 0xb3, 0x3e, 0x4c, 0x4d, 0x09, 0x04, 0xa4, 0xa9, 0x9c, + 0xa4, 0x9a, 0xf7, 0x28, 0xeb, 0x03, 0x48, 0xb0, 0xfb, 0x09, 0x2c, 0xf4, 0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, + 0x53, 0xd0, 0x9a, 0x53, 0xd2, 0x7c, 0x53, 0x84, 0x70, 0x5b, 0x59, 0xcf, 0x98, 0x1d, 0x60, 0xdb, 0xee, 0x76, 0x7e, + 0x94, 0x6d, 0xb7, 0xfa, 0x86, 0xe0, 0xc2, 0xe3, 0xff, 0xa4, 0xc4, 0x34, 0x90, 0x42, 0xea, 0xc6, 0x4f, 0xa8, 0xc3, + 0x3e, 0x91, 0x3a, 0x11, 0x03, 0x9a, 0xab, 0xb1, 0xe8, 0xdc, 0x6b, 0x8e, 0x92, 0xcb, 0xaa, 0xda, 0xd5, 0x12, 0x34, + 0x74, 0x23, 0x19, 0x13, 0xc5, 0x3c, 0x27, 0x00, 0x46, 0xb1, 0xf9, 0x73, 0xae, 0x93, 0xbc, 0x7f, 0x59, 0x99, 0xda, + 0xed, 0xfb, 0x7e, 0x94, 0x9f, 0xd1, 0x91, 0x8a, 0xca, 0xe6, 0x24, 0xe6, 0xdf, 0x95, 0x60, 0x1a, 0x13, 0x1f, 0xe9, + 0xb9, 0xfa, 0xa1, 0x00, 0x5f, 0xd9, 0x50, 0x6a, 0xb6, 0xd7, 0xbf, 0x75, 0xb6, 0x07, 0x72, 0x36, 0xc1, 0x02, 0x0b, + 0x74, 0x59, 0x83, 0x2f, 0x60, 0x19, 0xdc, 0x91, 0x7e, 0x0a, 0xbe, 0x9f, 0xd6, 0xc1, 0x67, 0xec, 0x7f, 0x01, 0x68, + 0x55, 0x60, 0x40, 0xf9, 0x70, 0xd1, 0xb0, 0x12, 0xe2, 0x12, 0x15, 0x66, 0x15, 0xe7, 0x8f, 0xeb, 0xbc, 0x6e, 0x5a, + 0x96, 0x18, 0x94, 0x9f, 0xba, 0x86, 0x1b, 0xdf, 0x59, 0xc8, 0x1f, 0xdf, 0x7f, 0x09, 0xba, 0x9d, 0x48, 0xbb, 0xb5, + 0x55, 0x6c, 0x90, 0x85, 0x86, 0xf7, 0xc2, 0xa6, 0xd0, 0x16, 0x2f, 0x02, 0x14, 0xea, 0x3b, 0x16, 0xe3, 0x6d, 0x11, + 0x2a, 0xc3, 0x2f, 0x58, 0x30, 0x05, 0x0c, 0xc1, 0x63, 0xa7, 0x32, 0xf9, 0x1d, 0x36, 0x9a, 0x62, 0xd7, 0x42, 0x18, + 0x7c, 0x39, 0xa8, 0x4a, 0xc9, 0x8b, 0x75, 0xb2, 0xbd, 0x38, 0x87, 0xef, 0xaf, 0xe3, 0x02, 0xa8, 0x83, 0xe8, 0x6b, + 0x2a, 0x8b, 0x0d, 0xe4, 0xe2, 0xa6, 0xac, 0xf5, 0x8a, 0x86, 0xc3, 0x1b, 0xbb, 0xf0, 0xba, 0x02, 0x1f, 0x47, 0xe9, + 0x30, 0x11, 0x93, 0x98, 0x49, 0x95, 0x2b, 0x72, 0x6d, 0x74, 0x2f, 0x6d, 0xd1, 0xbc, 0x14, 0x12, 0xbc, 0x22, 0xf0, + 0x82, 0xd0, 0x57, 0xfa, 0x72, 0xb5, 0x81, 0x82, 0x47, 0xed, 0x8b, 0x8b, 0x60, 0x62, 0xe2, 0x71, 0x43, 0x6a, 0xfa, + 0x75, 0x38, 0xb5, 0xb2, 0x58, 0x72, 0xf8, 0x75, 0xce, 0xd8, 0x82, 0x02, 0x20, 0x3e, 0x79, 0xb4, 0xde, 0x4d, 0x7a, + 0xa3, 0xb4, 0x83, 0xd2, 0x08, 0xf1, 0x5d, 0x85, 0xaf, 0x3b, 0x57, 0x7c, 0xe5, 0xaa, 0x7b, 0x5f, 0x57, 0xdc, 0xb8, + 0x60, 0xf4, 0x92, 0x4f, 0x92, 0x85, 0x6b, 0x37, 0x74, 0x57, 0xe7, 0x3b, 0xef, 0x0b, 0x99, 0xb7, 0x70, 0x05, 0x76, + 0xfe, 0x15, 0x77, 0x5e, 0x7a, 0x1f, 0x8c, 0x13, 0xe5, 0xef, 0xcd, 0x23, 0x5e, 0x39, 0xcc, 0xaa, 0x93, 0xe4, 0xef, + 0x7b, 0xdf, 0x07, 0xeb, 0x5b, 0x1a, 0x27, 0xc8, 0x6d, 0x75, 0x82, 0x4c, 0x94, 0x1b, 0xe9, 0x0d, 0xb7, 0x7f, 0x57, + 0x81, 0x20, 0x4e, 0xc5, 0xf4, 0x51, 0x39, 0xae, 0x1f, 0x2d, 0x50, 0xa9, 0x88, 0xf8, 0x5c, 0xe5, 0xae, 0xac, 0x4d, + 0x0d, 0xf5, 0x98, 0x4e, 0x66, 0xa1, 0x69, 0x56, 0xe4, 0x52, 0x2e, 0x7a, 0x8c, 0x5c, 0xb3, 0x53, 0x6d, 0x7e, 0x77, + 0xed, 0x21, 0x1d, 0xc7, 0xfb, 0x9e, 0xb5, 0x5a, 0x70, 0xbf, 0xab, 0x28, 0xbc, 0xeb, 0xc5, 0x46, 0x2a, 0x43, 0xcd, + 0x7a, 0x14, 0x7d, 0x1c, 0x77, 0x31, 0x97, 0x47, 0xd9, 0x9f, 0x35, 0x00, 0x4c, 0x47, 0x58, 0x74, 0x37, 0x3d, 0x63, + 0x4f, 0xa0, 0xa7, 0x27, 0x32, 0x48, 0xf4, 0x56, 0xe7, 0xab, 0x56, 0x89, 0xa5, 0x2b, 0x08, 0xec, 0xde, 0x90, 0xb1, + 0x2a, 0x69, 0xb7, 0x5c, 0xbf, 0x9c, 0xe7, 0xf3, 0x9c, 0x2f, 0xe5, 0xf9, 0xd4, 0x2c, 0xba, 0x8d, 0xa6, 0x7b, 0x73, + 0x6a, 0xa8, 0x98, 0x6b, 0x75, 0x93, 0xdf, 0x30, 0x5d, 0x0b, 0x43, 0x2d, 0x82, 0xcc, 0x6a, 0x57, 0xbd, 0x28, 0xcb, + 0x51, 0x3d, 0x93, 0x63, 0x24, 0x7c, 0x53, 0xe9, 0x0e, 0xd1, 0x0d, 0x53, 0x35, 0xd3, 0x77, 0x0b, 0xdb, 0x42, 0xb6, + 0x79, 0x79, 0x35, 0xcc, 0x81, 0xd2, 0x72, 0x7f, 0x99, 0x30, 0x7c, 0x77, 0x7d, 0xfd, 0x9d, 0x90, 0x53, 0x55, 0x47, + 0x6f, 0xfe, 0x5a, 0xf7, 0x0c, 0x46, 0xa5, 0x72, 0x22, 0x4e, 0xf9, 0xea, 0xc1, 0x17, 0x77, 0xaf, 0x80, 0xe5, 0x14, + 0xb0, 0x3b, 0xe5, 0xce, 0xc2, 0x50, 0xd5, 0x06, 0xfe, 0x62, 0xf5, 0x60, 0xab, 0xf6, 0xf0, 0x17, 0xbd, 0x2f, 0x82, + 0x1b, 0x1b, 0x1b, 0xdb, 0x78, 0xb7, 0x96, 0x08, 0xf2, 0x16, 0x0f, 0xf4, 0xf1, 0xea, 0xa3, 0xa0, 0xe5, 0x0a, 0xb1, + 0xcd, 0x7a, 0x0e, 0x85, 0xad, 0x41, 0xbe, 0x49, 0x99, 0x34, 0x98, 0x15, 0x3c, 0x9b, 0xc8, 0x19, 0x0a, 0x79, 0xcd, + 0xc7, 0x41, 0xdb, 0x11, 0xfe, 0x0f, 0x9c, 0xda, 0xf1, 0xf2, 0xfc, 0x13, 0xf4, 0x01, 0x4f, 0x57, 0x4a, 0x53, 0x8a, + 0x53, 0xaa, 0xa0, 0xce, 0x72, 0x9d, 0x07, 0x23, 0xc5, 0xc5, 0x18, 0x16, 0x17, 0x5c, 0x96, 0x1b, 0x67, 0x23, 0xa7, + 0xbf, 0xc4, 0xab, 0x8b, 0x74, 0xf9, 0x48, 0x64, 0xab, 0x96, 0xde, 0x2b, 0x7d, 0xba, 0x6d, 0x4f, 0x18, 0x1f, 0x67, + 0x43, 0x3a, 0x98, 0xf1, 0x71, 0x22, 0xbc, 0x3e, 0x31, 0xd4, 0x77, 0x8b, 0xc0, 0x74, 0x73, 0x6c, 0xf2, 0xc3, 0xf1, + 0x7a, 0xb3, 0x59, 0xe3, 0xf6, 0xde, 0x39, 0x9f, 0x9c, 0x79, 0x89, 0x11, 0x95, 0xb9, 0x86, 0x07, 0xb4, 0x42, 0xbc, + 0x78, 0xcf, 0x04, 0xc6, 0x65, 0x57, 0x24, 0xb5, 0xdd, 0x40, 0xe0, 0x62, 0x8f, 0x62, 0x96, 0x0c, 0x6d, 0x0f, 0xca, + 0x03, 0x7d, 0x31, 0x9a, 0x6e, 0x01, 0xd3, 0xf2, 0xda, 0xd9, 0x45, 0x6a, 0x7b, 0xd5, 0x54, 0x01, 0xcc, 0x92, 0xe5, + 0xf1, 0x19, 0xb2, 0xee, 0x57, 0xd0, 0x45, 0x0c, 0x18, 0x1b, 0x57, 0xe6, 0xdc, 0xf9, 0xaa, 0x15, 0xf1, 0x8d, 0x26, + 0xd2, 0xa4, 0x3e, 0xa2, 0xbe, 0xfd, 0xb0, 0x56, 0x57, 0x39, 0x48, 0xe0, 0x1e, 0x79, 0x77, 0xc4, 0xa5, 0xa3, 0xcf, + 0x2c, 0x36, 0xab, 0xf4, 0x2d, 0x75, 0x2d, 0x6e, 0x31, 0xec, 0x15, 0xf7, 0xc0, 0xfe, 0xc0, 0xb8, 0x45, 0x2c, 0xe2, + 0xed, 0xac, 0x96, 0xc2, 0xba, 0x30, 0x47, 0x8e, 0xb1, 0xf6, 0xe0, 0x15, 0xaf, 0xd6, 0x0c, 0xcc, 0x30, 0xe3, 0x8c, + 0xe4, 0x8d, 0x71, 0xaf, 0x6a, 0xd3, 0x91, 0xab, 0x00, 0xa2, 0x6f, 0x4e, 0x97, 0xe4, 0xf0, 0x4a, 0x96, 0xab, 0xce, + 0x90, 0x7f, 0x86, 0x75, 0xd6, 0x8b, 0x13, 0x70, 0x93, 0xa6, 0xac, 0xc4, 0xc4, 0x14, 0x71, 0xb9, 0x59, 0xc6, 0x3c, + 0x4d, 0x9f, 0x45, 0x3b, 0x38, 0x85, 0x91, 0xc0, 0x11, 0xfb, 0xc6, 0x32, 0x2c, 0x26, 0x6c, 0xc4, 0x44, 0x1a, 0x95, + 0x52, 0xc2, 0x7a, 0x72, 0xa9, 0x25, 0x7f, 0x99, 0xcb, 0xab, 0x2f, 0xb7, 0x09, 0x0e, 0x28, 0x6a, 0x60, 0x39, 0x34, + 0x8e, 0x5b, 0x06, 0x12, 0xb1, 0x18, 0x10, 0xa3, 0x56, 0xe5, 0x72, 0x32, 0xaa, 0x93, 0xfa, 0x0a, 0xb9, 0x50, 0x91, + 0x07, 0xb7, 0x04, 0x4a, 0xfe, 0x02, 0x53, 0x07, 0xd3, 0x52, 0xbb, 0x69, 0xb1, 0x49, 0xf2, 0x8e, 0x19, 0x90, 0x5c, + 0x7d, 0x0d, 0x0f, 0x8d, 0x5f, 0x86, 0x37, 0x14, 0x3d, 0x1d, 0x23, 0xe4, 0xb4, 0x34, 0xe6, 0xd2, 0x7f, 0x23, 0xcf, + 0xbe, 0x24, 0x60, 0x3f, 0x83, 0x98, 0x32, 0x70, 0x89, 0x8d, 0x0b, 0x92, 0xf2, 0x5a, 0x9e, 0xb2, 0xfb, 0x16, 0x94, + 0xef, 0x92, 0x49, 0x57, 0xa9, 0xac, 0x35, 0x56, 0xdd, 0xcf, 0x33, 0x96, 0x5f, 0x1d, 0x30, 0xcc, 0x4d, 0x46, 0x83, + 0x6c, 0xc9, 0xcc, 0xa6, 0xfc, 0x6a, 0xef, 0xc6, 0xb7, 0x3c, 0x94, 0x74, 0xa8, 0x56, 0xe9, 0xe6, 0xa5, 0x1b, 0x8e, + 0xf1, 0xc2, 0x0d, 0xc7, 0xb8, 0x43, 0xe7, 0xca, 0x15, 0xa9, 0x75, 0xfe, 0xfb, 0x52, 0xf8, 0x49, 0xec, 0xb5, 0xbe, + 0xde, 0x75, 0xfd, 0x95, 0xe9, 0xe9, 0x37, 0xa0, 0x6a, 0x64, 0x09, 0xdd, 0x84, 0x2a, 0x26, 0x23, 0x51, 0x62, 0xba, + 0x4a, 0x79, 0xd4, 0xd7, 0x88, 0x0b, 0x10, 0x37, 0x94, 0xbf, 0xf8, 0x97, 0xf0, 0xe2, 0x24, 0x40, 0x23, 0x6a, 0x3e, + 0xca, 0x52, 0xde, 0x18, 0x45, 0x93, 0x38, 0xb9, 0x0a, 0x66, 0x71, 0x63, 0x92, 0xa5, 0x59, 0x31, 0x05, 0xae, 0xf4, + 0x8a, 0x2b, 0xb0, 0xe1, 0x27, 0x8d, 0x59, 0xec, 0xbd, 0x64, 0xc9, 0x39, 0xe3, 0xf1, 0x20, 0xf2, 0xec, 0xfd, 0x1c, + 0xc4, 0x83, 0xf5, 0x36, 0xca, 0xf3, 0xec, 0xc2, 0xf6, 0x3e, 0x64, 0xa7, 0xc0, 0xb4, 0xde, 0xbb, 0xcb, 0xab, 0x33, + 0x96, 0x7a, 0x1f, 0x4f, 0x67, 0x29, 0x9f, 0x79, 0x45, 0x94, 0x16, 0x8d, 0x82, 0xe5, 0xf1, 0x08, 0xd4, 0x44, 0x92, + 0xe5, 0x0d, 0xcc, 0x7f, 0x9e, 0xb0, 0x20, 0x89, 0xcf, 0xc6, 0xdc, 0x1a, 0x46, 0xf9, 0xa7, 0x4e, 0xa3, 0x31, 0xcd, + 0xe3, 0x49, 0x94, 0x5f, 0x35, 0xa8, 0x45, 0x70, 0xaf, 0xb9, 0x1b, 0x7d, 0x36, 0x7a, 0xd0, 0xe1, 0x39, 0xf4, 0x8d, + 0x91, 0x8a, 0x01, 0x08, 0x1f, 0x6b, 0xf7, 0x61, 0x73, 0x52, 0x6c, 0x88, 0x13, 0xa5, 0x28, 0xe5, 0xe5, 0x89, 0x77, + 0x01, 0xb6, 0xed, 0x89, 0x7f, 0xca, 0x53, 0x0f, 0x7c, 0x39, 0x9e, 0xa5, 0xf3, 0xc1, 0x2c, 0x2f, 0x60, 0x80, 0x69, + 0x16, 0xa7, 0x9c, 0xe5, 0x9d, 0xd3, 0x2c, 0x07, 0xb2, 0x35, 0xf2, 0x68, 0x18, 0xcf, 0x8a, 0xe0, 0xc1, 0xf4, 0xb2, + 0x83, 0xb6, 0xc2, 0x59, 0x9e, 0xcd, 0xd2, 0xa1, 0x9c, 0x2b, 0x4e, 0x61, 0x63, 0xc4, 0xdc, 0xac, 0xa0, 0x37, 0xa1, + 0x00, 0x7c, 0x29, 0x8b, 0xf2, 0xc6, 0x19, 0x76, 0x46, 0x43, 0xbf, 0x39, 0x64, 0x67, 0x5e, 0x7e, 0x76, 0x1a, 0x39, + 0xad, 0xf6, 0x63, 0x4f, 0xfd, 0xf9, 0x0f, 0x5d, 0x30, 0xdc, 0x57, 0x16, 0xb7, 0x9a, 0xcd, 0xbf, 0x71, 0x3b, 0x0b, + 0xb3, 0x10, 0x40, 0x41, 0x6b, 0x7a, 0x69, 0x15, 0x59, 0x02, 0xeb, 0xb3, 0xaa, 0x67, 0x67, 0x0a, 0x7e, 0x53, 0x9c, + 0x9e, 0x05, 0xed, 0xe9, 0x65, 0x89, 0xd8, 0x05, 0x22, 0x21, 0x53, 0x22, 0x29, 0x9f, 0xe6, 0xbf, 0x15, 0xe2, 0x27, + 0xab, 0x21, 0x6e, 0x2b, 0x88, 0x2b, 0xaa, 0x37, 0x86, 0xb0, 0x0f, 0x88, 0xfc, 0xad, 0x42, 0x00, 0x32, 0x06, 0x27, + 0x30, 0x57, 0x70, 0xd0, 0xc3, 0x6f, 0x06, 0xa3, 0xbd, 0x1a, 0x8c, 0x27, 0xb7, 0x81, 0x91, 0xa7, 0xc3, 0x79, 0x7d, + 0x5d, 0x5b, 0xe0, 0x9c, 0x76, 0xc6, 0x0c, 0xf9, 0x29, 0x68, 0xe3, 0xf7, 0x8b, 0x78, 0xc8, 0xc7, 0xe2, 0x2b, 0xb1, + 0xf3, 0x85, 0xa8, 0x7b, 0xd8, 0x6c, 0x8a, 0xe7, 0x02, 0x14, 0x5a, 0xd0, 0xf2, 0xb1, 0x01, 0x30, 0xd1, 0xe7, 0xeb, + 0x5e, 0x62, 0xf3, 0xed, 0xad, 0x6f, 0xaa, 0xf1, 0xb8, 0xca, 0x1b, 0x14, 0x2a, 0x42, 0xbd, 0xb3, 0x05, 0x33, 0xde, + 0x8a, 0x6e, 0x4b, 0x1f, 0x54, 0xf5, 0xbe, 0xe5, 0xa4, 0xf5, 0x02, 0xe6, 0x99, 0xb9, 0x40, 0x9d, 0xac, 0x8b, 0x21, + 0xa9, 0x46, 0xc3, 0x05, 0xbd, 0xc1, 0x31, 0x84, 0x44, 0x07, 0x82, 0x4e, 0xd1, 0xcb, 0xe9, 0x9d, 0x1a, 0xa9, 0x1b, + 0xe4, 0x4e, 0xea, 0xc2, 0x96, 0x4f, 0xb5, 0x5c, 0x2f, 0xb6, 0xb6, 0xc0, 0xcb, 0xfe, 0x9c, 0xcb, 0x06, 0x20, 0xbd, + 0x2b, 0x49, 0x6b, 0xbc, 0x87, 0x44, 0xb9, 0x7c, 0xd9, 0x80, 0x28, 0x07, 0xbe, 0x3e, 0x1f, 0xa3, 0xdf, 0xad, 0xaf, + 0xae, 0x1b, 0x29, 0x35, 0x3b, 0xb6, 0xdb, 0xe3, 0x3a, 0x2b, 0x0b, 0xb3, 0xcf, 0x78, 0x89, 0xa3, 0x7c, 0xc9, 0x43, + 0x1c, 0xd1, 0x7b, 0x15, 0x0a, 0x37, 0x4d, 0x39, 0x69, 0xa3, 0xbb, 0x3a, 0x69, 0xf0, 0x35, 0xa6, 0xcc, 0x67, 0x15, + 0x27, 0x07, 0x37, 0xe6, 0x78, 0x20, 0xae, 0x20, 0x16, 0x55, 0x96, 0x7d, 0x44, 0xd0, 0x0b, 0xbf, 0x0b, 0x94, 0x14, + 0x46, 0x2e, 0xbf, 0xe2, 0xbf, 0xc3, 0xe3, 0x70, 0x34, 0xfa, 0x45, 0x36, 0xcb, 0x07, 0x78, 0x39, 0x60, 0x45, 0x28, + 0xc2, 0x26, 0x4b, 0xc0, 0xf6, 0xb8, 0x56, 0x40, 0x0c, 0xf3, 0x2c, 0xcc, 0xb7, 0x2f, 0x30, 0x3a, 0x9d, 0x11, 0x97, + 0x1f, 0x64, 0xf8, 0x45, 0xa1, 0x84, 0x3a, 0x75, 0x48, 0x89, 0x78, 0x74, 0x31, 0xd4, 0x9f, 0xa5, 0x31, 0x88, 0xe0, + 0xe3, 0x78, 0x48, 0x17, 0x62, 0xe2, 0x21, 0x9d, 0x90, 0x34, 0x28, 0x23, 0x0a, 0x43, 0xee, 0x50, 0x20, 0x17, 0x06, + 0xbf, 0xcb, 0x0c, 0x1b, 0xbb, 0x61, 0xe3, 0x29, 0x87, 0xa1, 0xc3, 0x87, 0xd9, 0x24, 0x8a, 0xd3, 0x00, 0x5f, 0x5c, + 0xe2, 0xe9, 0x11, 0x03, 0xec, 0xe2, 0xc1, 0xa7, 0x5a, 0xa3, 0x96, 0xeb, 0xff, 0x04, 0x02, 0x8e, 0xfa, 0x63, 0x32, + 0x0b, 0x91, 0x55, 0x10, 0x31, 0x54, 0x64, 0xde, 0x57, 0x7a, 0xde, 0x33, 0xab, 0x55, 0xcc, 0xb4, 0xbe, 0x0e, 0xcd, + 0x85, 0xe5, 0xd2, 0x67, 0xd8, 0xf5, 0x52, 0x10, 0xac, 0x5c, 0xe7, 0xd1, 0x53, 0x10, 0x67, 0x8f, 0xd1, 0x47, 0xaf, + 0xd1, 0x0a, 0x5a, 0xda, 0x2f, 0xaf, 0x5d, 0xb5, 0x15, 0x89, 0x3a, 0xf2, 0xba, 0x26, 0xe1, 0xa1, 0xbf, 0x0b, 0x5c, + 0xab, 0x67, 0x8d, 0xaf, 0x27, 0x37, 0x1d, 0x46, 0xa7, 0xce, 0x52, 0xa7, 0x06, 0x04, 0x1d, 0x74, 0xac, 0x99, 0xca, + 0x2d, 0x2b, 0xbc, 0xb5, 0xf1, 0x67, 0x0b, 0xcd, 0x89, 0xaf, 0x1e, 0x90, 0x33, 0xd2, 0x2b, 0x9e, 0x56, 0xf0, 0x5d, + 0x29, 0x09, 0xb2, 0x78, 0x21, 0x7f, 0xa1, 0x99, 0x00, 0xe5, 0x4a, 0x1f, 0x64, 0x2f, 0xd4, 0x8a, 0x47, 0x26, 0x22, + 0xde, 0xab, 0x9b, 0x50, 0xd6, 0xd8, 0x32, 0x5c, 0xe8, 0x8b, 0x16, 0x5c, 0xc1, 0x8f, 0x06, 0xd3, 0x88, 0xe1, 0xbd, + 0x94, 0x93, 0xcd, 0xf9, 0x97, 0xbc, 0xdc, 0xd9, 0x9c, 0xab, 0x86, 0xe2, 0x7b, 0x3c, 0xc4, 0x4f, 0x06, 0xf2, 0x6b, + 0x2e, 0xac, 0xc7, 0xc0, 0x7e, 0xff, 0xee, 0xe0, 0xd0, 0xf6, 0x4e, 0xb3, 0xe1, 0x55, 0x60, 0xc3, 0xee, 0x64, 0x76, + 0xe9, 0xfa, 0x7c, 0xcc, 0x52, 0x47, 0xb1, 0x78, 0x96, 0x30, 0x90, 0x08, 0x67, 0xe2, 0xb2, 0xe3, 0xa2, 0xe7, 0x3b, + 0x3c, 0xd9, 0xa3, 0xb7, 0x21, 0x75, 0xf7, 0xb8, 0x78, 0x51, 0x18, 0xcf, 0xf1, 0x6b, 0x17, 0x63, 0xff, 0x7b, 0x3b, + 0xf0, 0x05, 0x1f, 0x0e, 0x70, 0xcf, 0xd0, 0xd3, 0xe6, 0x7c, 0x89, 0x93, 0x7a, 0x38, 0xc4, 0xb8, 0x2b, 0x50, 0x28, + 0xa8, 0xd5, 0x49, 0x30, 0x3c, 0x39, 0x29, 0xe1, 0x2b, 0x8c, 0xb5, 0xa3, 0xc6, 0x45, 0x08, 0x55, 0x7f, 0xcd, 0x5d, + 0xf2, 0x75, 0x3b, 0x38, 0x04, 0xce, 0x3b, 0xc4, 0x06, 0xc4, 0x5d, 0xd8, 0x7b, 0xa8, 0x4b, 0x68, 0xd3, 0x8a, 0xa2, + 0x75, 0x10, 0x88, 0x86, 0x15, 0xd3, 0x8b, 0x10, 0x61, 0xb5, 0xba, 0x0a, 0xa4, 0xa1, 0x09, 0xdd, 0x89, 0x8b, 0x9f, + 0x04, 0x19, 0x7c, 0x12, 0x1d, 0x4e, 0xcc, 0x37, 0x84, 0x88, 0xcb, 0xfc, 0x9a, 0x5a, 0x47, 0x7f, 0x01, 0xdb, 0xc3, + 0xbb, 0x38, 0xa1, 0x96, 0x4a, 0x1d, 0xa1, 0x9d, 0x84, 0x6a, 0xbb, 0xa9, 0xec, 0x0e, 0xd0, 0xfd, 0x49, 0x34, 0x2d, + 0x58, 0xa0, 0xbe, 0x48, 0xcd, 0x84, 0x0a, 0x6e, 0xd9, 0x14, 0x90, 0x79, 0x31, 0xcf, 0xd0, 0x60, 0x58, 0xb6, 0x53, + 0x40, 0xf4, 0x39, 0x8d, 0xc6, 0xa0, 0x71, 0x7a, 0xe6, 0x96, 0x7c, 0x3c, 0x37, 0xf5, 0xda, 0x23, 0xd0, 0x6b, 0x98, + 0x93, 0xd7, 0x00, 0x4f, 0xed, 0x2c, 0x0d, 0x12, 0x36, 0xe2, 0x25, 0xc7, 0x4b, 0x5f, 0x73, 0x65, 0x48, 0xf8, 0xed, + 0x87, 0xa0, 0xeb, 0x2c, 0x1f, 0xff, 0xbd, 0x79, 0x62, 0xe8, 0x18, 0xa4, 0xa0, 0x9b, 0x28, 0x0b, 0x14, 0x33, 0xec, + 0x01, 0x5c, 0xf3, 0x79, 0x6e, 0x4c, 0x34, 0x60, 0x68, 0x64, 0x95, 0x1c, 0x64, 0xf2, 0xd8, 0xe3, 0xb9, 0xd9, 0x2e, + 0x75, 0xe7, 0x4b, 0x18, 0x2c, 0xeb, 0xfa, 0x5d, 0xb7, 0x2c, 0xc8, 0x64, 0x5d, 0x6e, 0xac, 0x0c, 0xa6, 0xfa, 0xd3, + 0x12, 0xf9, 0x0c, 0xd3, 0xae, 0x14, 0xc1, 0xd2, 0xb9, 0xe8, 0x71, 0x17, 0x62, 0xd6, 0x8c, 0x4e, 0xcf, 0xec, 0xe1, + 0x96, 0x71, 0x3a, 0x9d, 0xf1, 0x23, 0x0a, 0xd4, 0xe6, 0x78, 0x9d, 0xa0, 0x3f, 0x17, 0x73, 0x83, 0x17, 0x3c, 0x70, + 0x10, 0x00, 0xab, 0x61, 0x3d, 0x01, 0x6a, 0xba, 0xca, 0xf0, 0xf0, 0x1f, 0x23, 0x71, 0x4b, 0x9f, 0x5a, 0xaf, 0xa0, + 0xd2, 0x09, 0x58, 0xdd, 0x1d, 0xce, 0x9d, 0xa3, 0x37, 0x8e, 0xdb, 0xf7, 0x5e, 0x19, 0x2f, 0x2f, 0xb1, 0xd5, 0x1e, + 0xb0, 0x3d, 0xa4, 0xf7, 0xca, 0x26, 0x26, 0x93, 0x53, 0xb3, 0x57, 0x21, 0x36, 0x7c, 0xeb, 0xd8, 0xac, 0x98, 0x36, + 0x84, 0x48, 0x6a, 0x10, 0x33, 0xda, 0xd8, 0x55, 0x05, 0x56, 0xbf, 0xe2, 0x73, 0x92, 0x36, 0x5c, 0xbf, 0x29, 0xe4, + 0x88, 0xf7, 0xcd, 0x5b, 0x2d, 0xb5, 0x80, 0x3a, 0xd4, 0xb9, 0x86, 0xe4, 0x83, 0x47, 0xf9, 0xd6, 0x1b, 0x25, 0x37, + 0x4e, 0xf6, 0xeb, 0x92, 0x0c, 0xf6, 0x59, 0xa9, 0xdf, 0xa8, 0x06, 0x5a, 0x18, 0xe7, 0x87, 0x8d, 0x24, 0xf7, 0xdd, + 0x53, 0xb2, 0x12, 0x55, 0x1c, 0x9c, 0xae, 0x2c, 0xaa, 0x13, 0x0d, 0xa1, 0x50, 0x63, 0x3c, 0x77, 0xad, 0x25, 0xdd, + 0x76, 0x2a, 0x59, 0x24, 0x6c, 0x4c, 0x8b, 0xf0, 0x08, 0x6d, 0x30, 0xfa, 0x6c, 0xeb, 0xcf, 0x03, 0x50, 0x7f, 0x9f, + 0x42, 0x7b, 0x73, 0xee, 0xb8, 0xab, 0xef, 0xcd, 0x29, 0xcf, 0x50, 0x49, 0x61, 0x23, 0x63, 0xb1, 0x26, 0x5c, 0xd1, + 0x41, 0xb5, 0xbb, 0x28, 0x3e, 0xf7, 0x76, 0xc4, 0x44, 0xb0, 0xdb, 0x8f, 0xe5, 0x8b, 0x9e, 0xb8, 0x29, 0x12, 0x91, + 0xbc, 0xa2, 0xdc, 0x22, 0x36, 0x09, 0xed, 0x5b, 0x79, 0xc7, 0xb6, 0x84, 0x94, 0x42, 0x40, 0x95, 0xc0, 0x02, 0xe0, + 0x75, 0x19, 0x93, 0xb0, 0xc7, 0x92, 0x0c, 0x36, 0xce, 0x05, 0x8a, 0x00, 0x03, 0x47, 0x3c, 0x8a, 0x13, 0xd1, 0x45, + 0x06, 0xf6, 0x94, 0x03, 0xa8, 0x31, 0xc2, 0x23, 0xf5, 0x3a, 0x2e, 0x75, 0x12, 0x12, 0x66, 0x7b, 0x3b, 0x15, 0xdc, + 0x84, 0x19, 0xed, 0x32, 0xf3, 0x00, 0xab, 0xc2, 0x50, 0xd4, 0x01, 0x71, 0xe9, 0xda, 0x0c, 0x02, 0x58, 0xa8, 0x60, + 0x87, 0x97, 0xaa, 0x2b, 0x2c, 0x02, 0x96, 0x1c, 0x13, 0x85, 0xc1, 0xc8, 0x63, 0x5c, 0x13, 0x36, 0x17, 0xd9, 0x8f, + 0x0a, 0xda, 0x74, 0x09, 0xda, 0xb4, 0x0e, 0xed, 0x09, 0x12, 0xbd, 0xb7, 0x39, 0x8f, 0xcb, 0x10, 0xbe, 0xa5, 0x83, + 0x6c, 0xc8, 0x3e, 0x7e, 0x78, 0x85, 0x77, 0x00, 0xa1, 0x3d, 0x38, 0x0b, 0x99, 0x5b, 0x9e, 0xc8, 0xc5, 0x31, 0x75, + 0x82, 0xd8, 0xdb, 0x16, 0xcd, 0x45, 0x74, 0x05, 0x8a, 0xf6, 0x04, 0xe4, 0x6c, 0x48, 0x05, 0x61, 0x98, 0x53, 0x2f, + 0x0e, 0x4b, 0x2a, 0x5a, 0x0b, 0x99, 0x2e, 0x1a, 0x21, 0x11, 0x68, 0x67, 0x56, 0x34, 0xc0, 0x9c, 0x59, 0x93, 0x0e, + 0xc3, 0xf8, 0x5c, 0x73, 0x1b, 0x5d, 0x20, 0xea, 0xee, 0x01, 0x43, 0xb3, 0x04, 0xc6, 0xcc, 0xaf, 0xaf, 0x9b, 0x30, + 0x94, 0x78, 0xb4, 0xf6, 0x48, 0x36, 0x88, 0x77, 0x61, 0xc2, 0xcc, 0x2d, 0x4c, 0x4f, 0xc2, 0xab, 0x7a, 0x3d, 0x95, + 0x6f, 0x13, 0xc8, 0x01, 0x00, 0x46, 0x3a, 0xea, 0x27, 0x3e, 0xd0, 0xe6, 0x0d, 0x94, 0xc6, 0xc3, 0xe5, 0x32, 0xb0, + 0x4a, 0xa7, 0x58, 0x9a, 0x5d, 0x5f, 0xb7, 0xe0, 0x71, 0x12, 0xa7, 0xf8, 0x04, 0x33, 0xd3, 0x0d, 0x38, 0x78, 0x04, + 0xd3, 0x1c, 0xd8, 0x16, 0x6a, 0xa2, 0x4b, 0xac, 0x49, 0x55, 0x4d, 0x74, 0x09, 0xf2, 0x48, 0x54, 0x69, 0xf2, 0x14, + 0xc8, 0x70, 0xff, 0x1f, 0x16, 0x34, 0x93, 0x8b, 0x67, 0x69, 0xd2, 0x01, 0x98, 0x20, 0x2d, 0x35, 0xf1, 0xf6, 0x76, + 0x80, 0xcc, 0xb0, 0x18, 0xd2, 0xfa, 0x91, 0x3b, 0xae, 0x7a, 0x8f, 0x91, 0x90, 0x64, 0x6e, 0x2d, 0x0d, 0x81, 0x8a, + 0xd0, 0x1a, 0x04, 0xdf, 0x62, 0x78, 0x4c, 0x9b, 0x03, 0xf4, 0xbc, 0xd4, 0xfe, 0x0b, 0xb2, 0xa6, 0xea, 0xe0, 0xd9, + 0x7f, 0xfd, 0xc7, 0xbf, 0xb3, 0x3d, 0xb1, 0xb9, 0xb2, 0xd1, 0x08, 0x4c, 0x65, 0xeb, 0x0e, 0x7d, 0xfe, 0xd7, 0xdf, + 0xff, 0xdf, 0xff, 0xf3, 0x5f, 0x75, 0xb7, 0x14, 0x7a, 0x9d, 0xc8, 0x83, 0x3f, 0x25, 0x1d, 0x0c, 0x30, 0x15, 0x1a, + 0xa3, 0x28, 0x5d, 0x87, 0xc3, 0x91, 0x89, 0x43, 0x31, 0x65, 0x6c, 0xe8, 0xd9, 0x96, 0xed, 0x2d, 0x95, 0x1e, 0x27, + 0xec, 0x9c, 0xc9, 0xb7, 0x9e, 0xad, 0x9a, 0x6a, 0x45, 0x8f, 0x01, 0x28, 0x34, 0x2e, 0xcf, 0x3f, 0x25, 0x6f, 0x9b, + 0xa8, 0x48, 0xa9, 0x52, 0xeb, 0x87, 0xb4, 0xab, 0x8b, 0x0b, 0xcf, 0x36, 0xa6, 0x5f, 0x0b, 0x57, 0x6f, 0x4d, 0x79, + 0xd0, 0xf4, 0x9a, 0xeb, 0x20, 0xf3, 0xc0, 0x8f, 0xb4, 0xed, 0xbe, 0xa2, 0x11, 0x85, 0x7b, 0xcc, 0x0b, 0xec, 0xeb, + 0x68, 0x75, 0x2b, 0xf6, 0xa7, 0x39, 0x0e, 0x95, 0xb2, 0xa2, 0xb8, 0x05, 0x79, 0x58, 0x3e, 0xcf, 0xae, 0x5a, 0xdb, + 0x6b, 0x46, 0x01, 0x14, 0xda, 0x0f, 0x1f, 0x0a, 0x70, 0x3d, 0x47, 0xbb, 0x90, 0x66, 0x63, 0x36, 0x1a, 0x81, 0x10, + 0x29, 0xdc, 0x2a, 0x1f, 0x74, 0x14, 0x27, 0x1c, 0xcf, 0xb3, 0xc3, 0xae, 0xfd, 0x16, 0x36, 0x06, 0x5e, 0x0f, 0x75, + 0xa5, 0x5f, 0xaf, 0x32, 0xfd, 0x94, 0xd0, 0x5d, 0x0d, 0x97, 0x18, 0xb2, 0x0e, 0x93, 0x9c, 0xe6, 0xfa, 0x5a, 0xf9, + 0xcb, 0xb5, 0xf2, 0x3a, 0x39, 0x33, 0x72, 0x88, 0x57, 0xef, 0x9b, 0xbb, 0xec, 0x8e, 0x7f, 0xfd, 0xa7, 0xbf, 0xff, + 0x6f, 0x00, 0x06, 0x8e, 0x73, 0xb7, 0xad, 0x01, 0x1d, 0xfe, 0x27, 0x74, 0x98, 0xa5, 0x77, 0xef, 0xf2, 0xd7, 0xff, + 0xf2, 0xdf, 0xa1, 0x07, 0x5d, 0x60, 0x86, 0x7d, 0xa4, 0x40, 0x1f, 0x60, 0xd8, 0xe8, 0x77, 0xc1, 0x5e, 0x1b, 0xf7, + 0x2e, 0x70, 0xfc, 0x03, 0xa2, 0x5a, 0xf0, 0x6c, 0x7a, 0x57, 0xb8, 0x11, 0xd3, 0x41, 0x92, 0x15, 0xcc, 0x04, 0x5c, + 0x58, 0x0a, 0xbf, 0x0f, 0x72, 0x82, 0x64, 0x0a, 0x12, 0xb4, 0xb0, 0xcc, 0xa1, 0x25, 0xaf, 0xdc, 0x28, 0x08, 0x57, + 0x32, 0x54, 0xc1, 0x38, 0x91, 0x82, 0xac, 0xb9, 0x1a, 0xd3, 0x88, 0xb2, 0x25, 0x5e, 0x22, 0xe9, 0xae, 0x25, 0x97, + 0xd0, 0x58, 0xb7, 0xcc, 0xbb, 0x62, 0x7f, 0x89, 0x69, 0xc5, 0x99, 0xd7, 0xf2, 0xf0, 0xb5, 0x12, 0x50, 0x5d, 0xc7, + 0x2b, 0x4a, 0xa3, 0xcb, 0x15, 0xa5, 0xa8, 0x04, 0x35, 0x6c, 0x60, 0xed, 0x4d, 0xc4, 0x4b, 0x2f, 0xf4, 0xeb, 0x2e, + 0x6a, 0xd0, 0x91, 0x2a, 0xc3, 0x53, 0xfc, 0xfa, 0x2b, 0x00, 0xe4, 0x50, 0x42, 0xad, 0x1d, 0xe3, 0xbd, 0x1a, 0xbc, + 0x45, 0x3d, 0xcb, 0x19, 0xec, 0x99, 0x0b, 0xf3, 0x68, 0xfe, 0xe6, 0xc6, 0x63, 0x10, 0x0f, 0x3d, 0xb0, 0x27, 0xf5, + 0xaa, 0xde, 0x38, 0x6e, 0xf9, 0x2f, 0xff, 0xec, 0xfb, 0xff, 0xf2, 0xcf, 0xb7, 0x36, 0xc5, 0x51, 0xc1, 0x65, 0xe7, + 0xd5, 0xb0, 0xeb, 0xa9, 0xbb, 0x7a, 0xa6, 0x3a, 0xb9, 0x57, 0xb7, 0x59, 0xa2, 0x3f, 0xd6, 0x2f, 0x91, 0x7f, 0xa9, + 0x50, 0x50, 0xdf, 0xfa, 0x2d, 0x80, 0x21, 0x5e, 0xb7, 0x42, 0x86, 0x8d, 0x7e, 0x17, 0x68, 0x27, 0x6e, 0x70, 0xa7, + 0x15, 0xf9, 0xed, 0x14, 0xbe, 0x0d, 0x87, 0xdf, 0x09, 0xbe, 0x48, 0x07, 0x06, 0xd0, 0x4e, 0xd4, 0x8d, 0xa9, 0x5a, + 0x57, 0xbc, 0x74, 0xd9, 0x5b, 0x2a, 0x91, 0x6a, 0x25, 0x68, 0xba, 0xdd, 0xe6, 0xd6, 0x96, 0x83, 0xdd, 0xdf, 0xe0, + 0x9b, 0x21, 0xf6, 0x4e, 0x73, 0x15, 0x03, 0xb9, 0x41, 0x34, 0xe0, 0x10, 0x75, 0xac, 0x68, 0xd0, 0x25, 0xb9, 0x80, + 0xa5, 0x98, 0x61, 0x8a, 0x60, 0x7a, 0x60, 0x0e, 0x0b, 0x7b, 0xed, 0x99, 0x70, 0x6c, 0x82, 0x45, 0xd6, 0x96, 0x0e, + 0x4f, 0x8d, 0xe8, 0x9e, 0x75, 0x48, 0xf4, 0xa2, 0xc6, 0xac, 0xb2, 0x97, 0xc9, 0x4b, 0x44, 0x03, 0xf1, 0x44, 0xbc, + 0x2b, 0xe3, 0xeb, 0x75, 0xf1, 0xf6, 0xef, 0x6f, 0x8f, 0xb7, 0xc7, 0x77, 0x8c, 0xb7, 0x7f, 0xff, 0x07, 0xc7, 0xdb, + 0xbf, 0x36, 0xe3, 0xed, 0xb8, 0x88, 0x3f, 0xdf, 0x29, 0x26, 0xae, 0x22, 0x95, 0xd9, 0x45, 0x11, 0xb6, 0xa4, 0xa5, + 0x04, 0x8e, 0x34, 0x06, 0xc4, 0xff, 0xed, 0xe3, 0xdb, 0x30, 0xd1, 0x42, 0x74, 0x9b, 0xc2, 0xd9, 0x92, 0x07, 0x99, + 0x0a, 0x26, 0x37, 0x75, 0xee, 0x77, 0xe3, 0x81, 0xba, 0xec, 0x0a, 0x2e, 0x8c, 0xab, 0x0f, 0x04, 0xda, 0x2a, 0xdc, + 0x1c, 0xd0, 0xdb, 0xaa, 0x75, 0xc7, 0xf6, 0xb6, 0x4a, 0x3a, 0x36, 0x47, 0xe8, 0xa8, 0xb3, 0x64, 0x71, 0x53, 0x72, + 0x6e, 0xff, 0xa7, 0xa3, 0x56, 0x67, 0xb7, 0x35, 0x81, 0xde, 0xc0, 0x87, 0xf0, 0xd4, 0xec, 0xec, 0xee, 0xe2, 0xd3, + 0x85, 0x7a, 0x6a, 0xe3, 0x53, 0xac, 0x9e, 0x1e, 0xe2, 0xd3, 0x40, 0x3d, 0x3d, 0xc2, 0xa7, 0xa1, 0x7a, 0x7a, 0x8c, + 0x4f, 0xe7, 0x76, 0x79, 0xc4, 0x34, 0x70, 0x8f, 0xdd, 0xbe, 0x27, 0x4c, 0x51, 0x55, 0xf6, 0xd8, 0x6b, 0x61, 0x40, + 0x3b, 0x3a, 0x0b, 0x62, 0x4f, 0x38, 0xd4, 0x41, 0xe1, 0x5d, 0x8c, 0x59, 0x1a, 0x50, 0x4e, 0xf4, 0x73, 0x7c, 0x5b, + 0x10, 0xd8, 0xc0, 0x87, 0xf1, 0x84, 0xa9, 0xd7, 0xa6, 0x2b, 0xac, 0x41, 0x25, 0x1f, 0x35, 0xfb, 0x65, 0x47, 0xaf, + 0x93, 0x88, 0x84, 0xab, 0xf4, 0x4e, 0x5a, 0xb9, 0xaa, 0x4e, 0x4c, 0xd7, 0xd0, 0x2b, 0xbc, 0x26, 0xa8, 0x6a, 0xf8, + 0x95, 0x23, 0x90, 0xcd, 0x8d, 0x4b, 0x70, 0x2c, 0x57, 0x06, 0x5a, 0x11, 0x22, 0x1d, 0x68, 0x25, 0x9c, 0xf4, 0xd3, + 0x61, 0x74, 0xa6, 0xbf, 0xbf, 0x01, 0xdb, 0x21, 0x3a, 0x93, 0x2d, 0xd7, 0x07, 0x56, 0x09, 0x44, 0x33, 0xa8, 0xaa, + 0x80, 0x40, 0xc7, 0x13, 0x97, 0x06, 0xc3, 0x04, 0x32, 0x56, 0x8a, 0xd4, 0xa9, 0x87, 0x59, 0x69, 0xfa, 0x7a, 0x11, + 0x50, 0xb4, 0x2a, 0xd8, 0x03, 0x13, 0x86, 0x4a, 0x05, 0x85, 0xa1, 0x02, 0x0b, 0x44, 0xf5, 0x9a, 0x70, 0xaa, 0x72, + 0xfd, 0xd6, 0x07, 0x55, 0x2d, 0x15, 0x4f, 0x35, 0xcf, 0xa0, 0xf5, 0x01, 0xf4, 0x72, 0x14, 0xef, 0x5e, 0x6b, 0x80, + 0xff, 0xc9, 0x18, 0xe1, 0xbd, 0xd1, 0x68, 0x74, 0x63, 0x7c, 0xf5, 0xde, 0x70, 0xc4, 0xda, 0xec, 0x61, 0x07, 0xcf, + 0x27, 0x1b, 0x32, 0x6a, 0xd7, 0x2a, 0x89, 0x76, 0xf3, 0xbb, 0x35, 0xc6, 0x00, 0x1f, 0x1f, 0xcf, 0xef, 0x1e, 0x6b, + 0x2d, 0x81, 0x2a, 0xf3, 0x09, 0x48, 0xc5, 0x38, 0x0d, 0x9a, 0xa5, 0x7f, 0x2e, 0x83, 0x93, 0xf7, 0x9e, 0x3c, 0x79, + 0x52, 0xfa, 0x43, 0xf5, 0xd4, 0x1c, 0x0e, 0x4b, 0x7f, 0x30, 0xd7, 0x68, 0x34, 0x9b, 0xa3, 0x51, 0xe9, 0xc7, 0xaa, + 0x60, 0xb7, 0x3d, 0x18, 0xee, 0xb6, 0x4b, 0xff, 0xc2, 0x68, 0x51, 0xfa, 0x4c, 0x3e, 0xe5, 0x6c, 0x58, 0x3b, 0xe4, + 0x7c, 0x0c, 0xde, 0xb6, 0x2f, 0x18, 0x6d, 0x8e, 0x86, 0xb6, 0xf8, 0x1a, 0x44, 0x33, 0x9e, 0xa1, 0x00, 0xee, 0x00, + 0x9f, 0x1f, 0x6d, 0xca, 0x6b, 0xcc, 0xe2, 0xad, 0xe4, 0x25, 0x6c, 0xa1, 0x9f, 0xcd, 0x60, 0x23, 0x32, 0x33, 0x05, + 0x19, 0x63, 0x15, 0x8b, 0xac, 0x55, 0x23, 0x67, 0x51, 0xf5, 0xcf, 0x61, 0x5c, 0xc5, 0x20, 0x51, 0xda, 0x60, 0x4b, + 0x91, 0x8c, 0xf3, 0xdd, 0x3a, 0x19, 0xff, 0xc5, 0xed, 0x32, 0xfe, 0xea, 0x6e, 0x22, 0xfe, 0x8b, 0x3f, 0x58, 0xc4, + 0x7f, 0x67, 0x8a, 0x78, 0x21, 0xc4, 0xf6, 0x79, 0x68, 0x0f, 0xc6, 0x6c, 0xf0, 0xe9, 0x34, 0xbb, 0x6c, 0xe0, 0x96, + 0xc8, 0x6d, 0x92, 0x9e, 0x93, 0xdf, 0x7a, 0x20, 0xaa, 0x06, 0x33, 0x5e, 0x71, 0x4e, 0x4a, 0xf2, 0x5d, 0x1a, 0xda, + 0xef, 0x94, 0xfd, 0x2e, 0x4a, 0x46, 0x23, 0x28, 0x1a, 0x8d, 0x6c, 0x75, 0x79, 0x03, 0xc4, 0x16, 0xb5, 0x7a, 0x5b, + 0x2b, 0xa1, 0x56, 0x9f, 0x7f, 0x6e, 0x96, 0x99, 0x05, 0x32, 0x64, 0x69, 0x86, 0x27, 0x65, 0xcd, 0x30, 0x2e, 0x70, + 0xab, 0xe1, 0x9b, 0xd7, 0x97, 0x5e, 0x69, 0x25, 0x02, 0xab, 0xcb, 0x00, 0x57, 0xf1, 0x55, 0xe3, 0xed, 0xa9, 0x55, + 0x84, 0x15, 0x16, 0x54, 0x66, 0xd6, 0x3d, 0xbd, 0x7a, 0x35, 0x74, 0xf6, 0xb9, 0x5b, 0xc6, 0xc5, 0xbb, 0x74, 0x21, + 0x63, 0x59, 0xc0, 0x18, 0x86, 0x26, 0x5a, 0x25, 0xcf, 0xce, 0xce, 0x92, 0xe5, 0x1c, 0x58, 0xd1, 0xbd, 0x57, 0xc3, + 0x37, 0x30, 0x3b, 0x4a, 0x5d, 0x46, 0x3f, 0x99, 0x21, 0x52, 0xfb, 0x28, 0x27, 0x5b, 0x1d, 0xed, 0xce, 0xa5, 0xfc, + 0x97, 0x49, 0x5f, 0x8c, 0x0e, 0x51, 0x69, 0xe0, 0x61, 0x59, 0xca, 0xcc, 0x5a, 0x20, 0xc4, 0x14, 0xdf, 0xff, 0x26, + 0x7a, 0xc6, 0xb7, 0x89, 0xf0, 0xe2, 0xc2, 0x88, 0x0b, 0xd6, 0x96, 0xab, 0x54, 0x81, 0x41, 0x11, 0xdd, 0xdb, 0xc7, + 0x10, 0xa5, 0x88, 0x11, 0x2a, 0x22, 0xda, 0x56, 0x8f, 0xbe, 0xca, 0x88, 0x65, 0x85, 0x21, 0x06, 0x33, 0xf5, 0x82, + 0xa8, 0x2a, 0x55, 0x50, 0x9a, 0x81, 0x6f, 0xaa, 0x11, 0xd4, 0xa2, 0x30, 0x1b, 0xc0, 0x9e, 0x0a, 0x31, 0x0a, 0xd3, + 0x90, 0x3c, 0xd8, 0x9c, 0x57, 0x2b, 0x0f, 0x5d, 0x25, 0xd8, 0x82, 0x79, 0x41, 0x06, 0x63, 0x87, 0xae, 0x55, 0x03, + 0x3d, 0x5d, 0x8a, 0xce, 0xdd, 0x7c, 0xee, 0x75, 0xe2, 0x17, 0x17, 0x1e, 0xfc, 0x59, 0x7f, 0x9a, 0x83, 0xd0, 0x39, + 0xfd, 0x14, 0xf3, 0x06, 0x8f, 0xa6, 0x0d, 0xb4, 0xee, 0x29, 0xc8, 0x23, 0xa5, 0x33, 0xe5, 0x6f, 0x88, 0x7b, 0x96, + 0x9d, 0x59, 0x81, 0xc7, 0x63, 0x64, 0xa3, 0x06, 0x69, 0x96, 0xb2, 0x4e, 0x3d, 0x4f, 0xc7, 0x3c, 0x6d, 0x51, 0xc4, + 0xea, 0xcf, 0x33, 0x3c, 0x4e, 0xe3, 0x57, 0x41, 0x53, 0x4a, 0xf5, 0xa6, 0x3a, 0x6a, 0x69, 0xae, 0x6c, 0x1f, 0x48, + 0xda, 0x6e, 0x93, 0xf2, 0xca, 0x97, 0x8f, 0x94, 0xd6, 0x1d, 0x09, 0xdd, 0x96, 0xb5, 0x82, 0xc1, 0x21, 0xf5, 0x67, + 0xa4, 0xfb, 0x2c, 0x16, 0x53, 0xd6, 0xca, 0x5d, 0x20, 0x0b, 0xa2, 0x11, 0xbe, 0x96, 0xf4, 0x2e, 0x2d, 0x4f, 0x29, + 0x65, 0x7c, 0x8e, 0x5a, 0x26, 0x68, 0x3d, 0x99, 0x5e, 0xde, 0x7d, 0xf8, 0x9b, 0xd1, 0x2f, 0x25, 0x8d, 0xd4, 0xcd, + 0x7f, 0xdb, 0xee, 0xe0, 0x3e, 0x48, 0xa2, 0xab, 0x20, 0x4e, 0x49, 0xe5, 0x9d, 0x62, 0x94, 0xa7, 0x33, 0xcd, 0x64, + 0xfa, 0x55, 0xce, 0x12, 0xfa, 0xed, 0x1f, 0xb9, 0x14, 0xbb, 0x8f, 0xa6, 0x97, 0x6a, 0x35, 0x5a, 0x0b, 0x69, 0x55, + 0x7f, 0x68, 0xf6, 0xd4, 0xfa, 0x74, 0xad, 0x7a, 0x06, 0xd0, 0x43, 0x80, 0x41, 0xe8, 0xd9, 0x46, 0x2e, 0xa0, 0x6a, + 0x42, 0x89, 0x91, 0x3f, 0x56, 0x0d, 0x64, 0xf9, 0xbb, 0x20, 0xb9, 0xa3, 0x82, 0x75, 0xf0, 0xfd, 0xb0, 0xf1, 0x20, + 0x4a, 0xa4, 0x2e, 0x9f, 0xc4, 0xc3, 0x61, 0xc2, 0x3a, 0x4a, 0x5d, 0x5b, 0xad, 0x47, 0x98, 0x7e, 0x65, 0x2e, 0x59, + 0x7d, 0x55, 0x0c, 0xe2, 0x69, 0x3a, 0x45, 0xa7, 0x60, 0x3e, 0xe0, 0x4b, 0x5e, 0x57, 0x92, 0x53, 0xe6, 0x25, 0x35, + 0x2b, 0xe2, 0xd1, 0xf7, 0x3a, 0x2e, 0x0f, 0xc1, 0x76, 0xa1, 0x05, 0x6f, 0x76, 0x78, 0x36, 0x0d, 0x1a, 0xbb, 0x75, + 0x44, 0xb0, 0x4a, 0xa3, 0xe0, 0xad, 0x40, 0xcb, 0x43, 0x65, 0x25, 0x04, 0xb4, 0xe5, 0xb7, 0x64, 0x19, 0x0d, 0x80, + 0x2f, 0x12, 0xd5, 0x45, 0x65, 0x1d, 0x99, 0x7f, 0x9b, 0xdd, 0xf2, 0xd9, 0xea, 0xdd, 0xf2, 0x99, 0xda, 0x2d, 0x37, + 0x73, 0xec, 0xbd, 0x51, 0x0b, 0xff, 0xeb, 0x54, 0x08, 0xc1, 0xaa, 0x00, 0x39, 0x2c, 0x34, 0xd3, 0x1a, 0x6d, 0xf8, + 0x87, 0x86, 0xc6, 0x18, 0x74, 0x13, 0xf3, 0xc9, 0xbc, 0xa6, 0x85, 0x85, 0xf8, 0xd7, 0xac, 0x55, 0xb5, 0x1e, 0x60, + 0x1d, 0xf6, 0x7a, 0xb8, 0x5c, 0xd7, 0xbe, 0x79, 0xd3, 0x82, 0xbc, 0xe2, 0x4e, 0xa0, 0x84, 0x31, 0x78, 0x0e, 0xd1, + 0xe9, 0x29, 0x94, 0x8e, 0xb2, 0xc1, 0xac, 0xf8, 0x5b, 0x09, 0xbf, 0x24, 0xe2, 0x8d, 0x5b, 0x7a, 0x61, 0x1c, 0xd5, + 0x55, 0xe4, 0xf2, 0xa9, 0x11, 0xe6, 0x7a, 0x9d, 0x82, 0x02, 0x18, 0x93, 0x39, 0x6d, 0xff, 0xc1, 0x8a, 0x4d, 0xf0, + 0xef, 0xb2, 0x36, 0x2b, 0x91, 0xf9, 0xbd, 0xc4, 0xb8, 0x91, 0x08, 0xbf, 0x8a, 0x06, 0xe6, 0x1a, 0x36, 0x9f, 0xac, + 0x06, 0xf7, 0x48, 0xcd, 0xd4, 0x57, 0x4a, 0x41, 0xea, 0x1d, 0x30, 0x4a, 0xa3, 0x59, 0xc2, 0x6f, 0x1e, 0x75, 0x1d, + 0x67, 0x2c, 0x8d, 0x7a, 0x83, 0x40, 0xaf, 0xda, 0xde, 0x51, 0x4a, 0xdf, 0xfb, 0xec, 0x01, 0xfe, 0x27, 0xd2, 0x05, + 0xae, 0x2a, 0x53, 0x5d, 0xb8, 0xaa, 0x68, 0xaa, 0x4f, 0x6a, 0xb6, 0xb8, 0xd0, 0xe0, 0x64, 0x8e, 0xdf, 0xb5, 0x35, + 0x1a, 0x95, 0x77, 0x6a, 0x2e, 0x8d, 0xac, 0x5f, 0xd5, 0xfa, 0xd7, 0x0d, 0x7e, 0xc7, 0xb6, 0x03, 0x61, 0xb8, 0xd6, + 0xdb, 0xca, 0xdf, 0x61, 0x5a, 0x6a, 0xac, 0x28, 0x4e, 0xed, 0x27, 0xe1, 0x95, 0xf6, 0x50, 0xc4, 0xb9, 0x12, 0x3a, + 0x29, 0x13, 0xe1, 0xa4, 0xfc, 0x85, 0x87, 0xf7, 0xf1, 0x85, 0x84, 0xd6, 0xe5, 0x24, 0x49, 0xc1, 0x48, 0x1a, 0x73, + 0x3e, 0x0d, 0x76, 0x76, 0x2e, 0x2e, 0x2e, 0xfc, 0x8b, 0x5d, 0x3f, 0xcb, 0xcf, 0x76, 0xda, 0xcd, 0x66, 0x13, 0xdf, + 0x23, 0x67, 0x5b, 0xe7, 0x31, 0xbb, 0x78, 0x0a, 0x76, 0xb0, 0xfd, 0xd8, 0x7a, 0x62, 0x3d, 0xde, 0xb5, 0x1e, 0x3e, + 0xb2, 0x2d, 0x12, 0xe7, 0x50, 0xb2, 0x6b, 0x5b, 0x42, 0x9c, 0x87, 0x36, 0x14, 0x77, 0xf7, 0xce, 0x94, 0x45, 0x86, + 0xf7, 0x74, 0x84, 0xbd, 0x03, 0xce, 0x41, 0xf6, 0x89, 0xd5, 0x37, 0xae, 0x28, 0x6b, 0x48, 0xa5, 0xa0, 0x1e, 0x71, + 0xf7, 0x0e, 0xa2, 0x69, 0x40, 0x4c, 0x61, 0x16, 0x62, 0x0c, 0x46, 0x94, 0xd2, 0x14, 0x68, 0x65, 0x9e, 0xc2, 0x37, + 0x4c, 0xec, 0xb4, 0xe0, 0xfb, 0x9b, 0xf6, 0x63, 0xd0, 0x58, 0xe7, 0x8d, 0x07, 0x83, 0x66, 0xa3, 0x65, 0xb5, 0x1a, + 0x6d, 0xff, 0xb1, 0xd5, 0x16, 0xff, 0x82, 0xc4, 0xdb, 0xb5, 0x5a, 0xf0, 0x6d, 0xd7, 0x82, 0xe7, 0xf3, 0x07, 0xe2, + 0x00, 0x3a, 0xb2, 0x77, 0xba, 0x7b, 0xf8, 0xb3, 0x6a, 0x80, 0xd4, 0x67, 0xb6, 0xf8, 0x21, 0x48, 0xfb, 0x9e, 0x59, + 0xda, 0x7a, 0xb2, 0xb2, 0xb8, 0xfd, 0x78, 0x65, 0xf1, 0xee, 0xa3, 0x95, 0xc5, 0x0f, 0x1e, 0xd6, 0x8b, 0x77, 0xce, + 0x44, 0x95, 0xde, 0xe5, 0xa1, 0x3d, 0x89, 0x60, 0xd9, 0x2f, 0x9d, 0x16, 0xc0, 0xd9, 0xb4, 0x1a, 0xf8, 0xf1, 0xb8, + 0xed, 0xea, 0x5e, 0xa7, 0xd8, 0x4b, 0x63, 0xf9, 0xf8, 0x09, 0x60, 0xf9, 0xb2, 0xfd, 0x68, 0x80, 0xed, 0x08, 0x51, + 0xf8, 0x3b, 0xdf, 0x7d, 0x32, 0x00, 0xf9, 0x6e, 0xe1, 0x1f, 0xfc, 0x37, 0x7e, 0xd8, 0x1e, 0x88, 0x87, 0x26, 0xd6, + 0x7f, 0xd3, 0x7a, 0x5c, 0x40, 0x53, 0xfc, 0xef, 0x17, 0x6d, 0x10, 0xa3, 0x39, 0x6e, 0x8e, 0xfb, 0x00, 0x68, 0xf4, + 0x64, 0xdc, 0xf6, 0x3f, 0x3b, 0x7f, 0xec, 0x3f, 0x19, 0xb7, 0x1e, 0x7f, 0x23, 0x9e, 0x12, 0xa0, 0xe0, 0x67, 0xf8, + 0xf7, 0xcd, 0x6e, 0x13, 0xbc, 0x4b, 0xff, 0xc9, 0xf9, 0xae, 0xbf, 0x9b, 0x34, 0x1e, 0xf9, 0x4f, 0xf0, 0xaf, 0x1a, + 0x6e, 0x9c, 0x4d, 0x98, 0x6d, 0xe1, 0x7a, 0x2f, 0x78, 0x5b, 0xe6, 0x1c, 0xed, 0x07, 0xd6, 0xc3, 0x07, 0x2f, 0x9f, + 0xc0, 0x1a, 0x8d, 0x5b, 0x6d, 0xf8, 0x77, 0xdd, 0xd7, 0x6f, 0x90, 0xf0, 0x72, 0xe0, 0x88, 0x61, 0x86, 0xbd, 0x22, + 0x1c, 0xbd, 0xd3, 0xf0, 0xbe, 0x07, 0x0e, 0xd4, 0x6a, 0xef, 0x9a, 0xb1, 0xdb, 0x23, 0xa8, 0xec, 0x6e, 0xee, 0x35, + 0x63, 0x7f, 0xac, 0x7b, 0xcd, 0xd9, 0x42, 0x04, 0xf5, 0x92, 0x2f, 0x79, 0xd1, 0x8b, 0xae, 0xd7, 0x07, 0xee, 0x1c, + 0xfd, 0x85, 0xf7, 0xf1, 0x36, 0x09, 0xb4, 0x8e, 0x99, 0x19, 0x6c, 0xc8, 0x70, 0x23, 0xe3, 0x8f, 0x2b, 0xd2, 0xdd, + 0x9f, 0x75, 0x04, 0xc9, 0x6f, 0x27, 0xc8, 0x37, 0x77, 0xa3, 0x47, 0xfe, 0x07, 0xd3, 0xa3, 0x30, 0xe9, 0x51, 0x0b, + 0xe7, 0x92, 0x3b, 0x4b, 0xee, 0xe8, 0x01, 0x3d, 0x3b, 0x98, 0x84, 0xbd, 0x6d, 0xef, 0x30, 0x2c, 0x2a, 0x6c, 0x71, + 0x88, 0xf0, 0xf4, 0xd7, 0xc4, 0x9f, 0xc5, 0x8d, 0x8b, 0xd0, 0x96, 0xbe, 0xff, 0x14, 0xdf, 0xdb, 0xad, 0x1e, 0xce, + 0xc5, 0xad, 0xbe, 0x90, 0xae, 0xe4, 0x3e, 0xd4, 0x71, 0x03, 0xbc, 0x04, 0x13, 0xce, 0x33, 0x1e, 0xe1, 0x0f, 0xc3, + 0x01, 0xb9, 0xe9, 0x27, 0xe4, 0x62, 0x9e, 0x30, 0x3c, 0x24, 0x1f, 0x88, 0x77, 0x28, 0xc3, 0x57, 0x79, 0xdd, 0x16, + 0x6f, 0x71, 0x7c, 0x8d, 0x37, 0x50, 0x54, 0x60, 0x7a, 0x82, 0x2e, 0xf5, 0x1b, 0x36, 0x8c, 0x23, 0xc7, 0x76, 0xa6, + 0xb0, 0x91, 0x61, 0x96, 0x46, 0xed, 0xfa, 0x07, 0xdd, 0xfc, 0x70, 0x6d, 0xf5, 0xeb, 0x64, 0x39, 0xbe, 0xed, 0x31, + 0x3c, 0x92, 0x41, 0x2d, 0x5b, 0x9a, 0xf9, 0x30, 0xbe, 0x2a, 0xc9, 0x51, 0xa2, 0x57, 0xa6, 0x81, 0x2d, 0x6c, 0x83, + 0x96, 0xdf, 0x06, 0x5f, 0x81, 0x8a, 0xf1, 0xed, 0x79, 0xdf, 0x39, 0x8d, 0x5d, 0xb0, 0x5d, 0x8c, 0x6e, 0x7a, 0xa0, + 0xbe, 0xfe, 0xb1, 0x2b, 0xfd, 0x83, 0x8c, 0xf5, 0x3b, 0x33, 0xb6, 0xe0, 0x88, 0x7b, 0x02, 0x77, 0x5b, 0xbc, 0xa5, + 0x84, 0xa8, 0x47, 0x77, 0x46, 0xa1, 0xcc, 0x31, 0x7f, 0x98, 0x4f, 0xbc, 0x9d, 0x4f, 0xfc, 0x06, 0x67, 0x59, 0x35, + 0xe1, 0xee, 0x9c, 0x02, 0xef, 0x98, 0x64, 0x8c, 0x57, 0x75, 0x31, 0x0e, 0x1b, 0x1a, 0x34, 0xc5, 0x67, 0xb7, 0x46, + 0x64, 0xee, 0x69, 0x80, 0x88, 0xc0, 0xa1, 0xfc, 0xac, 0x8a, 0xd5, 0x17, 0x19, 0x5d, 0x01, 0xb7, 0x1d, 0x7f, 0xf9, + 0x88, 0x3e, 0x96, 0x62, 0x37, 0xe2, 0xec, 0x60, 0xa1, 0xb4, 0x1a, 0xaa, 0x8a, 0xd1, 0x14, 0x4f, 0xaf, 0x0e, 0xe5, + 0x6b, 0x3f, 0x6c, 0x0c, 0x81, 0x52, 0xe8, 0xbb, 0x7a, 0xe5, 0xe0, 0x36, 0xa8, 0x46, 0xfa, 0x21, 0x60, 0xca, 0x60, + 0x42, 0xed, 0x87, 0xb7, 0x6e, 0x2c, 0xe9, 0xf3, 0x84, 0xb6, 0xd0, 0x7d, 0x43, 0x76, 0x1e, 0x0f, 0xa4, 0x0a, 0xf3, + 0x2c, 0x79, 0x5b, 0xb0, 0x41, 0x4b, 0x13, 0xb6, 0x3c, 0xe1, 0xf5, 0xc3, 0x03, 0xea, 0xe3, 0x30, 0xcd, 0xec, 0xee, + 0xfd, 0xce, 0x3a, 0xe2, 0xe3, 0xaf, 0x12, 0x1f, 0x81, 0x97, 0xf9, 0xb7, 0xe1, 0x7d, 0xfc, 0x5d, 0xe2, 0xfb, 0x7d, + 0xdb, 0xf5, 0x49, 0x01, 0xdc, 0xaf, 0x7e, 0x9c, 0x18, 0xa5, 0xdf, 0x36, 0xe8, 0x6a, 0xef, 0xae, 0x4a, 0x5b, 0x2a, + 0xe8, 0xf6, 0xc3, 0x4a, 0x41, 0xc3, 0x77, 0x43, 0x22, 0x83, 0xb2, 0x68, 0xfb, 0x0f, 0x0d, 0xb1, 0x7f, 0xde, 0xc0, + 0xcf, 0x9a, 0xe0, 0x7f, 0x00, 0x0d, 0x94, 0xe4, 0x7f, 0x0d, 0xcd, 0x77, 0x85, 0x92, 0x81, 0x7e, 0xdf, 0x93, 0x58, + 0x96, 0x22, 0xb9, 0xb6, 0x0d, 0x56, 0x9c, 0xc6, 0x88, 0x6c, 0x2c, 0xdb, 0x73, 0xf4, 0x2f, 0x1e, 0xc9, 0x5d, 0x29, + 0xe3, 0x40, 0xcf, 0xa1, 0xaf, 0xa3, 0xdf, 0xe4, 0xbf, 0xaa, 0xce, 0xab, 0x49, 0x89, 0x15, 0x53, 0xe0, 0xbe, 0x5e, + 0x38, 0xf1, 0xe9, 0x88, 0x2b, 0x0c, 0xfa, 0x55, 0x40, 0xeb, 0x19, 0x5a, 0xde, 0x75, 0x70, 0x0d, 0x11, 0xc1, 0xe8, + 0x6d, 0xc3, 0x34, 0xc9, 0xab, 0x61, 0xb9, 0x38, 0x3f, 0xa6, 0x83, 0xe5, 0x99, 0x71, 0xa7, 0x50, 0x46, 0xef, 0x30, + 0x59, 0x74, 0x18, 0xe7, 0xf4, 0x62, 0x04, 0x05, 0x7a, 0x2d, 0x02, 0x58, 0x51, 0x89, 0xa4, 0x04, 0x2b, 0x7a, 0x36, + 0x16, 0xd9, 0x81, 0x4d, 0xe1, 0x23, 0xdb, 0x7c, 0xdd, 0xbe, 0x79, 0x73, 0x9d, 0x38, 0x99, 0x12, 0xbb, 0x71, 0xaf, + 0x22, 0x7d, 0x6c, 0x90, 0xb6, 0x6b, 0x77, 0x09, 0xd9, 0x60, 0x88, 0x6b, 0xf5, 0xfb, 0x72, 0xa6, 0x00, 0xb2, 0x4d, + 0x42, 0xeb, 0x71, 0x89, 0x84, 0xae, 0xa4, 0xd3, 0x29, 0x8b, 0xb8, 0x1f, 0xa5, 0x22, 0x0b, 0xc1, 0x10, 0x53, 0x5e, + 0x8b, 0xed, 0xba, 0x25, 0xc8, 0x46, 0x23, 0x6f, 0x42, 0xee, 0x6e, 0x28, 0x54, 0x17, 0x3d, 0x18, 0xaf, 0xe5, 0xb3, + 0x8e, 0xdb, 0xdd, 0x77, 0x87, 0xfb, 0x96, 0xd8, 0x94, 0x7b, 0x3b, 0xf0, 0xb8, 0x47, 0xfe, 0xb8, 0x48, 0xde, 0x0f, + 0x45, 0xf2, 0xbe, 0x25, 0x6f, 0x71, 0x50, 0x86, 0xe3, 0x8e, 0x40, 0xdb, 0xb6, 0x58, 0x3a, 0x10, 0x81, 0xc4, 0x09, + 0xf8, 0x2c, 0x31, 0xbe, 0xa2, 0x71, 0x07, 0xbb, 0x36, 0x70, 0xc1, 0x80, 0x9b, 0x45, 0xd4, 0x51, 0xd9, 0x35, 0x3c, + 0x55, 0x61, 0x47, 0xb0, 0x46, 0x98, 0xca, 0x40, 0x94, 0x43, 0xe9, 0xe4, 0xc5, 0xe5, 0xd6, 0xc5, 0xec, 0x74, 0x02, + 0x72, 0x52, 0xe5, 0x10, 0x7e, 0x94, 0x1d, 0xf6, 0x68, 0xaa, 0xee, 0x49, 0x29, 0xe3, 0xa2, 0xea, 0xf5, 0xf9, 0x0b, + 0x3f, 0x35, 0x2c, 0xb0, 0x97, 0x7a, 0x01, 0xb3, 0xf0, 0xc7, 0xbb, 0x5d, 0x1d, 0x89, 0x34, 0xeb, 0x4a, 0x40, 0x7d, + 0xb7, 0x7b, 0x12, 0x4c, 0xe5, 0x78, 0xaf, 0xb3, 0xa5, 0x9f, 0x2d, 0xd6, 0x72, 0xb2, 0x47, 0xd9, 0xa9, 0xe2, 0x6a, + 0x93, 0x04, 0x18, 0x56, 0x10, 0x60, 0x92, 0x26, 0x80, 0x45, 0xe7, 0xaa, 0xf6, 0xc3, 0xa6, 0x4a, 0x78, 0x85, 0x32, + 0xdc, 0x90, 0xa2, 0x8b, 0x31, 0x49, 0x2d, 0x98, 0x3b, 0x6e, 0x75, 0xf7, 0x22, 0x69, 0x5c, 0xa2, 0xf0, 0x28, 0x40, + 0x7a, 0x40, 0x67, 0xb4, 0xe0, 0xfc, 0x38, 0xdb, 0xb9, 0x60, 0xa7, 0x8d, 0x68, 0x1a, 0x57, 0x91, 0x53, 0x34, 0x35, + 0xf4, 0x94, 0x59, 0x35, 0x13, 0x7e, 0x8d, 0x16, 0x90, 0x24, 0xc1, 0x5d, 0xca, 0xb0, 0x2c, 0x59, 0xe8, 0xc0, 0x42, + 0x40, 0x61, 0x92, 0xeb, 0x2a, 0x7c, 0x2b, 0x35, 0x6e, 0x69, 0x77, 0xff, 0xfa, 0x8f, 0xff, 0x5b, 0x46, 0x64, 0x81, + 0x2a, 0x2d, 0x35, 0xd6, 0x02, 0xa1, 0xcb, 0x3d, 0xba, 0xb5, 0xa2, 0x8f, 0x10, 0xd9, 0x25, 0xb8, 0xf6, 0xf1, 0xb0, + 0x31, 0x8e, 0x92, 0x11, 0x00, 0xb6, 0x96, 0x40, 0x66, 0x52, 0xb8, 0x84, 0xba, 0x5e, 0x84, 0x2c, 0xf8, 0x9b, 0xd2, + 0x9b, 0x55, 0x96, 0x2c, 0xed, 0x56, 0x33, 0xd9, 0xb9, 0xda, 0x50, 0xb5, 0x84, 0x67, 0xf5, 0xdb, 0x7d, 0x4a, 0xa8, + 0xd5, 0xf2, 0x9c, 0xa1, 0xa5, 0x3e, 0x02, 0xf9, 0xd7, 0x7f, 0xfa, 0xbb, 0xff, 0xa1, 0x1e, 0xf1, 0x64, 0xe3, 0xaf, + 0xff, 0xf0, 0x9f, 0x31, 0x1b, 0xd3, 0xd2, 0xa7, 0x1f, 0x24, 0x27, 0xac, 0xea, 0xe8, 0x43, 0x08, 0x0c, 0x0b, 0x53, + 0x9d, 0x26, 0x20, 0x06, 0xe3, 0x41, 0x3d, 0xf3, 0xf9, 0x80, 0x26, 0xa4, 0xcd, 0x26, 0xa1, 0xa3, 0x4d, 0x5b, 0x56, + 0x3c, 0x52, 0x23, 0x39, 0xf1, 0x22, 0x54, 0x22, 0xbd, 0xef, 0x74, 0xfb, 0xc3, 0xd7, 0xab, 0x31, 0x57, 0xf1, 0x3e, + 0x2c, 0x29, 0xab, 0x72, 0x0b, 0x03, 0xf1, 0x73, 0x7c, 0x0c, 0xda, 0x46, 0x31, 0x2d, 0x5e, 0xad, 0x4f, 0xe7, 0xa7, + 0x19, 0xc0, 0x3f, 0x42, 0x8a, 0x8b, 0xa8, 0x22, 0x9d, 0x79, 0x36, 0xd0, 0xe6, 0x4b, 0xae, 0x4a, 0x1a, 0x45, 0x38, + 0x8a, 0x0f, 0x9e, 0xfc, 0x4d, 0xf9, 0xe7, 0x09, 0x5a, 0x56, 0x96, 0x33, 0x89, 0x2e, 0xa5, 0xfb, 0xf8, 0xa8, 0xd9, + 0x9c, 0x5e, 0xba, 0xf3, 0x6a, 0x06, 0x6f, 0xdd, 0x64, 0x14, 0x89, 0x34, 0x07, 0xa4, 0xc3, 0x52, 0x1d, 0xf4, 0x04, + 0x8f, 0xa9, 0x89, 0x31, 0xb2, 0xb2, 0xfc, 0xd3, 0x9c, 0xe2, 0x6e, 0xf1, 0x2f, 0x78, 0xa8, 0x29, 0x43, 0x94, 0x50, + 0x62, 0x60, 0x31, 0x37, 0x7a, 0xb5, 0x45, 0xaf, 0x71, 0x6b, 0xf9, 0xea, 0x83, 0x79, 0x28, 0x6b, 0x1e, 0xa7, 0x3e, + 0xc0, 0x03, 0xd2, 0x71, 0xcb, 0x1b, 0xb7, 0xe7, 0x7a, 0x78, 0xce, 0xb3, 0x89, 0x79, 0x0a, 0xcb, 0x22, 0x36, 0x60, + 0x23, 0x15, 0xda, 0x95, 0xf5, 0xe2, 0x84, 0xb5, 0x1c, 0xef, 0xae, 0x98, 0x4b, 0x82, 0x44, 0xa7, 0xaf, 0x00, 0xcf, + 0x3d, 0xdc, 0x80, 0x40, 0xff, 0x2c, 0xe2, 0x01, 0xf1, 0x6b, 0xc7, 0x3c, 0xcb, 0x8d, 0x50, 0xca, 0x64, 0x73, 0x03, + 0x9e, 0x8e, 0x68, 0x8a, 0x41, 0xd6, 0xfa, 0xd5, 0x93, 0xd2, 0xa7, 0xee, 0xe6, 0x50, 0x22, 0x46, 0xf3, 0x8d, 0x3c, + 0x22, 0x7d, 0x5a, 0x0b, 0x6e, 0x48, 0x15, 0xd3, 0x76, 0xbd, 0x95, 0xf5, 0x42, 0x53, 0x8b, 0xda, 0x6f, 0xb8, 0x63, + 0x13, 0x98, 0xf6, 0x62, 0x2b, 0x2a, 0xc4, 0x56, 0x4f, 0xc3, 0x6f, 0xb4, 0xeb, 0x13, 0x4d, 0xa7, 0xd4, 0xd0, 0x05, + 0x26, 0x26, 0x83, 0x15, 0x65, 0x07, 0x1d, 0xff, 0x8b, 0xd3, 0x76, 0xd9, 0x46, 0x6e, 0x04, 0xf1, 0x4d, 0x9e, 0xc3, + 0xe3, 0xaf, 0xae, 0x74, 0xff, 0x1f, 0x1c, 0x1d, 0xa5, 0x5f, 0x1b, 0x82, 0x00, 0x00}; } // namespace web_server } // namespace esphome From 98277f6cebfd03c5cc00517727fd1839f1338162 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 4 Jul 2023 14:03:58 +1200 Subject: [PATCH 017/270] Bump version to 2023.6.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0d5b211c18..d8eda3fd63 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.3" +__version__ = "2023.6.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 62aee36f82bf0b88c61dbed0ebb067abfd0c3798 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Jul 2023 11:08:46 -1000 Subject: [PATCH 018/270] Fix bulk and single Bluetooth parser coexistence (#5073) --- .../bluetooth_proxy/bluetooth_connection.cpp | 4 ++ .../bluetooth_proxy/bluetooth_connection.h | 1 + .../bluetooth_proxy/bluetooth_proxy.cpp | 8 ++++ .../bluetooth_proxy/bluetooth_proxy.h | 1 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 42 +++++++++++++++---- .../esp32_ble_tracker/esp32_ble_tracker.h | 17 +++++--- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 26304325c1..97a25262cb 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -275,6 +275,10 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl return ESP_OK; } +esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { + return this->proxy_->get_advertisement_parser_type(); +} + } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 8b13f4d1c2..e6ab3cbccc 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -14,6 +14,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp_err_t read_characteristic(uint16_t handle); esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index b633fe2430..f188439d0e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -198,6 +198,12 @@ void BluetoothProxy::loop() { } } +esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { + if (this->raw_advertisements_) + return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; + return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; +} + BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { for (auto *connection : this->connections_) { if (connection->get_address() == address) @@ -435,6 +441,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection } this->api_connection_ = api_connection; this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; + this->parent_->recalculate_advertisement_parser_types(); } void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { @@ -444,6 +451,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti } this->api_connection_ = nullptr; this->raw_advertisements_ = false; + this->parent_->recalculate_advertisement_parser_types(); } void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 97b6396b55..35a37f934a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -51,6 +51,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; void dump_config() override; void loop() override; + esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { this->connections_.push_back(connection); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0f6c4117d2..1569ea0dd5 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -107,16 +107,16 @@ void ESP32BLETracker::loop() { ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); } - bool bulk_parsed = false; - - for (auto *listener : this->listeners_) { - bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - for (auto *client : this->clients_) { - bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + for (auto *client : this->clients_) { + client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } } - if (!bulk_parsed) { + if (this->parse_advertisements_) { for (size_t i = 0; i < index; i++) { ESPBTDevice device; device.parse_scan_rst(this->scan_result_buffer_[i]); @@ -284,6 +284,32 @@ void ESP32BLETracker::end_of_scan_() { void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { + listener->set_parent(this); + this->listeners_.push_back(listener); + this->recalculate_advertisement_parser_types(); +} + +void ESP32BLETracker::recalculate_advertisement_parser_types() { + this->raw_advertisements_ = false; + this->parse_advertisements_ = false; + for (auto *listener : this->listeners_) { + if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } + for (auto *client : this->clients_) { + if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { + this->parse_advertisements_ = true; + } else { + this->raw_advertisements_ = true; + } + } } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 43e88fbf2b..6efdded3ff 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -27,6 +27,11 @@ using namespace esp32_ble; using adv_data_t = std::vector; +enum AdvertisementParserType { + PARSED_ADVERTISEMENTS, + RAW_ADVERTISEMENTS, +}; + struct ServiceData { ESPBTUUID uuid; adv_data_t data; @@ -116,6 +121,9 @@ class ESPBTDeviceListener { virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { return false; }; + virtual AdvertisementParserType get_advertisement_parser_type() { + return AdvertisementParserType::PARSED_ADVERTISEMENTS; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: @@ -184,12 +192,9 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv void loop() override; - void register_listener(ESPBTDeviceListener *listener) { - listener->set_parent(this); - this->listeners_.push_back(listener); - } - + void register_listener(ESPBTDeviceListener *listener); void register_client(ESPBTClient *client); + void recalculate_advertisement_parser_types(); void print_bt_device_info(const ESPBTDevice &device); @@ -231,6 +236,8 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv bool scan_continuous_; bool scan_active_; bool scanner_idle_; + bool raw_advertisements_{false}; + bool parse_advertisements_{false}; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; From d7bfdd0efce65920404d2e947f1dfd2a63b00b78 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 9 Jul 2023 17:55:02 -0400 Subject: [PATCH 019/270] binary_sensor: Validate max_length for on_click/on_double_click (#5068) --- esphome/components/binary_sensor/__init__.py | 58 +++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index f4a5c95b12..eaf11c056a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -323,6 +323,18 @@ def validate_multi_click_timing(value): validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +def validate_click_timing(value): + for v in value: + min_length = v.get(CONF_MIN_LENGTH) + max_length = v.get(CONF_MAX_LENGTH) + if max_length < min_length: + raise cv.Invalid( + f"Max length ({max_length}) must be larger than min length ({min_length})." + ) + + return value + + BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.GenerateID(): cv.declare_id(BinarySensor), @@ -342,27 +354,33 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), } ), - cv.Optional(CONF_ON_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } + cv.Optional(CONF_ON_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + validate_click_timing, ), - cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), - cv.Optional( - CONF_MIN_LENGTH, default="50ms" - ): cv.positive_time_period_milliseconds, - cv.Optional( - CONF_MAX_LENGTH, default="350ms" - ): cv.positive_time_period_milliseconds, - } + cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All( + automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger), + cv.Optional( + CONF_MIN_LENGTH, default="50ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_MAX_LENGTH, default="350ms" + ): cv.positive_time_period_milliseconds, + } + ), + validate_click_timing, ), cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation( { From a77cf1beec9454b21568cc6ef4413ebafb04e5f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:24:49 +1200 Subject: [PATCH 020/270] Bump version to 2023.6.5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index d8eda3fd63..ea660723e4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.6.4" +__version__ = "2023.6.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 76b438f79c6d3087d162a6eaaee29048c5105bb0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:50:48 +1200 Subject: [PATCH 021/270] Bump version to 2023.7.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3442c392c7..e74f23e9b4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0-dev" +__version__ = "2023.7.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d7945de0013772a9f242afb8c7d843b9521e67ea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:19:04 +1200 Subject: [PATCH 022/270] Dont do mqtt ip lookup if `use_address` has ip address (#5096) * Dont do mqtt ip lookup id `use_address` is in config * Fix after actually testing =) --- esphome/__main__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c7c83ad83b..ecf0092b05 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -32,7 +32,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent +from esphome.helpers import indent, is_ip_address from esphome.util import ( run_external_command, run_external_process, @@ -308,8 +308,10 @@ def upload_program(config, args, host): password = ota_conf.get(CONF_PASSWORD, "") if ( - get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] - ) and CONF_MQTT in config: + not is_ip_address(CORE.address) + and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]) + and CONF_MQTT in config + ): from esphome import mqtt host = mqtt.get_esphome_device_ip( From 6bdc0c92fe2ac8b9bc4ca5ce23ee6aceac71c2a5 Mon Sep 17 00:00:00 2001 From: Pierre-Alexis Ciavaldini Date: Sun, 16 Jul 2023 21:42:01 +0200 Subject: [PATCH 023/270] ESP32 enable ADC2 when wifi is disabled (#4381) Co-authored-by: Keith Burzinski --- esphome/components/adc/__init__.py | 59 +++++++++++++++-- esphome/components/adc/adc_sensor.cpp | 93 ++++++++++++++++++--------- esphome/components/adc/adc_sensor.h | 20 ++++-- esphome/components/adc/sensor.py | 40 ++++++++++-- 4 files changed, 166 insertions(+), 46 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index cceaa594ef..99dad68501 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -24,6 +24,7 @@ ATTENUATION_MODES = { } adc1_channel_t = cg.global_ns.enum("adc1_channel_t") +adc2_channel_t = cg.global_ns.enum("adc2_channel_t") # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h # pin to adc1 channel mapping @@ -78,6 +79,49 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { }, } +ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { + # TODO: add other variants + VARIANT_ESP32: { + 4: adc2_channel_t.ADC2_CHANNEL_0, + 0: adc2_channel_t.ADC2_CHANNEL_1, + 2: adc2_channel_t.ADC2_CHANNEL_2, + 15: adc2_channel_t.ADC2_CHANNEL_3, + 13: adc2_channel_t.ADC2_CHANNEL_4, + 12: adc2_channel_t.ADC2_CHANNEL_5, + 14: adc2_channel_t.ADC2_CHANNEL_6, + 27: adc2_channel_t.ADC2_CHANNEL_7, + 25: adc2_channel_t.ADC2_CHANNEL_8, + 26: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S2: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32S3: { + 11: adc2_channel_t.ADC2_CHANNEL_0, + 12: adc2_channel_t.ADC2_CHANNEL_1, + 13: adc2_channel_t.ADC2_CHANNEL_2, + 14: adc2_channel_t.ADC2_CHANNEL_3, + 15: adc2_channel_t.ADC2_CHANNEL_4, + 16: adc2_channel_t.ADC2_CHANNEL_5, + 17: adc2_channel_t.ADC2_CHANNEL_6, + 18: adc2_channel_t.ADC2_CHANNEL_7, + 19: adc2_channel_t.ADC2_CHANNEL_8, + 20: adc2_channel_t.ADC2_CHANNEL_9, + }, + VARIANT_ESP32C3: { + 5: adc2_channel_t.ADC2_CHANNEL_0, + }, +} + def validate_adc_pin(value): if str(value).upper() == "VCC": @@ -89,11 +133,18 @@ def validate_adc_pin(value): if CORE.is_esp32: value = pins.internal_gpio_input_pin_number(value) variant = get_esp32_variant() - if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: + if ( + variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and variant not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL + ): raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") - if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: + if ( + value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + and value not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): raise cv.Invalid(f"{variant} doesn't support ADC on this pin") + return pins.internal_gpio_input_pin_schema(value) if CORE.is_esp8266: @@ -104,7 +155,7 @@ def validate_adc_pin(value): ) if value != 17: # A0 - raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") + raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") return pins.gpio_pin_schema( {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) @@ -112,7 +163,7 @@ def validate_adc_pin(value): if CORE.is_rp2040: value = pins.internal_gpio_input_pin_number(value) if value not in (26, 27, 28, 29): - raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") + raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") return pins.internal_gpio_input_pin_schema(value) raise NotImplementedError diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 9bfe0f5eed..bb6a7a8c85 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -20,20 +20,20 @@ namespace adc { static const char *const TAG = "adc"; -// 13bit for S2, and 12bit for all other esp32 variants +// 13-bit for S2, 12-bit for all other ESP32 variants #ifdef USE_ESP32 static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast(ADC_WIDTH_MAX - 1); #ifndef SOC_ADC_RTC_MAX_BITWIDTH #if USE_ESP32_VARIANT_ESP32S2 -static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; #else -static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; +static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; #endif #endif -static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) -static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) +static const int32_t ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) +static const int32_t ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) #endif #ifdef USE_RP2040 @@ -47,14 +47,21 @@ extern "C" #endif #ifdef USE_ESP32 - adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); - if (!autorange_) { - adc1_config_channel_atten(channel_, attenuation_); + if (channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); + if (!autorange_) { + adc1_config_channel_atten(channel1_, attenuation_); + } + } else if (channel2_ != ADC2_CHANNEL_MAX) { + if (!autorange_) { + adc2_config_channel_atten(channel2_, attenuation_); + } } // load characteristics for each attenuation - for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) { - auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, + for (int32_t i = 0; i < (int32_t) ADC_ATTEN_MAX; i++) { + auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; + auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, 1100, // default vref &cal_characteristics_[i]); switch (cal_value) { @@ -136,9 +143,9 @@ void ADCSensor::update() { #ifdef USE_ESP8266 float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC - int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) + int32_t raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance) #else - int raw = analogRead(this->pin_->get_pin()); // NOLINT + int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT #endif if (output_raw_) { return raw; @@ -150,29 +157,53 @@ float ADCSensor::sample() { #ifdef USE_ESP32 float ADCSensor::sample() { if (!autorange_) { - int raw = adc1_get_raw(channel_); + int32_t raw = -1; + if (channel1_ != ADC1_CHANNEL_MAX) { + raw = adc1_get_raw(channel1_); + } else if (channel2_ != ADC2_CHANNEL_MAX) { + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); + } + if (raw == -1) { return NAN; } if (output_raw_) { return raw; } - uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]); + uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]); return mv / 1000.0f; } - int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); - raw11 = adc1_get_raw(channel_); - if (raw11 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); - raw6 = adc1_get_raw(channel_); - if (raw6 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); - raw2 = adc1_get_raw(channel_); - if (raw2 < ADC_MAX) { - adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); - raw0 = adc1_get_raw(channel_); + int32_t raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; + + if (channel1_ != ADC1_CHANNEL_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11); + raw11 = adc1_get_raw(channel1_); + if (raw11 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6); + raw6 = adc1_get_raw(channel1_); + if (raw6 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5); + raw2 = adc1_get_raw(channel1_); + if (raw2 < ADC_MAX) { + adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0); + raw0 = adc1_get_raw(channel1_); + } + } + } + } else if (channel2_ != ADC2_CHANNEL_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_11); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw11); + if (raw11 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_6); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); + if (raw6 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_2_5); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); + if (raw2 < ADC_MAX) { + adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_0); + adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); + } } } } @@ -181,10 +212,10 @@ float ADCSensor::sample() { return NAN; } - uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]); - uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]); - uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); - uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); + uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_11]); + uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); + uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); + uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) uint32_t c11 = std::min(raw11, ADC_HALF); @@ -212,7 +243,7 @@ float ADCSensor::sample() { adc_select_input(pin - 26); } - int raw = adc_read(); + int32_t raw = adc_read(); if (this->is_temperature_) { adc_set_temp_sensor_enabled(false); } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 22cddde6f8..a905177790 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; } - void set_channel(adc1_channel_t channel) { channel_ = channel; } + void set_channel1(adc1_channel_t channel) { + channel1_ = channel; + channel2_ = ADC2_CHANNEL_MAX; + } + void set_channel2(adc2_channel_t channel) { + channel2_ = channel; + channel1_ = ADC1_CHANNEL_MAX; + } void set_autorange(bool autorange) { autorange_ = autorange; } #endif - /// Update adc values. + /// Update ADC values void update() override; - /// Setup ADc + /// Setup ADC void setup() override; void dump_config() override; - /// `HARDWARE_LATE` setup priority. + /// `HARDWARE_LATE` setup priority float get_setup_priority() const override; void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_output_raw(bool output_raw) { output_raw_ = output_raw; } @@ -52,9 +59,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #ifdef USE_ESP32 adc_atten_t attenuation_{ADC_ATTEN_DB_0}; - adc1_channel_t channel_{}; + adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; + adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; - esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {}; + esp_adc_cal_characteristics_t cal_characteristics_[(int32_t) ADC_ATTEN_MAX] = {}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 4695e96570..a0eda1d659 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.core import CORE from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import get_esp32_variant from esphome.const import ( @@ -8,15 +10,15 @@ from esphome.const import ( CONF_NUMBER, CONF_PIN, CONF_RAW, + CONF_WIFI, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) -from esphome.core import CORE - from . import ( ATTENUATION_MODES, ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, + ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, validate_adc_pin, ) @@ -25,7 +27,23 @@ AUTO_LOAD = ["voltage_sampler"] def validate_config(config): if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": - raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.") + raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") + + return config + + +def final_validate_config(config): + if CORE.is_esp32: + variant = get_esp32_variant() + if ( + CONF_WIFI in fv.full_config.get() + and config[CONF_PIN][CONF_NUMBER] + in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + raise cv.Invalid( + f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" + ) + return config @@ -55,6 +73,8 @@ CONFIG_SCHEMA = cv.All( validate_config, ) +FINAL_VALIDATE_SCHEMA = final_validate_config + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -81,5 +101,15 @@ async def to_code(config): if CORE.is_esp32: variant = get_esp32_variant() pin_num = config[CONF_PIN][CONF_NUMBER] - chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] - cg.add(var.set_channel(chan)) + if ( + variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel1(chan)) + elif ( + variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL + and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] + ): + chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] + cg.add(var.set_channel2(chan)) From 74e062fdb30bacd6cfc872d0a2ef0fc277e80e79 Mon Sep 17 00:00:00 2001 From: Ilia Sotnikov Date: Sun, 16 Jul 2023 23:28:31 +0300 Subject: [PATCH 024/270] [Sprinkler] Resume fixes (#5100) --- esphome/components/sprinkler/sprinkler.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 095884997c..8afafcb5ce 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -954,10 +954,18 @@ void Sprinkler::pause() { } void Sprinkler::resume() { + if (this->standby()) { + ESP_LOGD(TAG, "resume called but standby is enabled; no action taken"); + return; + } + if (this->paused_valve_.has_value() && (this->resume_duration_.has_value())) { - ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), - this->resume_duration_.value_or(0)); - this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + // Resume only if valve has not been completed yet + if (!this->valve_cycle_complete_(this->paused_valve_.value())) { + ESP_LOGD(TAG, "Resuming valve %u with %u seconds remaining", this->paused_valve_.value_or(0), + this->resume_duration_.value_or(0)); + this->fsm_request_(this->paused_valve_.value(), this->resume_duration_.value()); + } this->reset_resume(); } else { ESP_LOGD(TAG, "No valve to resume!"); From d57a5d1793a84a9351c0003668da5d0fb0e12060 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:11:43 +1200 Subject: [PATCH 025/270] Remove template switch restore_state (#5106) --- esphome/components/template/switch/__init__.py | 5 +++-- esphome/components/template/switch/template_switch.cpp | 5 ----- esphome/components/template/switch/template_switch.h | 2 -- tests/test1.yaml | 2 -- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index e002c4e3d8..a221cbaa60 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -43,7 +43,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( single=True ), - cv.Optional(CONF_RESTORE_STATE, default=False): cv.boolean, + cv.Optional(CONF_RESTORE_STATE): cv.invalid( + "The restore_state option has been removed in 2023.7.0. Use the restore_mode option instead" + ), } ) .extend(cv.COMPONENT_SCHEMA), @@ -70,7 +72,6 @@ async def to_code(config): ) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) - cg.add(var.set_restore_state(config[CONF_RESTORE_STATE])) @automation.register_action( diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5db346b99f..b2a221669e 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -40,9 +40,6 @@ float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWA Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { - if (!this->restore_state_) - return; - optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { @@ -57,10 +54,8 @@ void TemplateSwitch::setup() { } void TemplateSwitch::dump_config() { LOG_SWITCH("", "Template Switch", this); - ESP_LOGCONFIG(TAG, " Restore State: %s", YESNO(this->restore_state_)); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); } -void TemplateSwitch::set_restore_state(bool restore_state) { this->restore_state_ = restore_state; } void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index ef9b567451..bfe9ac25d6 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,7 +15,6 @@ class TemplateSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - void set_restore_state(bool restore_state); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -35,7 +34,6 @@ class TemplateSwitch : public switch_::Switch, public Component { Trigger<> *turn_on_trigger_; Trigger<> *turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; - bool restore_state_{false}; }; } // namespace template_ diff --git a/tests/test1.yaml b/tests/test1.yaml index d0c9801933..bf099e2844 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2475,7 +2475,6 @@ switch: level: !lambda "return 0.5;" turn_off_action: - switch.turn_on: living_room_lights_off - restore_state: false on_turn_on: - switch.template.publish: id: livingroom_lights @@ -2511,7 +2510,6 @@ switch: } optimistic: true assumed_state: false - restore_state: true on_turn_off: - switch.template.publish: id: my_switch From c4b906574972b279c0f11023d648dead71306a0a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 17 Jul 2023 07:17:31 +1000 Subject: [PATCH 026/270] Add timeout filter (#5104) --- esphome/components/sensor/__init__.py | 10 ++++++++++ esphome/components/sensor/filter.cpp | 11 +++++++++++ esphome/components/sensor/filter.h | 12 ++++++++++++ tests/test3.1.yaml | 1 + 4 files changed, 34 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 06b96171a7..caaffd9701 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -217,6 +217,7 @@ OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -536,6 +537,15 @@ async def heartbeat_filter_to_code(config, filter_id): return var +@FILTER_REGISTRY.register( + "timeout", TimeoutFilter, cv.positive_time_period_milliseconds +) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id, config) + await cg.register_component(var, {}) + return var + + @FILTER_REGISTRY.register( "debounce", DebounceFilter, cv.positive_time_period_milliseconds ) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 472649ebdc..ccefa556b6 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -373,6 +373,17 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } +// TimeoutFilter +optional TimeoutFilter::new_value(float value) { + this->set_timeout("timeout", this->time_period_, [this]() { this->output(NAN); }); + this->output(value); + + return {}; +} + +TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} +float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } + // DebounceFilter optional DebounceFilter::new_value(float value) { this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 05934a26e8..296990f34f 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -313,6 +313,18 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +class TimeoutFilter : public Filter, public Component { + public: + explicit TimeoutFilter(uint32_t time_period); + + optional new_value(float value) override; + + float get_setup_priority() const override; + + protected: + uint32_t time_period_; +}; + class DebounceFilter : public Filter, public Component { public: explicit DebounceFilter(uint32_t time_period); diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 5f1d3ff28f..104f4bbda8 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -86,6 +86,7 @@ sensor: - delta: 100 - throttle: 100ms - debounce: 500s + - timeout: 10min - calibrate_linear: - 0 -> 0 - 100 -> 100 From 68affce274d34eb05aa508f9b6c359101d55ec37 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:29:32 +1200 Subject: [PATCH 027/270] Bump version to 2023.7.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e74f23e9b4..8e0efaca09 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0b1" +__version__ = "2023.7.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 553132443fbf777664fb0959301d8788ad26e8c0 Mon Sep 17 00:00:00 2001 From: bwynants Date: Mon, 17 Jul 2023 00:42:49 +0200 Subject: [PATCH 028/270] P1 values for capacity tariff in Belgium (#5081) --- esphome/components/dsmr/__init__.py | 2 +- esphome/components/dsmr/sensor.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index d3d20ca2a7..9f56dc3465 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -87,7 +87,7 @@ async def to_code(config): cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.7") + cg.add_library("glmnet/Dsmr", "0.8") # Crypto cg.add_library("rweather/Crypto", "0.4.0") diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 2e2050ecab..f2398d1908 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -243,6 +243,30 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_WATER, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional( + "active_energy_import_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_running_month" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_last_13_months" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(cv.COMPONENT_SCHEMA) From f840eee1b7633333d3a23abc457c5cc38a419cef Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 16 Jul 2023 15:43:57 -0700 Subject: [PATCH 029/270] airthings_wave: Silence compiler warnings (#5098) --- .../components/airthings_wave_base/airthings_wave_base.cpp | 2 +- .../components/airthings_wave_plus/airthings_wave_plus.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index eff466f413..16789ff454 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -76,7 +76,7 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt } } -bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } +bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; } void AirthingsWaveBase::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index e44d5fbcaa..a32128e992 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -51,9 +51,9 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { this->response_received_(); } -bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } +bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return radon <= 16383; } -bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } +bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; } void AirthingsWavePlus::dump_config() { // these really don't belong here, but there doesn't seem to be a From 036e14ab7fb72bb25be026f9c9099aaf3ac95856 Mon Sep 17 00:00:00 2001 From: PlainTechEnthusiast <135363826+PlainTechEnthusiast@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:49:04 -0400 Subject: [PATCH 030/270] Sigma delta fix (#4911) --- .../sigma_delta_output/sigma_delta_output.cpp | 57 +++++++++++++++++++ .../sigma_delta_output/sigma_delta_output.h | 31 ++-------- 2 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 esphome/components/sigma_delta_output/sigma_delta_output.cpp diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.cpp b/esphome/components/sigma_delta_output/sigma_delta_output.cpp new file mode 100644 index 0000000000..d386f8db1a --- /dev/null +++ b/esphome/components/sigma_delta_output/sigma_delta_output.cpp @@ -0,0 +1,57 @@ +#include "sigma_delta_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sigma_delta_output { + +static const char *const TAG = "output.sigma_delta"; + +void SigmaDeltaOutput::setup() { + if (this->pin_) + this->pin_->setup(); +} + +void SigmaDeltaOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Sigma Delta Output:"); + LOG_PIN(" Pin: ", this->pin_); + if (this->state_change_trigger_) { + ESP_LOGCONFIG(TAG, " State change automation configured"); + } + if (this->turn_on_trigger_) { + ESP_LOGCONFIG(TAG, " Turn on automation configured"); + } + if (this->turn_off_trigger_) { + ESP_LOGCONFIG(TAG, " Turn off automation configured"); + } + LOG_UPDATE_INTERVAL(this); + LOG_FLOAT_OUTPUT(this); +} + +void SigmaDeltaOutput::update() { + this->accum_ += this->state_; + const bool next_value = this->accum_ > 0; + + if (next_value) { + this->accum_ -= 1.; + } + + if (next_value != this->value_) { + this->value_ = next_value; + if (this->pin_) { + this->pin_->digital_write(next_value); + } + + if (this->state_change_trigger_) { + this->state_change_trigger_->trigger(next_value); + } + + if (next_value && this->turn_on_trigger_) { + this->turn_on_trigger_->trigger(); + } else if (!next_value && this->turn_off_trigger_) { + this->turn_off_trigger_->trigger(); + } + } +} + +} // namespace sigma_delta_output +} // namespace esphome diff --git a/esphome/components/sigma_delta_output/sigma_delta_output.h b/esphome/components/sigma_delta_output/sigma_delta_output.h index 5a5acd2dfb..8fd1e1f761 100644 --- a/esphome/components/sigma_delta_output/sigma_delta_output.h +++ b/esphome/components/sigma_delta_output/sigma_delta_output.h @@ -1,9 +1,12 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" namespace esphome { namespace sigma_delta_output { + class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { public: Trigger<> *get_turn_on_trigger() { @@ -25,31 +28,9 @@ class SigmaDeltaOutput : public PollingComponent, public output::FloatOutput { void set_pin(GPIOPin *pin) { this->pin_ = pin; }; void write_state(float state) override { this->state_ = state; } - void update() override { - this->accum_ += this->state_; - const bool next_value = this->accum_ > 0; - - if (next_value) { - this->accum_ -= 1.; - } - - if (next_value != this->value_) { - this->value_ = next_value; - if (this->pin_) { - this->pin_->digital_write(next_value); - } - - if (this->state_change_trigger_) { - this->state_change_trigger_->trigger(next_value); - } - - if (next_value && this->turn_on_trigger_) { - this->turn_on_trigger_->trigger(); - } else if (!next_value && this->turn_off_trigger_) { - this->turn_off_trigger_->trigger(); - } - } - } + void setup() override; + void dump_config() override; + void update() override; protected: GPIOPin *pin_{nullptr}; From 4449248c6f65a2ad4e70fbdc96e7f1db0cc3542c Mon Sep 17 00:00:00 2001 From: voed Date: Tue, 18 Jul 2023 03:50:32 +0300 Subject: [PATCH 031/270] [LD2410] Remove baud_rate check (#5112) --- esphome/components/ld2410/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index be39cc2979..47c4cdb0bd 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -112,7 +112,6 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "ld2410", - baud_rate=256000, require_tx=True, require_rx=True, parity="NONE", From 746488cabf2d4ac8db4b0d7883dc72dd8c5aa73c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:38:47 +1200 Subject: [PATCH 032/270] Fix silence detection flag on voice assistant (#5120) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 3 ++- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_pb2.cpp | 9 +++++++++ esphome/components/api/api_pb2.h | 1 + esphome/components/api/api_server.cpp | 6 +++--- esphome/components/api/api_server.h | 2 +- esphome/components/voice_assistant/voice_assistant.cpp | 2 +- esphome/components/voice_assistant/voice_assistant.h | 6 +----- 9 files changed, 20 insertions(+), 12 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 0d68d9fe55..86685aa5e6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1420,6 +1420,7 @@ message VoiceAssistantRequest { bool start = 1; string conversation_id = 2; + bool use_vad = 3; } message VoiceAssistantResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 858ff0e525..a46efd80e5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -907,12 +907,13 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) { +bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad) { if (!this->voice_assistant_subscription_) return false; VoiceAssistantRequest msg; msg.start = start; msg.conversation_id = conversation_id; + msg.use_vad = use_vad; return this->send_voice_assistant_request(msg); } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c146adff02..acc4578661 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -124,7 +124,7 @@ class APIConnection : public APIServerConnection { void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { this->voice_assistant_subscription_ = msg.subscribe; } - bool request_voice_assistant(bool start, const std::string &conversation_id); + bool request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad); void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 8c7f6d0c4a..3a2d980e57 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6348,6 +6348,10 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->start = value.as_bool(); return true; } + case 3: { + this->use_vad = value.as_bool(); + return true; + } default: return false; } @@ -6365,6 +6369,7 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); + buffer.encode_bool(3, this->use_vad); } #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { @@ -6377,6 +6382,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" conversation_id: "); out.append("'").append(this->conversation_id).append("'"); out.append("\n"); + + out.append(" use_vad: "); + out.append(YESNO(this->use_vad)); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 769f7aaff5..627165953d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1655,6 +1655,7 @@ class VoiceAssistantRequest : public ProtoMessage { public: bool start{false}; std::string conversation_id{}; + bool use_vad{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 87b5f9e63f..f70d45ecd0 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -323,16 +323,16 @@ void APIServer::on_shutdown() { } #ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant(const std::string &conversation_id) { +bool APIServer::start_voice_assistant(const std::string &conversation_id, bool use_vad) { for (auto &c : this->clients_) { - if (c->request_voice_assistant(true, conversation_id)) + if (c->request_voice_assistant(true, conversation_id, use_vad)) return true; } return false; } void APIServer::stop_voice_assistant() { for (auto &c : this->clients_) { - if (c->request_voice_assistant(false, "")) + if (c->request_voice_assistant(false, "", false)) return; } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index be124f42ff..9b40a5ef02 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -81,7 +81,7 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(const std::string &conversation_id); + bool start_voice_assistant(const std::string &conversation_id, bool use_vad); void stop_voice_assistant(); #endif diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 44d640ff39..217ddb6354 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -130,7 +130,7 @@ void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { void VoiceAssistant::request_start(bool continuous) { ESP_LOGD(TAG, "Requesting start..."); - if (!api::global_api_server->start_voice_assistant(this->conversation_id_)) { + if (!api::global_api_server->start_voice_assistant(this->conversation_id_, this->silence_detection_)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); this->continuous_ = false; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index b103584509..e67baaee65 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -25,10 +25,9 @@ namespace voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support -// Version 3: Adds continuous support +// Version 3: Unused/skip static const uint32_t INITIAL_VERSION = 1; static const uint32_t SPEAKER_SUPPORT = 2; -static const uint32_t SILENCE_DETECTION_SUPPORT = 3; class VoiceAssistant : public Component { public: @@ -48,9 +47,6 @@ class VoiceAssistant : public Component { uint32_t get_version() const { #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { - if (this->silence_detection_) { - return SILENCE_DETECTION_SUPPORT; - } return SPEAKER_SUPPORT; } #endif From f4a4956dd40f43e89a1aea3e8ecc64cb2558a622 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:41:24 +1200 Subject: [PATCH 033/270] Bump version to 2023.7.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8e0efaca09..8dd947b26b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0b2" +__version__ = "2023.7.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From f5e98eb86f584c455851d9bfc5ce3e470c39973b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:59:51 +1200 Subject: [PATCH 034/270] Bump version to 2023.7.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8dd947b26b..f04e19c359 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0b3" +__version__ = "2023.7.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2a7aa2fc0dc940b85738c3e2d8f57f9e507b85c4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Jul 2023 14:07:42 +1200 Subject: [PATCH 035/270] bump pyyaml to 6.0.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 618fc94e0b..74c15c9213 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ voluptuous==0.13.1 -PyYAML==6.0 +PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 tornado==6.3.2 From 73db164fb13807ce8bbf0431035041fc293dede7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Wed, 19 Jul 2023 22:39:35 +0200 Subject: [PATCH 036/270] Dashboard: use Popen() on Windows (#5110) --- esphome/dashboard/dashboard.py | 60 +++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index dd800f534c..a3a44de9ed 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -25,6 +25,7 @@ import tornado.ioloop import tornado.iostream import tornado.netutil import tornado.process +import tornado.queues import tornado.web import tornado.websocket import yaml @@ -202,7 +203,11 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): super().__init__(application, request, **kwargs) self._proc = None + self._queue = None self._is_closed = False + # Windows doesn't support non-blocking pipes, + # use Popen() with a reading thread instead + self._use_popen = os.name == "nt" @authenticated def on_message(self, message): @@ -224,13 +229,28 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): return command = self.build_command(json_message) _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) - self._proc = tornado.process.Subprocess( - command, - stdout=tornado.process.Subprocess.STREAM, - stderr=subprocess.STDOUT, - stdin=tornado.process.Subprocess.STREAM, - ) - self._proc.set_exit_callback(self._proc_on_exit) + + if self._use_popen: + self._queue = tornado.queues.Queue() + # pylint: disable=consider-using-with + self._proc = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout_thread = threading.Thread(target=self._stdout_thread) + stdout_thread.daemon = True + stdout_thread.start() + else: + self._proc = tornado.process.Subprocess( + command, + stdout=tornado.process.Subprocess.STREAM, + stderr=subprocess.STDOUT, + stdin=tornado.process.Subprocess.STREAM, + ) + self._proc.set_exit_callback(self._proc_on_exit) + tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) @property @@ -252,7 +272,13 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): while True: try: - data = yield self._proc.stdout.read_until_regex(reg) + if self._use_popen: + data = yield self._queue.get() + if data is None: + self._proc_on_exit(self._proc.poll()) + break + else: + data = yield self._proc.stdout.read_until_regex(reg) except tornado.iostream.StreamClosedError: break data = codecs.decode(data, "utf8", "replace") @@ -260,6 +286,19 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): _LOGGER.debug("> stdout: %s", data) self.write_message({"event": "line", "data": data}) + def _stdout_thread(self): + if not self._use_popen: + return + while True: + data = self._proc.stdout.readline() + if data: + data = data.replace(b"\r", b"") + self._queue.put_nowait(data) + if self._proc.poll() is not None: + break + self._proc.wait(1.0) + self._queue.put_nowait(None) + def _proc_on_exit(self, returncode): if not self._is_closed: # Check if the proc was not forcibly closed @@ -270,7 +309,10 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # Check if proc exists (if 'start' has been run) if self.is_process_active: _LOGGER.debug("Terminating process") - self._proc.proc.terminate() + if self._use_popen: + self._proc.terminate() + else: + self._proc.proc.terminate() # Shutdown proc on WS close self._is_closed = True From 3843d21dbf4d2869b1a5fcb6d5c0bb8952f931fc Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 30 Jul 2023 16:19:06 -0500 Subject: [PATCH 037/270] Swap ADC back to use 'int' because C3 (#5151) --- esphome/components/adc/adc_sensor.cpp | 10 +++++----- esphome/components/adc/adc_sensor.h | 2 +- tests/test7.yaml | 8 ++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index bb6a7a8c85..665ecfd6b5 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -32,8 +32,8 @@ static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; #endif #endif -static const int32_t ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) -static const int32_t ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) +static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit) +static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit) #endif #ifdef USE_RP2040 @@ -59,7 +59,7 @@ extern "C" } // load characteristics for each attenuation - for (int32_t i = 0; i < (int32_t) ADC_ATTEN_MAX; i++) { + for (int32_t i = 0; i <= ADC_ATTEN_DB_11; i++) { auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, 1100, // default vref @@ -157,7 +157,7 @@ float ADCSensor::sample() { #ifdef USE_ESP32 float ADCSensor::sample() { if (!autorange_) { - int32_t raw = -1; + int raw = -1; if (channel1_ != ADC1_CHANNEL_MAX) { raw = adc1_get_raw(channel1_); } else if (channel2_ != ADC2_CHANNEL_MAX) { @@ -174,7 +174,7 @@ float ADCSensor::sample() { return mv / 1000.0f; } - int32_t raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; + int raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; if (channel1_ != ADC1_CHANNEL_MAX) { adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11); diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index a905177790..7d9c8959da 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -62,7 +62,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; - esp_adc_cal_characteristics_t cal_characteristics_[(int32_t) ADC_ATTEN_MAX] = {}; + esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; #endif }; diff --git a/tests/test7.yaml b/tests/test7.yaml index 10e1b035ab..8d48c9a601 100644 --- a/tests/test7.yaml +++ b/tests/test7.yaml @@ -31,3 +31,11 @@ logger: http_request: useragent: esphome/tagreader timeout: 10s + +sensor: + - platform: adc + id: adc_sensor_p4 + name: ADC pin 4 + pin: 4 + attenuation: 11db + update_interval: 1s From 9b19c45735d6135e5dd4b968429f665a562a9a13 Mon Sep 17 00:00:00 2001 From: Stijn Tintel Date: Mon, 31 Jul 2023 00:23:30 +0300 Subject: [PATCH 038/270] wifi: handle WIFI_REASON_ROAMING reason in event (#5153) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 744fc755fe..086a80cad0 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -569,6 +569,8 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_ROAMING: + return "Station Roaming"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -631,7 +633,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; - + } else if (it.reason == WIFI_REASON_ROAMING) { + ESP_LOGI(TAG, "Event: Disconnected ssid='%s' reason='Station Roaming'", buf); + return; } else { ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); From 91e920c498dfbd15a94b56ca44a431a337b194ef Mon Sep 17 00:00:00 2001 From: cvwillegen Date: Sun, 30 Jul 2023 23:32:09 +0200 Subject: [PATCH 039/270] Slightly lower template switch setup priority (#5163) --- esphome/components/template/switch/template_switch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index b2a221669e..fa236f6364 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -36,7 +36,7 @@ void TemplateSwitch::write_state(bool state) { void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } void TemplateSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } -float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } +float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { From 2a12ec09fb0d93bdf46271f1bda0a7aa85662ec7 Mon Sep 17 00:00:00 2001 From: PlainTechEnthusiast <135363826+PlainTechEnthusiast@users.noreply.github.com> Date: Sun, 30 Jul 2023 17:40:55 -0400 Subject: [PATCH 040/270] update "Can't convert" warning to match others in homeassistant_sensor (#5162) --- .../components/homeassistant/sensor/homeassistant_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index f5e73c8854..35e660f7c1 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -12,7 +12,7 @@ void HomeassistantSensor::setup() { this->entity_id_, this->attribute_, [this](const std::string &state) { auto val = parse_number(state); if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); this->publish_state(NAN); return; } From dec044ad8b4083219015dff939304ed7fc348a5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 15:23:52 -0700 Subject: [PATCH 041/270] Increase maximum number of BLE notifications (#5155) --- esphome/components/esp32_ble_tracker/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 30589f1a3f..8ba77c7db7 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -263,6 +263,7 @@ async def to_code(config): # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") From c63cdae84fb1dc348bcdecfaa05f428c0ddbccf8 Mon Sep 17 00:00:00 2001 From: Joris S <100357138+Jorre05@users.noreply.github.com> Date: Mon, 31 Jul 2023 00:30:21 +0200 Subject: [PATCH 042/270] invert min_rssi check (#5150) --- esphome/components/ble_presence/binary_sensor.py | 2 +- esphome/components/ble_presence/ble_presence_device.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index d54b7678e1..75366ce864 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, cv.Optional(CONF_IBEACON_UUID): cv.uuid, cv.Optional(CONF_MIN_RSSI): cv.All( - cv.decibel, cv.int_range(min=-90, max=-30) + cv.decibel, cv.int_range(min=-100, max=-30) ), } ) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 953ea460a8..1be9adeb30 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -51,7 +51,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (this->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) { + if (this->check_minimum_rssi_ && this->minimum_rssi_ > device.get_rssi()) { return false; } switch (this->match_by_) { From b3d5a4dfdba90301ef24aea3f20d769c5008a795 Mon Sep 17 00:00:00 2001 From: Maxime Michel Date: Tue, 1 Aug 2023 02:03:34 +0200 Subject: [PATCH 043/270] Fix graininess & streaks for 7.50inV2alt Waveshare e-paper (#5168) --- .../components/waveshare_epaper/waveshare_epaper.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index d64a5500dd..d17f8230de 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1492,11 +1492,10 @@ void WaveshareEPaper7P5InV2alt::initialize() { this->command(0x01); // 1-0=11: internal power - this->data(0x17); - + this->data(0x07); this->data(0x17); // VGH&VGL this->data(0x3F); // VSH - this->data(0x3F); // VSL + this->data(0x26); // VSL this->data(0x11); // VSHR // VCOM DC Setting @@ -1510,10 +1509,6 @@ void WaveshareEPaper7P5InV2alt::initialize() { 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); @@ -1535,7 +1530,7 @@ void WaveshareEPaper7P5InV2alt::initialize() { // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x10); - this->data(0x07); + this->data(0x00); // COMMAND TCON SETTING this->command(0x60); this->data(0x22); From 956e19be7de7fd73fc16f30d123414fc92eee91c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 1 Aug 2023 12:08:36 +1200 Subject: [PATCH 044/270] Bump version to 2023.7.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f04e19c359..139555f19b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.7.0" +__version__ = "2023.7.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 02ed2c0ebeaa975e1d869b05f1b0fd669e02e5b7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:30:26 +1200 Subject: [PATCH 045/270] Bump version to 2023.8.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4977726361..6b442dd633 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0-dev" +__version__ = "2023.8.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 72e72d7d4b4556876817a0e4af5eef6536eb1c89 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:40:13 +1200 Subject: [PATCH 046/270] Fix duplicate --- esphome/components/sensor/__init__.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index c8cd2f9f7f..8f7d581b2d 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -560,15 +560,6 @@ async def timeout_filter_to_code(config, filter_id): return var -@FILTER_REGISTRY.register( - "timeout", TimeoutFilter, cv.positive_time_period_milliseconds -) -async def timeout_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config) - await cg.register_component(var, {}) - return var - - @FILTER_REGISTRY.register( "debounce", DebounceFilter, cv.positive_time_period_milliseconds ) From 44a917929d5d3af8d803c8363e5031f56be42b7c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:20:58 +1200 Subject: [PATCH 047/270] Read string of bool env and match against well known values (#5232) --- esphome/helpers.py | 9 ++++++++- tests/unit_tests/test_helpers.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index fd8893ad99..4012b2067f 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -144,7 +144,14 @@ def resolve_ip_address(host): def get_bool_env(var, default=False): - return bool(os.getenv(var, default)) + value = os.getenv(var, default) + if isinstance(value, str): + value = value.lower() + if value in ["1", "true"]: + return True + if value in ["0", "false"]: + return False + return bool(value) def get_str_env(var, default=None): diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index b98838024f..67fabd7af8 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -108,6 +108,10 @@ def test_is_ip_address__valid(value): ("FOO", None, False, False), ("FOO", None, True, True), ("FOO", "", False, False), + ("FOO", "False", False, False), + ("FOO", "True", False, True), + ("FOO", "FALSE", True, False), + ("FOO", "fAlSe", True, False), ("FOO", "Yes", False, True), ("FOO", "123", False, True), ), From 2fa79a2e2f606b42b33dccc49dae72e97a221a6b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 10 Aug 2023 21:21:24 -0700 Subject: [PATCH 048/270] fix aeha data template (#5231) Co-authored-by: Samuel Sieb --- esphome/components/remote_base/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 0666b96d1e..24993e84d3 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1569,4 +1569,7 @@ def aeha_dumper(var, config): async def aeha_action(var, config, args): template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) cg.add(var.set_address(template_)) - cg.add(var.set_data(config[CONF_DATA])) + template_ = await cg.templatable( + config[CONF_DATA], args, cg.std_vector.template(cg.uint8) + ) + cg.add(var.set_data(template_)) From 351e7ea16b0a9af8d126b9019294f53c55d767c9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:21:44 +1200 Subject: [PATCH 049/270] Expose start to speaker interface (#5228) --- esphome/components/i2s_audio/speaker/i2s_audio_speaker.h | 2 +- esphome/components/speaker/speaker.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index f2e83142b3..b075722e1b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -51,7 +51,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud #endif void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } - void start(); + void start() override; void stop() override; size_t play(const uint8_t *data, size_t length) override; diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 53f97da5ac..3f520e3c5e 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -13,8 +13,9 @@ enum State : uint8_t { class Speaker { public: virtual size_t play(const uint8_t *data, size_t length) = 0; - virtual size_t play(const std::vector &data) { return this->play(data.data(), data.size()); } + size_t play(const std::vector &data) { return this->play(data.data(), data.size()); } + virtual void start() = 0; virtual void stop() = 0; bool is_running() const { return this->state_ == STATE_RUNNING; } From 99a765dc0639f559e1a8fcdb9447869f7df78313 Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Fri, 11 Aug 2023 07:51:53 +0200 Subject: [PATCH 050/270] New features added for Haier integration (#5196) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/haier/climate.py | 74 ++++++- esphome/components/haier/haier_base.cpp | 75 +++++-- esphome/components/haier/haier_base.h | 43 ++-- esphome/components/haier/hon_climate.cpp | 144 ++++++------- esphome/components/haier/hon_climate.h | 5 +- esphome/components/haier/hon_packet.h | 16 +- .../components/haier/smartair2_climate.cpp | 204 +++++++++++++----- esphome/components/haier/smartair2_climate.h | 13 +- esphome/components/haier/smartair2_packet.h | 7 +- platformio.ini | 2 +- 10 files changed, 394 insertions(+), 189 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index c518282bfa..fec39d2967 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -14,7 +14,10 @@ from esphome.const import ( CONF_MIN_TEMPERATURE, CONF_PROTOCOL, CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_SWING_MODES, + CONF_TARGET_TEMPERATURE, + CONF_TEMPERATURE_STEP, CONF_VISUAL, CONF_WIFI, DEVICE_CLASS_TEMPERATURE, @@ -23,25 +26,29 @@ from esphome.const import ( UNIT_CELSIUS, ) from esphome.components.climate import ( - ClimateSwingMode, ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, ) _LOGGER = logging.getLogger(__name__) PROTOCOL_MIN_TEMPERATURE = 16.0 PROTOCOL_MAX_TEMPERATURE = 30.0 -PROTOCOL_TEMPERATURE_STEP = 1.0 +PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 +PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 CODEOWNERS = ["@paveldn"] AUTO_LOAD = ["sensor"] DEPENDENCIES = ["climate", "uart"] CONF_WIFI_SIGNAL = "wifi_signal" +CONF_ANSWER_TIMEOUT = "answer_timeout" +CONF_DISPLAY = "display" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" - PROTOCOL_HON = "HON" PROTOCOL_SMARTAIR2 = "SMARTAIR2" PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2] @@ -89,6 +96,17 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = { "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, } +SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, +} + +SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + def validate_visual(config): if CONF_VISUAL in config: @@ -109,10 +127,29 @@ def validate_visual(config): ) else: config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE + if CONF_TEMPERATURE_STEP in visual_config: + temp_step = config[CONF_VISUAL][CONF_TEMPERATURE_STEP][ + CONF_TARGET_TEMPERATURE + ] + if ((int)(temp_step * 2)) / 2 != temp_step: + raise cv.Invalid( + f"Configured visual temperature step {temp_step} is wrong, it should be a multiple of 0.5" + ) + else: + config[CONF_VISUAL][CONF_TEMPERATURE_STEP] = ( + { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + }, + ) else: config[CONF_VISUAL] = { CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE, CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE, + CONF_TEMPERATURE_STEP: { + CONF_TARGET_TEMPERATURE: PROTOCOL_TARGET_TEMPERATURE_STEP, + CONF_CURRENT_TEMPERATURE: PROTOCOL_CURRENT_TEMPERATURE_STEP, + }, } return config @@ -132,6 +169,11 @@ BASE_CONFIG_SCHEMA = ( "BOTH", ], ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, + cv.Optional(CONF_DISPLAY): cv.boolean, + cv.Optional( + CONF_ANSWER_TIMEOUT, + ): cv.positive_time_period_milliseconds, } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -144,13 +186,26 @@ CONFIG_SCHEMA = cv.All( PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Smartair2Climate), + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=list( + SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys() + ), + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) + ), } ), PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(HonClimate), - cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean, cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional( + CONF_SUPPORTED_PRESETS, + default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), + ): cv.ensure_list( + cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) + ), cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, @@ -354,10 +409,11 @@ async def to_code(config): await uart.register_uart_device(var, config) await climate.register_climate(var, config) - if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]): - cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) if CONF_BEEPER in config: cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_DISPLAY in config: + cg.add(var.set_display_state(config[CONF_DISPLAY])) if CONF_OUTDOOR_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) cg.add(var.set_outdoor_temperature_sensor(sens)) @@ -365,5 +421,9 @@ async def to_code(config): cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) if CONF_SUPPORTED_SWING_MODES in config: cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_ANSWER_TIMEOUT in config: + cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) # https://github.com/paveldn/HaierProtocol - cg.add_library("pavlodn/HaierProtocol", "0.9.18") + cg.add_library("pavlodn/HaierProtocol", "0.9.20") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index d9349cb8fe..5faee5207b 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -2,6 +2,9 @@ #include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif #include "haier_base.h" using namespace esphome::climate; @@ -24,14 +27,15 @@ constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command suppli const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { static const char *phase_names[] = { "SENDING_INIT_1", - "WAITING_ANSWER_INIT_1", + "WAITING_INIT_1_ANSWER", "SENDING_INIT_2", - "WAITING_ANSWER_INIT_2", + "WAITING_INIT_2_ANSWER", "SENDING_FIRST_STATUS_REQUEST", "WAITING_FIRST_STATUS_ANSWER", "SENDING_ALARM_STATUS_REQUEST", "WAITING_ALARM_STATUS_ANSWER", "IDLE", + "UNKNOWN", "SENDING_STATUS_REQUEST", "WAITING_STATUS_ANSWER", "SENDING_UPDATE_SIGNAL_REQUEST", @@ -63,7 +67,8 @@ HaierClimateBase::HaierClimateBase() forced_publish_(false), forced_request_status_(false), first_control_attempt_(false), - reset_protocol_request_(false) { + reset_protocol_request_(false), + send_wifi_signal_(true) { this->traits_ = climate::ClimateTraits(); this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, @@ -77,7 +82,7 @@ HaierClimateBase::HaierClimateBase() HaierClimateBase::~HaierClimateBase() {} -void HaierClimateBase::set_phase_(ProtocolPhases phase) { +void HaierClimateBase::set_phase(ProtocolPhases phase) { if (this->protocol_phase_ != phase) { #if (HAIER_LOG_LEVEL > 4) ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); @@ -109,10 +114,27 @@ bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); } -bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) { +bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); } +#ifdef USE_WIFI +haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { + static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; + if (wifi::global_wifi_component->is_connected()) { + wifi_status_data[1] = 0; + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); + ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); + } else { + ESP_LOGD(TAG, "WiFi is not connected"); + wifi_status_data[1] = 1; + wifi_status_data[3] = 0; + } + return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); +} +#endif + bool HaierClimateBase::get_display_state() const { return this->display_status_; } void HaierClimateBase::set_display_state(bool state) { @@ -136,10 +158,15 @@ void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionR void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } + void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { this->traits_.set_supported_swing_modes(modes); - this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available - this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available + if (!modes.empty()) + this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); +} + +void HaierClimateBase::set_answer_timeout(uint32_t timeout) { + this->answer_timeout_ = std::chrono::milliseconds(timeout); } void HaierClimateBase::set_supported_modes(const std::set &modes) { @@ -148,6 +175,14 @@ void HaierClimateBase::set_supported_modes(const std::set this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available } +void HaierClimateBase::set_supported_presets(const std::set &presets) { + this->traits_.set_supported_presets(presets); + if (!presets.empty()) + this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); +} + +void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } + haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, uint8_t answer_message_type, @@ -155,9 +190,9 @@ haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t reques ProtocolPhases expected_phase) { haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) - result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) - result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; if (is_message_invalid(answer_message_type)) @@ -172,9 +207,9 @@ haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); #endif if (this->protocol_phase_ > ProtocolPhases::IDLE) { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else { - this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_phase(ProtocolPhases::SENDING_INIT_1); } return haier_protocol::HandlerError::HANDLER_OK; } @@ -183,8 +218,8 @@ void HaierClimateBase::setup() { ESP_LOGI(TAG, "Haier initialization..."); // Set timestamp here to give AC time to boot this->last_request_timestamp_ = std::chrono::steady_clock::now(); - this->set_phase_(ProtocolPhases::SENDING_INIT_1); - this->set_answers_handlers(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + this->set_handlers(); this->haier_protocol_.set_default_timeout_handler( std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); } @@ -212,7 +247,7 @@ void HaierClimateBase::loop() { this->set_force_send_control_(false); if (this->hvac_settings_.valid) this->hvac_settings_.reset(); - this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_phase(ProtocolPhases::SENDING_INIT_1); return; } else { // No need to reset protocol if we didn't pass initialization phase @@ -229,7 +264,7 @@ void HaierClimateBase::loop() { this->process_pending_action(); } else if (this->hvac_settings_.valid || this->force_send_control_) { ESP_LOGV(TAG, "Control packet is pending..."); - this->set_phase_(ProtocolPhases::SENDING_CONTROL); + this->set_phase(ProtocolPhases::SENDING_CONTROL); } } this->process_phase(now); @@ -243,10 +278,10 @@ void HaierClimateBase::process_pending_action() { } switch (request) { case ActionRequest::TURN_POWER_ON: - this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND); + this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); break; case ActionRequest::TURN_POWER_OFF: - this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND); + this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); break; case ActionRequest::TOGGLE_POWER: case ActionRequest::NO_ACTION: @@ -303,7 +338,11 @@ void HaierClimateBase::set_force_send_control_(bool status) { } void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { - this->haier_protocol_.send_message(command, use_crc); + if (this->answer_timeout_.has_value()) { + this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value()); + } else { + this->haier_protocol_.send_message(command, use_crc); + } this->last_request_timestamp_ = std::chrono::steady_clock::now(); } diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index 046b59af96..b2446d6fb5 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -44,6 +44,7 @@ class HaierClimateBase : public esphome::Component, void reset_protocol() { this->reset_protocol_request_ = true; }; void set_supported_modes(const std::set &modes); void set_supported_swing_modes(const std::set &modes); + void set_supported_presets(const std::set &presets); size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; @@ -52,39 +53,41 @@ class HaierClimateBase : public esphome::Component, esphome::uart::UARTDevice::write_array(data, len); }; bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; + void set_answer_timeout(uint32_t timeout); + void set_send_wifi(bool send_wifi); protected: enum class ProtocolPhases { UNKNOWN = -1, // INITIALIZATION SENDING_INIT_1 = 0, - WAITING_ANSWER_INIT_1 = 1, + WAITING_INIT_1_ANSWER = 1, SENDING_INIT_2 = 2, - WAITING_ANSWER_INIT_2 = 3, + WAITING_INIT_2_ANSWER = 3, SENDING_FIRST_STATUS_REQUEST = 4, WAITING_FIRST_STATUS_ANSWER = 5, SENDING_ALARM_STATUS_REQUEST = 6, WAITING_ALARM_STATUS_ANSWER = 7, // FUNCTIONAL STATE IDLE = 8, - SENDING_STATUS_REQUEST = 9, - WAITING_STATUS_ANSWER = 10, - SENDING_UPDATE_SIGNAL_REQUEST = 11, - WAITING_UPDATE_SIGNAL_ANSWER = 12, - SENDING_SIGNAL_LEVEL = 13, - WAITING_SIGNAL_LEVEL_ANSWER = 14, - SENDING_CONTROL = 15, - WAITING_CONTROL_ANSWER = 16, - SENDING_POWER_ON_COMMAND = 17, - WAITING_POWER_ON_ANSWER = 18, - SENDING_POWER_OFF_COMMAND = 19, - WAITING_POWER_OFF_ANSWER = 20, + SENDING_STATUS_REQUEST = 10, + WAITING_STATUS_ANSWER = 11, + SENDING_UPDATE_SIGNAL_REQUEST = 12, + WAITING_UPDATE_SIGNAL_ANSWER = 13, + SENDING_SIGNAL_LEVEL = 14, + WAITING_SIGNAL_LEVEL_ANSWER = 15, + SENDING_CONTROL = 16, + WAITING_CONTROL_ANSWER = 17, + SENDING_POWER_ON_COMMAND = 18, + WAITING_POWER_ON_ANSWER = 19, + SENDING_POWER_OFF_COMMAND = 20, + WAITING_POWER_OFF_ANSWER = 21, NUM_PROTOCOL_PHASES }; #if (HAIER_LOG_LEVEL > 4) const char *phase_to_string_(ProtocolPhases phase); #endif - virtual void set_answers_handlers() = 0; + virtual void set_handlers() = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual haier_protocol::HaierMessage get_control_message() = 0; virtual bool is_message_invalid(uint8_t message_type) = 0; @@ -99,14 +102,17 @@ class HaierClimateBase : public esphome::Component, // Helper functions void set_force_send_control_(bool status); void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); - void set_phase_(ProtocolPhases phase); + virtual void set_phase(ProtocolPhases phase); bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, size_t timeout); bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); - bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now); + bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); +#ifdef USE_WIFI + haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); +#endif struct HvacSettings { esphome::optional mode; @@ -136,6 +142,9 @@ class HaierClimateBase : public esphome::Component, std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout std::chrono::steady_clock::time_point last_status_request_; // To request AC status std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message + optional answer_timeout_; // Message answer timeout + bool send_wifi_signal_; + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level }; } // namespace haier diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 3950b34724..feb1e019d8 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -2,9 +2,6 @@ #include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" -#ifdef USE_WIFI -#include "esphome/components/wifi/wifi_component.h" -#endif #include "hon_climate.h" #include "hon_packet.h" @@ -58,14 +55,7 @@ HonClimate::HonClimate() hvac_functions_{false, false, false, false, false}, use_crc_(hvac_functions_[2]), active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - outdoor_sensor_(nullptr), - send_wifi_signal_(true) { - this->traits_.set_supported_presets({ - climate::CLIMATE_PRESET_NONE, - climate::CLIMATE_PRESET_ECO, - climate::CLIMATE_PRESET_BOOST, - climate::CLIMATE_PRESET_SLEEP, - }); + outdoor_sensor_(nullptr) { this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; } @@ -121,17 +111,22 @@ void HonClimate::start_steri_cleaning() { } } -void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } - haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, size_t data_size) { + // Should check this before preprocess + if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " + "protocol instead of hOn"); + this->set_phase(ProtocolPhases::SENDING_INIT_1); + return haier_protocol::HandlerError::INVALID_ANSWER; + } haier_protocol::HandlerError result = this->answer_preprocess_( request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1); + (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); if (result == haier_protocol::HandlerError::HANDLER_OK) { if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { // Wrong structure - this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_phase(ProtocolPhases::SENDING_INIT_1); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } // All OK @@ -152,11 +147,11 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support this->hvac_hardware_info_available_ = true; - this->set_phase_(ProtocolPhases::SENDING_INIT_2); + this->set_phase(ProtocolPhases::SENDING_INIT_2); return result; } else { - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); return result; } } @@ -165,13 +160,13 @@ haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t r const uint8_t *data, size_t data_size) { haier_protocol::HandlerError result = this->answer_preprocess_( request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2); + (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); if (result == haier_protocol::HandlerError::HANDLER_OK) { - this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); return result; } else { - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); return result; } } @@ -185,8 +180,8 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); } else { if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); @@ -196,13 +191,13 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u } if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); this->set_force_send_control_(false); if (this->hvac_settings_.valid) this->hvac_settings_.reset(); @@ -210,8 +205,8 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u } return result; } else { - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); return result; } } @@ -225,10 +220,10 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); if (result == haier_protocol::HandlerError::HANDLER_OK) { - this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL); + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); return result; } else { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); return result; } } @@ -239,7 +234,7 @@ haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(u haier_protocol::HandlerError result = this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); return result; } @@ -248,24 +243,24 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_ if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { // Unexpected answer to request - this->set_phase_(ProtocolPhases::IDLE); - return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; } if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { // Don't expect this answer now - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; } memcpy(this->active_alarms_, data + 2, 8); - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::HANDLER_OK; } else { - this->set_phase_(ProtocolPhases::IDLE); - return haier_protocol::HandlerError::UNSUPORTED_MESSAGE; + this->set_phase(ProtocolPhases::IDLE); + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; } } -void HonClimate::set_answers_handlers() { +void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), @@ -311,7 +306,7 @@ void HonClimate::dump_config() { void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: - if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { this->hvac_hardware_info_available_ = false; // Indicate device capabilities: // bit 0 - if 1 module support interactive mode @@ -323,24 +318,24 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); - this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1); + this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); } break; case ProtocolPhases::SENDING_INIT_2: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); this->send_message_(DEVICEID_REQUEST, this->use_crc_); - this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2); + this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER); } break; case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage STATUS_REQUEST( - (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA); + (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); this->send_message_(STATUS_REQUEST, this->use_crc_); this->last_status_request_ = now; - this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); + this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; #ifdef USE_WIFI @@ -350,26 +345,14 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); this->last_signal_request_ = now; - this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); + this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); } break; case ProtocolPhases::SENDING_SIGNAL_LEVEL: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; - if (wifi::global_wifi_component->is_connected()) { - wifi_status_data[1] = 0; - int8_t rssi = wifi::global_wifi_component->wifi_rssi(); - wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f); - ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]); - } else { - ESP_LOGD(TAG, "WiFi is not connected"); - wifi_status_data[1] = 1; - wifi_status_data[3] = 0; - } - haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, - wifi_status_data, sizeof(wifi_status_data)); - this->send_message_(wifi_status_request, this->use_crc_); - this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), + this->use_crc_); + this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); } break; case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: @@ -380,7 +363,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); break; #endif case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: @@ -388,7 +371,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); - this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); + this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); } break; case ProtocolPhases::SENDING_CONTROL: @@ -403,12 +386,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { this->hvac_settings_.reset(); this->forced_request_status_ = true; this->forced_publish_ = true; - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { haier_protocol::HaierMessage control_message = get_control_message(); this->send_message_(control_message, this->use_crc_); ESP_LOGI(TAG, "Control packet sent"); - this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); } break; case ProtocolPhases::SENDING_POWER_ON_COMMAND: @@ -418,17 +401,17 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) pwr_cmd_buf[1] = 0x01; haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, - ((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1, + ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, pwr_cmd_buf, sizeof(pwr_cmd_buf)); this->send_message_(power_cmd, this->use_crc_); - this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); } break; - case ProtocolPhases::WAITING_ANSWER_INIT_1: - case ProtocolPhases::WAITING_ANSWER_INIT_2: + case ProtocolPhases::WAITING_INIT_1_ANSWER: + case ProtocolPhases::WAITING_INIT_2_ANSWER: case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: case ProtocolPhases::WAITING_STATUS_ANSWER: @@ -438,14 +421,14 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { - this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->forced_request_status_ = false; } #ifdef USE_WIFI else if (this->send_wifi_signal_ && (std::chrono::duration_cast(now - this->last_signal_request_).count() > SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) - this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); #endif } break; default: @@ -456,7 +439,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { #else ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); #endif - this->set_phase_(ProtocolPhases::SENDING_INIT_1); + this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } @@ -551,11 +534,12 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { } } if (climate_control.target_temperature.has_value()) { - out_data->set_point = - climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { - // If AC is off - no presets alowed + // If AC is off - no presets allowed out_data->quiet_mode = 0; out_data->fast_mode = 0; out_data->sleep_mode = 0; @@ -631,7 +615,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { break; } return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, - (uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS, + (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); } @@ -669,7 +653,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * { // Target temperature float old_target_temperature = this->target_temperature; - this->target_temperature = packet.control.set_point + 16.0f; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); should_publish = should_publish || (old_target_temperature != this->target_temperature); } { @@ -747,7 +731,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * if (new_cleaning != this->cleaning_status_) { ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); if (new_cleaning == CleaningState::NO_CLEANING) { - // Turnuin AC off after cleaning + // Turning AC off after cleaning this->action_request_ = ActionRequest::TURN_POWER_OFF; } this->cleaning_status_ = new_cleaning; @@ -837,7 +821,7 @@ void HonClimate::process_pending_action() { case ActionRequest::START_SELF_CLEAN: case ActionRequest::START_STERI_CLEAN: // Will reset action with control message sending - this->set_phase_(ProtocolPhases::SENDING_CONTROL); + this->set_phase(ProtocolPhases::SENDING_CONTROL); break; default: HaierClimateBase::process_pending_action(); diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index ab913f44e2..cf566e3b8e 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -48,10 +48,9 @@ class HonClimate : public HaierClimateBase { CleaningState get_cleaning_status() const; void start_self_cleaning(); void start_steri_cleaning(); - void set_send_wifi(bool send_wifi); protected: - void set_answers_handlers() override; + void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; haier_protocol::HaierMessage get_control_message() override; bool is_message_invalid(uint8_t message_type) override; @@ -87,8 +86,6 @@ class HonClimate : public HaierClimateBase { bool &use_crc_; uint8_t active_alarms_[8]; esphome::sensor::Sensor *outdoor_sensor_; - bool send_wifi_signal_; - std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level }; } // namespace haier diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index d572ce80d9..c6b32df200 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -53,12 +53,12 @@ struct HaierPacketControl { // 13 uint8_t : 8; // 14 - uint8_t ten_degree : 1; // 10 degree status - uint8_t display_status : 1; // If 0 disables AC's display - uint8_t half_degree : 1; // Use half degree - uint8_t intelegence_status : 1; // Intelligence status - uint8_t pmv_status : 1; // Comfort/PMV status - uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius + uint8_t ten_degree : 1; // 10 degree status + uint8_t display_status : 1; // If 0 disables AC's display + uint8_t half_degree : 1; // Use half degree + uint8_t intelligence_status : 1; // Intelligence status + uint8_t pmv_status : 1; // Comfort/PMV status + uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius uint8_t : 1; uint8_t steri_clean : 1; // 15 @@ -153,7 +153,7 @@ enum class FrameType : uint8_t { // <-> device, required) REPORT = 0x06, // Report frame (module <-> device, interactive, required) STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) - SYSTEM_DOWNLIK = 0x11, // System downlink frame (module -> device, optional) + SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional) DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) @@ -210,7 +210,7 @@ enum class FrameType : uint8_t { WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) }; -enum class SubcomandsControl : uint16_t { +enum class SubcommandsControl : uint16_t { GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 9c0fbac350..91b6bb0545 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -11,15 +11,10 @@ namespace esphome { namespace haier { static const char *const TAG = "haier.climate"; +constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; Smartair2Climate::Smartair2Climate() - : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) { - this->traits_.set_supported_presets({ - climate::CLIMATE_PRESET_NONE, - climate::CLIMATE_PRESET_BOOST, - climate::CLIMATE_PRESET_COMFORT, - }); -} + : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, size_t data_size) { @@ -30,8 +25,8 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); } else { if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); @@ -41,11 +36,11 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t } if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); this->set_force_send_control_(false); if (this->hvac_settings_.valid) this->hvac_settings_.reset(); @@ -53,17 +48,82 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t } return result; } else { - this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); return result; } } -void Smartair2Climate::set_answers_handlers() { +haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) + return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; + if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) + return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; + // Invalid packet is expected answer + if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && + ((data[37] & 0x04) != 0)) { + ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " + "instead of smartAir2"); + } + this->set_phase(ProtocolPhases::SENDING_INIT_2); + return haier_protocol::HandlerError::HANDLER_OK; +} + +haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, + uint8_t message_type, + const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + (uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) { + if (this->protocol_phase_ >= ProtocolPhases::IDLE) + return HaierClimateBase::timeout_default_handler_(message_type); + this->timeouts_counter_++; + ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, + (int) this->protocol_phase_, this->timeouts_counter_); + if (this->timeouts_counter_ >= 3) { + ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); + if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) + new_phase = ProtocolPhases::SENDING_INIT_1; + this->set_phase(new_phase); + } else { + // Returning to the previous state to try again + this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1)); + } + return haier_protocol::HandlerError::HANDLER_OK; +} + +void Smartair2Climate::set_handlers() { + // Set handlers + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( (uint8_t) (smartair2_protocol::FrameType::CONTROL), std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_answer_handler( + (uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), + std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_timeout_handler( + (uint8_t) (smartair2_protocol::FrameType::CONTROL), + std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); } void Smartair2Climate::dump_config() { @@ -74,39 +134,60 @@ void Smartair2Climate::dump_config() { void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: - this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); - break; - case ProtocolPhases::WAITING_ANSWER_INIT_1: - case ProtocolPhases::SENDING_INIT_2: - case ProtocolPhases::WAITING_ANSWER_INIT_2: - case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: - case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: - this->set_phase_(ProtocolPhases::SENDING_INIT_1); - break; - case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: - case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - this->set_phase_(ProtocolPhases::IDLE); - break; - case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: - if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) { - static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, - 0x4D01); - this->send_message_(STATUS_REQUEST, false); - this->last_status_request_ = now; - this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER); + if (this->can_send_message() && + (((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) || + ((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) { + // Indicate device capabilities: + // bit 0 - if 1 module support interactive mode + // bit 1 - if 1 module support controller-device mode + // bit 2 - if 1 module support crc + // bit 3 - if 1 module support multiple devices + // bit 4..bit 15 - not used + uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; + static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( + (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, + sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, false); + this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); } break; + case ProtocolPhases::SENDING_INIT_2: + case ProtocolPhases::WAITING_INIT_2_ANSWER: + this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D01); this->send_message_(STATUS_REQUEST, false); this->last_status_request_ = now; - this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER); + this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; +#ifdef USE_WIFI + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { + this->send_message_( + this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false); + this->last_signal_request_ = now; + this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + } + break; + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + break; +#else + case ProtocolPhases::SENDING_SIGNAL_LEVEL: + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER this->set_phase(ProtocolPhases::IDLE); break; +#endif + case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: + case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: + this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); + break; + case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: + case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: + this->set_phase(ProtocolPhases::SENDING_INIT_1); + break; case ProtocolPhases::SENDING_CONTROL: if (this->first_control_attempt_) { this->control_request_timestamp_ = now; @@ -119,14 +200,14 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) this->hvac_settings_.reset(); this->forced_request_status_ = true; this->forced_publish_ = true; - this->set_phase_(ProtocolPhases::IDLE); + this->set_phase(ProtocolPhases::IDLE); } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests { haier_protocol::HaierMessage control_message = get_control_message(); this->send_message_(control_message, false); ESP_LOGI(TAG, "Control packet sent"); - this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER); + this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); } break; case ProtocolPhases::SENDING_POWER_ON_COMMAND: @@ -136,11 +217,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) (uint8_t) smartair2_protocol::FrameType::CONTROL, this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); this->send_message_(power_cmd, false); - this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND + ? ProtocolPhases::WAITING_POWER_ON_ANSWER + : ProtocolPhases::WAITING_POWER_OFF_ANSWER); } break; + case ProtocolPhases::WAITING_INIT_1_ANSWER: case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: case ProtocolPhases::WAITING_STATUS_ANSWER: case ProtocolPhases::WAITING_CONTROL_ANSWER: @@ -149,14 +231,25 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { - this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST); + this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->forced_request_status_ = false; } +#ifdef USE_WIFI + else if (this->send_wifi_signal_ && + (std::chrono::duration_cast(now - this->last_signal_request_).count() > + SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) + this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); +#endif } break; default: // Shouldn't get here +#if (HAIER_LOG_LEVEL > 4) + ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", + phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); +#else ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); - this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); +#endif + this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } @@ -256,11 +349,12 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } } if (climate_control.target_temperature.has_value()) { - out_data->set_point = - climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16. + float target_temp = climate_control.target_temperature.value(); + out_data->set_point = target_temp - 16; // set the temperature with offset 16 + out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { - // If AC is off - no presets alowed + // If AC is off - no presets allowed out_data->turbo_mode = 0; out_data->quiet_mode = 0; } else if (climate_control.preset.has_value()) { @@ -312,7 +406,7 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin { // Target temperature float old_target_temperature = this->target_temperature; - this->target_temperature = packet.control.set_point + 16.0f; + this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f); should_publish = should_publish || (old_target_temperature != this->target_temperature); } { @@ -333,7 +427,7 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } switch (packet.control.fan_mode) { case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO: - // Somtimes AC reports in fan only mode that fan speed is auto + // Sometimes AC reports in fan only mode that fan speed is auto // but never accept this value back if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) { this->fan_mode = CLIMATE_FAN_AUTO; @@ -453,5 +547,15 @@ bool Smartair2Climate::is_message_invalid(uint8_t message_type) { return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; } +void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) { + int old_phase = (int) this->protocol_phase_; + int new_phase = (int) phase; + int min_p = std::min(old_phase, new_phase); + int max_p = std::max(old_phase, new_phase); + if ((min_p % 2 != 0) || (max_p - min_p > 1)) + this->timeouts_counter_ = 0; + HaierClimateBase::set_phase(phase); +} + } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h index c89d1f0be9..f173b10749 100644 --- a/esphome/components/haier/smartair2_climate.h +++ b/esphome/components/haier/smartair2_climate.h @@ -15,16 +15,25 @@ class Smartair2Climate : public HaierClimateBase { void dump_config() override; protected: - void set_answers_handlers() override; + void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; haier_protocol::HaierMessage get_control_message() override; bool is_message_invalid(uint8_t message_type) override; - // Answers handlers + void set_phase(HaierClimateBase::ProtocolPhases phase) override; + // Answer and timeout handlers haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, + const uint8_t *data, size_t data_size); + haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); std::unique_ptr last_status_message_; + unsigned int timeouts_counter_; }; } // namespace haier diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h index 8046516c5f..f791c21af2 100644 --- a/esphome/components/haier/smartair2_packet.h +++ b/esphome/components/haier/smartair2_packet.h @@ -53,8 +53,8 @@ struct HaierPacketControl { uint8_t : 2; uint8_t health_mode : 1; // Health mode on or off uint8_t compressor : 1; // Compressor on or off ??? - uint8_t : 1; - uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) + uint8_t half_degree : 1; // Use half degree + uint8_t ten_degree : 1; // 10 degree status (only work in heat mode) uint8_t : 0; // 28 uint8_t : 8; @@ -88,6 +88,9 @@ enum class FrameType : uint8_t { INVALID = 0x03, CONFIRM = 0x05, GET_DEVICE_VERSION = 0x61, + GET_DEVICE_VERSION_RESPONSE = 0x62, + GET_DEVICE_ID = 0x70, + GET_DEVICE_ID_RESPONSE = 0x71, REPORT_NETWORK_STATUS = 0xF7, NO_COMMAND = 0xFF, }; diff --git a/platformio.ini b/platformio.ini index ba149ce99e..5da3b9f978 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,7 +39,7 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 - pavlodn/HaierProtocol@0.9.18 ; haier + pavlodn/HaierProtocol@0.9.20 ; haier ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library build_flags = From be6f95d43eaaa419ed65641965fb384ec94c3a5f Mon Sep 17 00:00:00 2001 From: Steve Rodgers Date: Fri, 11 Aug 2023 17:50:33 -0700 Subject: [PATCH 051/270] pca9554 cache reads (#5137) --- esphome/components/pca9554/pca9554.cpp | 25 +++++++++++++++++++++++-- esphome/components/pca9554/pca9554.h | 6 ++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 39093fcf54..74c64dffaa 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -26,7 +26,7 @@ void PCA9554Component::setup() { this->config_mask_ = 0; // Invert mask as the part sees a 1 as an input this->write_register_(CONFIG_REG, ~this->config_mask_); - // All ouputs low + // All outputs low this->output_mask_ = 0; this->write_register_(OUTPUT_REG, this->output_mask_); // Read the inputs @@ -34,6 +34,14 @@ void PCA9554Component::setup() { ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); } + +void PCA9554Component::loop() { + // The read_inputs_() method will cache the input values from the chip. + this->read_inputs_(); + // Clear all the previously read flags. + this->was_previously_read_ = 0x00; +} + void PCA9554Component::dump_config() { ESP_LOGCONFIG(TAG, "PCA9554:"); LOG_I2C_DEVICE(this) @@ -43,7 +51,16 @@ void PCA9554Component::dump_config() { } bool PCA9554Component::digital_read(uint8_t pin) { - this->read_inputs_(); + // Note: We want to try and avoid doing any I2C bus read transactions here + // to conserve I2C bus bandwidth. So what we do is check to see if we + // have seen a read during the time esphome is running this loop. If we have, + // we do an I2C bus transaction to get the latest value. If we haven't + // we return a cached value which was read at the time loop() was called. + if (this->was_previously_read_ & (1 << pin)) + this->read_inputs_(); // Force a read of a new value + // Indicate we saw a read request for this pin in case a + // read happens later in the same loop. + this->was_previously_read_ |= (1 << pin); return this->input_mask_ & (1 << pin); } @@ -98,6 +115,10 @@ bool PCA9554Component::write_register_(uint8_t reg, uint8_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } +// Run our loop() method very early in the loop, so that we cache read values before +// before other components call our digital_read() method. +float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index d1bfc36bec..c2aa5c30ed 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -13,6 +13,8 @@ class PCA9554Component : public Component, public i2c::I2CDevice { /// Check i2c availability and setup masks void setup() override; + /// Poll for input changes periodically + void loop() override; /// Helper function to read the value of a pin. bool digital_read(uint8_t pin); /// Helper function to write the value of a pin. @@ -22,6 +24,8 @@ class PCA9554Component : public Component, public i2c::I2CDevice { float get_setup_priority() const override; + float get_loop_priority() const override; + void dump_config() override; protected: @@ -35,6 +39,8 @@ class PCA9554Component : public Component, public i2c::I2CDevice { uint8_t output_mask_{0x00}; /// The state of the actual input pin states - 1 means HIGH, 0 means LOW uint8_t input_mask_{0x00}; + /// Flags to check if read previously during this loop + uint8_t was_previously_read_ = {0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; }; From 3717e34bbab1fa21861a282e8621894a2cad2c7e Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 14 Aug 2023 01:06:04 +0400 Subject: [PATCH 052/270] fix midea: undo approved PR#4053 (#5233) --- esphome/components/remote_base/__init__.py | 18 +++++++---------- .../components/remote_base/midea_protocol.h | 20 +++++-------------- tests/test1.yaml | 2 ++ 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 24993e84d3..9e46506b3c 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1488,11 +1488,9 @@ MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_p MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) MIDEA_SCHEMA = cv.Schema( { - cv.Required(CONF_CODE): cv.templatable( - cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=5, max=5), - ) + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=5, max=5), ), } ) @@ -1519,12 +1517,10 @@ def midea_dumper(var, config): MIDEA_SCHEMA, ) async def midea_action(var, config, args): - code_ = config[CONF_CODE] - if cg.is_template(code_): - template_ = await cg.templatable(code_, args, cg.std_vector.template(cg.uint8)) - cg.add(var.set_code_template(template_)) - else: - cg.add(var.set_code_static(code_)) + template_ = await cg.templatable( + config[CONF_CODE], args, cg.std_vector.template(cg.uint8) + ) + cg.add(var.set_code(template_)) # AEHA diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index d81a50241b..f5db313579 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,11 +1,11 @@ #pragma once +#include +#include + #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" -#include -#include -#include namespace esphome { namespace remote_base { @@ -84,23 +84,13 @@ using MideaDumper = RemoteReceiverDumper; template class MideaAction : public RemoteTransmitterActionBase { TEMPLATABLE_VALUE(std::vector, code) - void set_code_static(std::vector code) { code_static_ = std::move(code); } - void set_code_template(std::function(Ts...)> func) { this->code_func_ = func; } + void set_code(std::initializer_list code) { this->code_ = code; } void encode(RemoteTransmitData *dst, Ts... x) override { - MideaData data; - if (!this->code_static_.empty()) { - data = MideaData(this->code_static_); - } else { - data = MideaData(this->code_func_(x...)); - } + MideaData data(this->code_.value(x...)); data.finalize(); MideaProtocol().encode(dst, data); } - - protected: - std::function(Ts...)> code_func_{}; - std::vector code_static_{}; }; } // namespace remote_base diff --git a/tests/test1.yaml b/tests/test1.yaml index 5c9b83a915..4eb78515c9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2333,6 +2333,8 @@ switch: second: !lambda "return 0xB21F98;" - remote_transmitter.transmit_midea: code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] + - remote_transmitter.transmit_midea: + code: !lambda "return {0xA2, 0x08, 0xFF, 0xFF, 0xFF};" - platform: gpio name: "MCP23S08 Pin #0" pin: From f26238e824d8df401eea8392633d7e1c3e042ed7 Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Sun, 13 Aug 2023 23:08:18 +0200 Subject: [PATCH 053/270] Fixing smartair2 protocol implementation if no Wi-Fi (#5238) --- esphome/components/haier/smartair2_climate.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 91b6bb0545..8bee37dadf 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -178,7 +178,9 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) break; #else case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER this->set_phase(ProtocolPhases::IDLE); break; + case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: + this->set_phase(ProtocolPhases::IDLE); + break; #endif case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: From 3a899e28dc5016ed9be8575b864f968a9d28b85f Mon Sep 17 00:00:00 2001 From: Kjell Braden Date: Sun, 13 Aug 2023 23:09:51 +0200 Subject: [PATCH 054/270] tuya: add time sync callback only once to prevent memleak (#5234) --- esphome/components/tuya/tuya.cpp | 9 +++++++-- esphome/components/tuya/tuya.h | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 7e6b1d53fe..0fad151488 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -246,8 +246,13 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff #ifdef USE_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_(); }); + + if (!this->time_sync_callback_registered_) { + // tuya mcu supports time, so we let them know when our time changed + auto *time_id = *this->time_id_; + time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + this->time_sync_callback_registered_ = true; + } } else { ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured"); } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index b9901dd5e7..26f6f65912 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -130,6 +130,7 @@ class Tuya : public Component, public uart::UARTDevice { #ifdef USE_TIME void send_local_time_(); optional time_id_{}; + bool time_sync_callback_registered_{false}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; bool init_failed_{false}; From b05a3fbb55da3b655a3c8f7dce0948296299ec06 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:09:20 +1200 Subject: [PATCH 055/270] Fix duplicate tuya time warning (#5243) --- esphome/components/tuya/tuya.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 0fad151488..daf5080e7a 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,9 +1,9 @@ #include "tuya.h" #include "esphome/components/network/util.h" +#include "esphome/core/gpio.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#include "esphome/core/gpio.h" #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" @@ -253,12 +253,11 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); this->time_sync_callback_registered_ = true; } - } else { + } else +#endif + { ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured"); } -#else - ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); -#endif break; case TuyaCommandType::VACUUM_MAP_UPLOAD: this->send_command_( From 560e36a65c3df089856c9c77bea5f2133d467310 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:09:49 +1200 Subject: [PATCH 056/270] Bump version to 2023.8.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6b442dd633..1442ebde9d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0b1" +__version__ = "2023.8.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 67b06a88b22bbd661f349e2fff442739d3606f90 Mon Sep 17 00:00:00 2001 From: MrEditor97 Date: Mon, 14 Aug 2023 20:14:08 +0100 Subject: [PATCH 057/270] Change XL9535 `setup_priority` to IO (#5246) --- esphome/components/xl9535/xl9535.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h index 8f0a868c42..dd67990fa8 100644 --- a/esphome/components/xl9535/xl9535.h +++ b/esphome/components/xl9535/xl9535.h @@ -26,7 +26,7 @@ class XL9535Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } + float get_setup_priority() const override { return setup_priority::IO; } }; class XL9535GPIOPin : public GPIOPin { From afd26c6f1a57508d9936f4887641a59992be199a Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 14 Aug 2023 23:21:22 +0400 Subject: [PATCH 058/270] rmt_base additional minor changes (#5245) --- esphome/components/remote_base/__init__.py | 21 +++++-------------- .../components/remote_base/midea_protocol.h | 1 - 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9e46506b3c..e2d96c9472 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1488,10 +1488,7 @@ MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_p MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) MIDEA_SCHEMA = cv.Schema( { - cv.Required(CONF_CODE): cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=5, max=5), - ), + cv.Required(CONF_CODE): cv.All([cv.hex_uint8_t], cv.Length(min=5, max=5)), } ) @@ -1511,15 +1508,10 @@ def midea_dumper(var, config): pass -@register_action( - "midea", - MideaAction, - MIDEA_SCHEMA, -) +@register_action("midea", MideaAction, MIDEA_SCHEMA) async def midea_action(var, config, args): - template_ = await cg.templatable( - config[CONF_CODE], args, cg.std_vector.template(cg.uint8) - ) + vec_ = cg.std_vector.template(cg.uint8) + template_ = await cg.templatable(config[CONF_CODE], args, vec_, vec_) cg.add(var.set_code(template_)) @@ -1530,10 +1522,7 @@ AEHAData, AEHABinarySensor, AEHATrigger, AEHAAction, AEHADumper = declare_protoc AEHA_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, - cv.Required(CONF_DATA): cv.All( - [cv.Any(cv.hex_uint8_t, cv.uint8_t)], - cv.Length(min=2, max=35), - ), + cv.Required(CONF_DATA): cv.All([cv.hex_uint8_t], cv.Length(min=2, max=35)), } ) diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index f5db313579..6925686b34 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -84,7 +84,6 @@ using MideaDumper = RemoteReceiverDumper; template class MideaAction : public RemoteTransmitterActionBase { TEMPLATABLE_VALUE(std::vector, code) - void set_code(std::initializer_list code) { this->code_ = code; } void encode(RemoteTransmitData *dst, Ts... x) override { MideaData data(this->code_.value(x...)); From ff8a73c2d140ee1847c308fc6d0bd4f3f3109a62 Mon Sep 17 00:00:00 2001 From: mulder-fbi Date: Wed, 16 Aug 2023 00:52:56 +0200 Subject: [PATCH 059/270] Fix 24 bit signed integer parsing in sml parser (#5250) --- esphome/components/sml/sml_parser.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index ff7da4cabd..91b320a30e 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -88,6 +88,11 @@ uint64_t bytes_to_uint(const bytes &buffer) { for (auto const value : buffer) { val = (val << 8) + value; } + // Some smart meters send 24 bit signed integers. Sign extend to 64 bit if the + // 24 bit value is negative. + if (buffer.size() == 3 && buffer[0] & 0x80) { + val |= 0xFFFFFFFFFF000000; + } return val; } From 48e4cb5ae24145ca6da22f3206eb2dcb85ef71d0 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Tue, 15 Aug 2023 18:13:43 -0500 Subject: [PATCH 060/270] Fix IDFI2CBus::writev ignoring stop parameter (#4840) Co-authored-by: Alexander Dimitrov --- esphome/components/i2c/i2c_bus_esp_idf.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index e2c7e7ddcb..5d35c1968b 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -202,11 +202,13 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b return ERROR_UNKNOWN; } } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; + if (stop) { + err = i2c_master_stop(cmd); + if (err != ESP_OK) { + ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); + i2c_cmd_link_delete(cmd); + return ERROR_UNKNOWN; + } } err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); From 63fa922547fb904f61553dc226bdd949d62e4010 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Wed, 16 Aug 2023 02:31:18 +0300 Subject: [PATCH 061/270] Add configuration flow abilites to the ld2410 component (#4434) --- CODEOWNERS | 2 +- esphome/components/ld2410/__init__.py | 188 +++--- esphome/components/ld2410/automation.h | 22 + esphome/components/ld2410/binary_sensor.py | 45 +- esphome/components/ld2410/button/__init__.py | 57 ++ .../components/ld2410/button/query_button.cpp | 9 + .../components/ld2410/button/query_button.h | 18 + .../components/ld2410/button/reset_button.cpp | 9 + .../components/ld2410/button/reset_button.h | 18 + .../ld2410/button/restart_button.cpp | 9 + .../components/ld2410/button/restart_button.h | 18 + esphome/components/ld2410/ld2410.cpp | 557 +++++++++++++++--- esphome/components/ld2410/ld2410.h | 207 +++++-- esphome/components/ld2410/number/__init__.py | 128 ++++ .../ld2410/number/gate_threshold_number.cpp | 14 + .../ld2410/number/gate_threshold_number.h | 19 + .../ld2410/number/light_threshold_number.cpp | 12 + .../ld2410/number/light_threshold_number.h | 18 + .../number/max_distance_timeout_number.cpp | 12 + .../number/max_distance_timeout_number.h | 18 + esphome/components/ld2410/select/__init__.py | 81 +++ .../ld2410/select/baud_rate_select.cpp | 12 + .../ld2410/select/baud_rate_select.h | 18 + .../select/distance_resolution_select.cpp | 12 + .../select/distance_resolution_select.h | 18 + .../select/light_out_control_select.cpp | 12 + .../ld2410/select/light_out_control_select.h | 18 + esphome/components/ld2410/sensor.py | 111 +++- esphome/components/ld2410/switch/__init__.py | 44 ++ .../ld2410/switch/bluetooth_switch.cpp | 12 + .../ld2410/switch/bluetooth_switch.h | 18 + .../ld2410/switch/engineering_mode_switch.cpp | 12 + .../ld2410/switch/engineering_mode_switch.h | 18 + esphome/components/ld2410/text_sensor.py | 33 ++ tests/test1.yaml | 149 ++++- 35 files changed, 1621 insertions(+), 327 deletions(-) create mode 100644 esphome/components/ld2410/automation.h create mode 100644 esphome/components/ld2410/button/__init__.py create mode 100644 esphome/components/ld2410/button/query_button.cpp create mode 100644 esphome/components/ld2410/button/query_button.h create mode 100644 esphome/components/ld2410/button/reset_button.cpp create mode 100644 esphome/components/ld2410/button/reset_button.h create mode 100644 esphome/components/ld2410/button/restart_button.cpp create mode 100644 esphome/components/ld2410/button/restart_button.h create mode 100644 esphome/components/ld2410/number/__init__.py create mode 100644 esphome/components/ld2410/number/gate_threshold_number.cpp create mode 100644 esphome/components/ld2410/number/gate_threshold_number.h create mode 100644 esphome/components/ld2410/number/light_threshold_number.cpp create mode 100644 esphome/components/ld2410/number/light_threshold_number.h create mode 100644 esphome/components/ld2410/number/max_distance_timeout_number.cpp create mode 100644 esphome/components/ld2410/number/max_distance_timeout_number.h create mode 100644 esphome/components/ld2410/select/__init__.py create mode 100644 esphome/components/ld2410/select/baud_rate_select.cpp create mode 100644 esphome/components/ld2410/select/baud_rate_select.h create mode 100644 esphome/components/ld2410/select/distance_resolution_select.cpp create mode 100644 esphome/components/ld2410/select/distance_resolution_select.h create mode 100644 esphome/components/ld2410/select/light_out_control_select.cpp create mode 100644 esphome/components/ld2410/select/light_out_control_select.h create mode 100644 esphome/components/ld2410/switch/__init__.py create mode 100644 esphome/components/ld2410/switch/bluetooth_switch.cpp create mode 100644 esphome/components/ld2410/switch/bluetooth_switch.h create mode 100644 esphome/components/ld2410/switch/engineering_mode_switch.cpp create mode 100644 esphome/components/ld2410/switch/engineering_mode_switch.h create mode 100644 esphome/components/ld2410/text_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 408caee4f2..b3ac833ee4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,7 +144,7 @@ esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb esphome/components/lcd_menu/* @numo68 -esphome/components/ld2410/* @sebcaps +esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/lilygo_t5_47/touchscreen/* @jesserockz diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index 47c4cdb0bd..2b30b65f46 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -1,113 +1,64 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID, CONF_TIMEOUT +from esphome.const import CONF_ID, CONF_THROTTLE, CONF_TIMEOUT, CONF_PASSWORD from esphome import automation from esphome.automation import maybe_simple_id DEPENDENCIES = ["uart"] -CODEOWNERS = ["@sebcaps"] +CODEOWNERS = ["@sebcaps", "@regevbr"] MULTI_CONF = True ld2410_ns = cg.esphome_ns.namespace("ld2410") LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice) -LD2410Restart = ld2410_ns.class_("LD2410Restart", automation.Action) + CONF_LD2410_ID = "ld2410_id" + CONF_MAX_MOVE_DISTANCE = "max_move_distance" CONF_MAX_STILL_DISTANCE = "max_still_distance" -CONF_G0_MOVE_THRESHOLD = "g0_move_threshold" -CONF_G0_STILL_THRESHOLD = "g0_still_threshold" -CONF_G1_MOVE_THRESHOLD = "g1_move_threshold" -CONF_G1_STILL_THRESHOLD = "g1_still_threshold" -CONF_G2_MOVE_THRESHOLD = "g2_move_threshold" -CONF_G2_STILL_THRESHOLD = "g2_still_threshold" -CONF_G3_MOVE_THRESHOLD = "g3_move_threshold" -CONF_G3_STILL_THRESHOLD = "g3_still_threshold" -CONF_G4_MOVE_THRESHOLD = "g4_move_threshold" -CONF_G4_STILL_THRESHOLD = "g4_still_threshold" -CONF_G5_MOVE_THRESHOLD = "g5_move_threshold" -CONF_G5_STILL_THRESHOLD = "g5_still_threshold" -CONF_G6_MOVE_THRESHOLD = "g6_move_threshold" -CONF_G6_STILL_THRESHOLD = "g6_still_threshold" -CONF_G7_MOVE_THRESHOLD = "g7_move_threshold" -CONF_G7_STILL_THRESHOLD = "g7_still_threshold" -CONF_G8_MOVE_THRESHOLD = "g8_move_threshold" -CONF_G8_STILL_THRESHOLD = "g8_still_threshold" +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] +CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] -DISTANCES = [0.75, 1.5, 2.25, 3, 3.75, 4.5, 5.25, 6] +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2410Component), + cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=cv.TimePeriod(milliseconds=1)), + ), + cv.Optional(CONF_MAX_MOVE_DISTANCE): cv.invalid( + f"The '{CONF_MAX_MOVE_DISTANCE}' option has been moved to the '{CONF_MAX_MOVE_DISTANCE}'" + f" number component" + ), + cv.Optional(CONF_MAX_STILL_DISTANCE): cv.invalid( + f"The '{CONF_MAX_STILL_DISTANCE}' option has been moved to the '{CONF_MAX_STILL_DISTANCE}'" + f" number component" + ), + cv.Optional(CONF_TIMEOUT): cv.invalid( + f"The '{CONF_TIMEOUT}' option has been moved to the '{CONF_TIMEOUT}'" + f" number component" + ), + } +) + +for i in range(9): + CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_MOVE_THRESHOLDS[i]): cv.invalid( + f"The '{CONF_MOVE_THRESHOLDS[i]}' option has been moved to the '{CONF_MOVE_THRESHOLDS[i]}'" + f" number component" + ), + cv.Optional(CONF_STILL_THRESHOLDS[i]): cv.invalid( + f"The '{CONF_STILL_THRESHOLDS[i]}' option has been moved to the '{CONF_STILL_THRESHOLDS[i]}'" + f" number component" + ), + } + ) + ) CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(LD2410Component), - cv.Optional(CONF_MAX_MOVE_DISTANCE, default="4.5m"): cv.All( - cv.distance, cv.one_of(*DISTANCES, float=True) - ), - cv.Optional(CONF_MAX_STILL_DISTANCE, default="4.5m"): cv.All( - cv.distance, cv.one_of(*DISTANCES, float=True) - ), - cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( - cv.positive_time_period_seconds, - cv.Range(max=cv.TimePeriod(seconds=32767)), - ), - cv.Optional(CONF_G0_MOVE_THRESHOLD, default=50): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G0_STILL_THRESHOLD, default=0): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G1_MOVE_THRESHOLD, default=50): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G1_STILL_THRESHOLD, default=0): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G2_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G2_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G3_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G3_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G4_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G4_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G5_MOVE_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G5_STILL_THRESHOLD, default=40): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G6_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G6_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G7_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G7_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G8_MOVE_THRESHOLD, default=30): cv.int_range( - min=0, max=100 - ), - cv.Optional(CONF_G8_STILL_THRESHOLD, default=15): cv.int_range( - min=0, max=100 - ), - } - ) - .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA.extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( @@ -123,31 +74,7 @@ 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) - cg.add(var.set_timeout(config[CONF_TIMEOUT])) - cg.add(var.set_max_move_distance(int(config[CONF_MAX_MOVE_DISTANCE] / 0.75))) - cg.add(var.set_max_still_distance(int(config[CONF_MAX_STILL_DISTANCE] / 0.75))) - cg.add( - var.set_range_config( - config[CONF_G0_MOVE_THRESHOLD], - config[CONF_G0_STILL_THRESHOLD], - config[CONF_G1_MOVE_THRESHOLD], - config[CONF_G1_STILL_THRESHOLD], - config[CONF_G2_MOVE_THRESHOLD], - config[CONF_G2_STILL_THRESHOLD], - config[CONF_G3_MOVE_THRESHOLD], - config[CONF_G3_STILL_THRESHOLD], - config[CONF_G4_MOVE_THRESHOLD], - config[CONF_G4_STILL_THRESHOLD], - config[CONF_G5_MOVE_THRESHOLD], - config[CONF_G5_STILL_THRESHOLD], - config[CONF_G6_MOVE_THRESHOLD], - config[CONF_G6_STILL_THRESHOLD], - config[CONF_G7_MOVE_THRESHOLD], - config[CONF_G7_STILL_THRESHOLD], - config[CONF_G8_MOVE_THRESHOLD], - config[CONF_G8_STILL_THRESHOLD], - ) - ) + cg.add(var.set_throttle(config[CONF_THROTTLE])) CALIBRATION_ACTION_SCHEMA = maybe_simple_id( @@ -155,3 +82,28 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( cv.Required(CONF_ID): cv.use_id(LD2410Component), } ) + + +# Actions +BluetoothPasswordSetAction = ld2410_ns.class_( + "BluetoothPasswordSetAction", automation.Action +) + + +BLUETOOTH_PASSWORD_SET_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(LD2410Component), + cv.Required(CONF_PASSWORD): cv.templatable(cv.string_strict), + } +) + + +@automation.register_action( + "bluetooth_password.set", BluetoothPasswordSetAction, BLUETOOTH_PASSWORD_SET_SCHEMA +) +async def bluetooth_password_set_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_PASSWORD], args, cg.std_string) + cg.add(var.set_password(template_)) + return var diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h new file mode 100644 index 0000000000..7cb9855f84 --- /dev/null +++ b/esphome/components/ld2410/automation.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "ld2410.h" + +namespace esphome { +namespace ld2410 { + +template class BluetoothPasswordSetAction : public Action { + public: + explicit BluetoothPasswordSetAction(LD2410Component *ld2410_comp) : ld2410_comp_(ld2410_comp) {} + TEMPLATABLE_VALUE(std::string, password) + + void play(Ts... x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } + + protected: + LD2410Component *ld2410_comp_; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index 02f73d57b7..3057480d25 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -1,36 +1,55 @@ import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY +from esphome.const import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_PRESENCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_MOTION_SENSOR, + ICON_ACCOUNT, +) from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] CONF_HAS_TARGET = "has_target" CONF_HAS_MOVING_TARGET = "has_moving_target" CONF_HAS_STILL_TARGET = "has_still_target" +CONF_OUT_PIN_PRESENCE_STATUS = "out_pin_presence_status" CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_OCCUPANCY + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_MOTION + device_class=DEVICE_CLASS_MOTION, + icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( - device_class=DEVICE_CLASS_OCCUPANCY + device_class=DEVICE_CLASS_OCCUPANCY, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_OUT_PIN_PRESENCE_STATUS): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PRESENCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_ACCOUNT, ), } async def to_code(config): ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) - if CONF_HAS_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_TARGET]) - cg.add(ld2410_component.set_target_sensor(sens)) - if CONF_HAS_MOVING_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_MOVING_TARGET]) - cg.add(ld2410_component.set_moving_target_sensor(sens)) - if CONF_HAS_STILL_TARGET in config: - sens = await binary_sensor.new_binary_sensor(config[CONF_HAS_STILL_TARGET]) - cg.add(ld2410_component.set_still_target_sensor(sens)) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(ld2410_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(ld2410_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(ld2410_component.set_still_target_binary_sensor(sens)) + if out_pin_presence_status_config := config.get(CONF_OUT_PIN_PRESENCE_STATUS): + sens = await binary_sensor.new_binary_sensor(out_pin_presence_status_config) + cg.add(ld2410_component.set_out_pin_presence_status_binary_sensor(sens)) diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py new file mode 100644 index 0000000000..3567114c2c --- /dev/null +++ b/esphome/components/ld2410/button/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_DIAGNOSTIC, + ENTITY_CATEGORY_CONFIG, + ICON_RESTART, + ICON_RESTART_ALERT, + ICON_DATABASE, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +QueryButton = ld2410_ns.class_("QueryButton", button.Button) +ResetButton = ld2410_ns.class_("ResetButton", button.Button) +RestartButton = ld2410_ns.class_("RestartButton", button.Button) + +CONF_FACTORY_RESET = "factory_reset" +CONF_RESTART = "restart" +CONF_QUERY_PARAMS = "query_params" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + ResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + QueryButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_reset_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_restart_button(b)) + if query_params_config := config.get(CONF_QUERY_PARAMS): + b = await button.new_button(query_params_config) + await cg.register_parented(b, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_query_button(b)) diff --git a/esphome/components/ld2410/button/query_button.cpp b/esphome/components/ld2410/button/query_button.cpp new file mode 100644 index 0000000000..47ab416f5a --- /dev/null +++ b/esphome/components/ld2410/button/query_button.cpp @@ -0,0 +1,9 @@ +#include "query_button.h" + +namespace esphome { +namespace ld2410 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h new file mode 100644 index 0000000000..c7a47e32d8 --- /dev/null +++ b/esphome/components/ld2410/button/query_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/reset_button.cpp b/esphome/components/ld2410/button/reset_button.cpp new file mode 100644 index 0000000000..f16c5faa79 --- /dev/null +++ b/esphome/components/ld2410/button/reset_button.cpp @@ -0,0 +1,9 @@ +#include "reset_button.h" + +namespace esphome { +namespace ld2410 { + +void ResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/reset_button.h b/esphome/components/ld2410/button/reset_button.h new file mode 100644 index 0000000000..78dd92c9f5 --- /dev/null +++ b/esphome/components/ld2410/button/reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class ResetButton : public button::Button, public Parented { + public: + ResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp new file mode 100644 index 0000000000..de0d36c1ef --- /dev/null +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2410 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h new file mode 100644 index 0000000000..d00dc05a53 --- /dev/null +++ b/esphome/components/ld2410/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 8e67ba54d7..c3b57815d6 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -1,5 +1,13 @@ #include "ld2410.h" +#include +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -8,48 +16,97 @@ namespace ld2410 { static const char *const TAG = "ld2410"; +LD2410Component::LD2410Component() {} + void LD2410Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2410:"); #ifdef USE_BINARY_SENSOR - LOG_BINARY_SENSOR(" ", "HasTargetSensor", this->target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "MovingSensor", this->moving_binary_sensor_); - LOG_BINARY_SENSOR(" ", "StillSensor", this->still_binary_sensor_); + LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "OutPinPresenceStatusBinarySensor", this->out_pin_presence_status_binary_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "EngineeringModeSwitch", this->engineering_mode_switch_); + LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_); +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "ResetButton", this->reset_button_); + LOG_BUTTON(" ", "RestartButton", this->restart_button_); + LOG_BUTTON(" ", "QueryButton", this->query_button_); #endif #ifdef USE_SENSOR - LOG_SENSOR(" ", "Moving Distance", this->moving_target_distance_sensor_); - LOG_SENSOR(" ", "Still Distance", this->still_target_distance_sensor_); - LOG_SENSOR(" ", "Moving Energy", this->moving_target_energy_sensor_); - LOG_SENSOR(" ", "Still Energy", this->still_target_energy_sensor_); - LOG_SENSOR(" ", "Detection Distance", this->detection_distance_sensor_); + LOG_SENSOR(" ", "LightSensor", this->light_sensor_); + LOG_SENSOR(" ", "MovingTargetDistanceSensor", this->moving_target_distance_sensor_); + LOG_SENSOR(" ", "StillTargetDistanceSensor", this->still_target_distance_sensor_); + LOG_SENSOR(" ", "MovingTargetEnergySensor", this->moving_target_energy_sensor_); + LOG_SENSOR(" ", "StillTargetEnergySensor", this->still_target_energy_sensor_); + LOG_SENSOR(" ", "DetectionDistanceSensor", this->detection_distance_sensor_); + for (sensor::Sensor *s : this->gate_still_sensors_) { + LOG_SENSOR(" ", "NthGateStillSesnsor", s); + } + for (sensor::Sensor *s : this->gate_move_sensors_) { + LOG_SENSOR(" ", "NthGateMoveSesnsor", s); + } #endif - this->set_config_mode_(true); - this->get_version_(); - this->set_config_mode_(false); - ESP_LOGCONFIG(TAG, " Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], - this->version_[3], this->version_[4], this->version_[5]); +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); + LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); +#endif +#ifdef USE_SELECT + LOG_SELECT(" ", "LightFunctionSelect", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevelSelect", this->out_pin_level_select_); + LOG_SELECT(" ", "DistanceResolutionSelect", this->distance_resolution_select_); + LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); +#endif +#ifdef USE_NUMBER + LOG_NUMBER(" ", "LightThresholdNumber", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxStillDistanceGateNumber", this->max_still_distance_gate_number_); + LOG_NUMBER(" ", "MaxMoveDistanceGateNumber", this->max_move_distance_gate_number_); + LOG_NUMBER(" ", "TimeoutNumber", this->timeout_number_); + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds Number", n); + } + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds Number", n); + } +#endif + this->read_all_info(); + ESP_LOGCONFIG(TAG, " Throttle_ : %ums", this->throttle_); + ESP_LOGCONFIG(TAG, " MAC Address : %s", const_cast(this->mac_.c_str())); + ESP_LOGCONFIG(TAG, " Firmware Version : %s", const_cast(this->version_.c_str())); } void LD2410Component::setup() { ESP_LOGCONFIG(TAG, "Setting up LD2410..."); - this->set_config_mode_(true); - this->set_max_distances_timeout_(this->max_move_distance_, this->max_still_distance_, this->timeout_); - // Configure Gates sensitivity - this->set_gate_threshold_(0, this->rg0_move_threshold_, this->rg0_still_threshold_); - this->set_gate_threshold_(1, this->rg1_move_threshold_, this->rg1_still_threshold_); - this->set_gate_threshold_(2, this->rg2_move_threshold_, this->rg2_still_threshold_); - this->set_gate_threshold_(3, this->rg3_move_threshold_, this->rg3_still_threshold_); - this->set_gate_threshold_(4, this->rg4_move_threshold_, this->rg4_still_threshold_); - this->set_gate_threshold_(5, this->rg5_move_threshold_, this->rg5_still_threshold_); - this->set_gate_threshold_(6, this->rg6_move_threshold_, this->rg6_still_threshold_); - this->set_gate_threshold_(7, this->rg7_move_threshold_, this->rg7_still_threshold_); - this->set_gate_threshold_(8, this->rg8_move_threshold_, this->rg8_still_threshold_); - this->get_version_(); - this->set_config_mode_(false); - ESP_LOGCONFIG(TAG, "Firmware Version : %u.%u.%u%u%u%u", this->version_[0], this->version_[1], this->version_[2], - this->version_[3], this->version_[4], this->version_[5]); + this->read_all_info(); + ESP_LOGCONFIG(TAG, "Mac Address : %s", const_cast(this->mac_.c_str())); + ESP_LOGCONFIG(TAG, "Firmware Version : %s", const_cast(this->version_.c_str())); ESP_LOGCONFIG(TAG, "LD2410 setup complete."); } +void LD2410Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + this->get_mac_(); + this->get_distance_resolution_(); + this->get_light_control_(); + this->query_parameters_(); + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2410Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + void LD2410Component::loop() { const int max_line_length = 80; static uint8_t buffer[max_line_length]; @@ -59,9 +116,8 @@ void LD2410Component::loop() { } } -void LD2410Component::send_command_(uint8_t command, uint8_t *command_value, int command_value_len) { - // lastCommandSuccess->publish_state(false); - +void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, int command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); // frame start bytes this->write_array(CMD_FRAME_HEADER, 4); // length bytes @@ -95,40 +151,43 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK) // Check constant values return; // data head=0xAA, data end=0x55, crc=0x00 - /* - Data Type: 6th - 0x01: Engineering mode - 0x02: Normal mode - */ - // char data_type = buffer[DATA_TYPES]; - /* - Target states: 9th - 0x00 = No target - 0x01 = Moving targets - 0x02 = Still targets - 0x03 = Moving+Still targets - */ -#ifdef USE_BINARY_SENSOR - char target_state = buffer[TARGET_STATES]; - if (this->target_binary_sensor_ != nullptr) { - this->target_binary_sensor_->publish_state(target_state != 0x00); - } -#endif - /* Reduce data update rate to prevent home assistant database size grow fast */ int32_t current_millis = millis(); - if (current_millis - last_periodic_millis < 1000) + if (current_millis - last_periodic_millis_ < this->throttle_) return; - last_periodic_millis = current_millis; + last_periodic_millis_ = current_millis; -#ifdef USE_BINARY_SENSOR - if (this->moving_binary_sensor_ != nullptr) { - this->moving_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = buffer[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr && + current_millis - last_engineering_mode_change_millis_ > this->throttle_) { + this->engineering_mode_switch_->publish_state(engineering_mode); } - if (this->still_binary_sensor_ != nullptr) { - this->still_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); +#endif +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = buffer[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); } #endif /* @@ -164,26 +223,126 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { if (this->detection_distance_sensor_->get_state() != new_detect_distance) this->detection_distance_sensor_->publish_state(new_detect_distance); } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving enery: 20~28th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { + sensor::Sensor *s = this->gate_move_sensors_[i]; + if (s != nullptr) { + s->publish_state(buffer[MOVING_SENSOR_START + i]); + } + } + /* + Still energy: 29~37th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { + sensor::Sensor *s = this->gate_still_sensors_[i]; + if (s != nullptr) { + s->publish_state(buffer[STILL_SENSOR_START + i]); + } + } + /* + Light sensor: 38th bytes + */ + if (this->light_sensor_ != nullptr) { + int new_light_sensor = buffer[LIGHT_SENSOR]; + if (this->light_sensor_->get_state() != new_light_sensor) + this->light_sensor_->publish_state(new_light_sensor); + } + } else { + for (auto *s : this->gate_move_sensors_) { + if (s != nullptr && !std::isnan(s->get_state())) { + s->publish_state(NAN); + } + } + for (auto *s : this->gate_still_sensors_) { + if (s != nullptr && !std::isnan(s->get_state())) { + s->publish_state(NAN); + } + } + if (this->light_sensor_ != nullptr && !std::isnan(this->light_sensor_->get_state())) { + this->light_sensor_->publish_state(NAN); + } + } +#endif +#ifdef USE_BINARY_SENSOR + if (engineering_mode) { + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state(buffer[OUT_PIN_SENSOR] == 0x01); + } + } else { + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state(false); + } + } #endif } -void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { - ESP_LOGV(TAG, "Handling ACK DATA for COMMAND"); +const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; + +std::string format_version(uint8_t *buffer) { + std::string::size_type version_size = 256; + std::string version; + do { + version.resize(version_size + 1); + version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], + buffer[16], buffer[15], buffer[14]); + } while (version_size + 1 > version.size()); + version.resize(version_size); + return version; +} + +const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; + +const std::string UNKNOWN_MAC("unknown"); +const std::string NO_MAC("08:05:04:03:02:01"); + +std::string format_mac(uint8_t *buffer) { + std::string::size_type mac_size = 256; + std::string mac; + do { + mac.resize(mac_size + 1); + mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], + buffer[15]); + } while (mac_size + 1 > mac.size()); + mac.resize(mac_size); + if (mac == NO_MAC) { + return UNKNOWN_MAC; + } + return mac; +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + float normalized_value = value * 1.0; + if (n != nullptr && (!n->has_state() || n->state != normalized_value)) { + n->state = normalized_value; + return [n, normalized_value]() { n->publish_state(normalized_value); }; + } + return []() {}; +} +#endif + +bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); if (len < 10) { ESP_LOGE(TAG, "Error with last command : incorrect length"); - return; + return true; } if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes ESP_LOGE(TAG, "Error with last command : incorrect Header"); - return; + return true; } if (buffer[COMMAND_STATUS] != 0x01) { ESP_LOGE(TAG, "Error with last command : status != 0x01"); - return; + return true; } if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); - return; + return true; } switch (buffer[COMMAND]) { @@ -193,49 +352,127 @@ void LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { case lowbyte(CMD_DISABLE_CONF): ESP_LOGV(TAG, "Handled Disabled conf command"); break; + case lowbyte(CMD_SET_BAUD_RATE): + ESP_LOGV(TAG, "Handled baud rate change command"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); + } +#endif + break; case lowbyte(CMD_VERSION): - ESP_LOGV(TAG, "FW Version is: %u.%u.%u%u%u%u", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], - buffer[14]); - this->version_[0] = buffer[13]; - this->version_[1] = buffer[12]; - this->version_[2] = buffer[17]; - this->version_[3] = buffer[16]; - this->version_[4] = buffer[15]; - this->version_[5] = buffer[14]; - + this->version_ = format_version(buffer); + ESP_LOGV(TAG, "FW Version is: %s", const_cast(this->version_.c_str())); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(this->version_); + } +#endif + break; + case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { + std::string distance_resolution = + DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); + ESP_LOGV(TAG, "Distance resolution is: %s", const_cast(distance_resolution.c_str())); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr && + this->distance_resolution_select_->state != distance_resolution) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + } break; + case lowbyte(CMD_QUERY_LIGHT_CONTROL): { + this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); + this->light_threshold_ = buffer[11] * 1.0; + this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); + ESP_LOGV(TAG, "Light function is: %s", const_cast(this->light_function_.c_str())); + ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); + ESP_LOGV(TAG, "Out pin level is: %s", const_cast(this->out_pin_level_.c_str())); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { + this->light_function_select_->publish_state(this->light_function_); + } + if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->state != this->out_pin_level_) { + this->out_pin_level_select_->publish_state(this->out_pin_level_); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && + (!this->light_threshold_number_->has_state() || + this->light_threshold_number_->state != this->light_threshold_)) { + this->light_threshold_number_->publish_state(this->light_threshold_); + } +#endif + } break; + case lowbyte(CMD_MAC): + if (len < 20) { + return false; + } + this->mac_ = format_mac(buffer); + ESP_LOGV(TAG, "MAC Address is: %s", const_cast(this->mac_.c_str())); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(this->mac_); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + } +#endif break; case lowbyte(CMD_GATE_SENS): ESP_LOGV(TAG, "Handled sensitivity command"); break; + case lowbyte(CMD_BLUETOOTH): + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + case lowbyte(CMD_SET_DISTANCE_RESOLUTION): + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + case lowbyte(CMD_SET_LIGHT_CONTROL): + ESP_LOGV(TAG, "Handled set light control command"); + break; + case lowbyte(CMD_BT_PASSWORD): + ESP_LOGV(TAG, "Handled set bluetooth password command"); + break; case lowbyte(CMD_QUERY): // Query parameters response { if (buffer[10] != 0xAA) - return; // value head=0xAA + return true; // value head=0xAA +#ifdef USE_NUMBER /* Moving distance range: 13th byte Still distance range: 14th byte */ - // TODO - // maxMovingDistanceRange->publish_state(buffer[12]); - // maxStillDistanceRange->publish_state(buffer[13]); + std::vector> updates; + updates.push_back(set_number_value(this->max_move_distance_gate_number_, buffer[12])); + updates.push_back(set_number_value(this->max_still_distance_gate_number_, buffer[13])); /* Moving Sensitivities: 15~23th bytes + */ + for (std::vector::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], buffer[14 + i])); + } + /* Still Sensitivities: 24~32th bytes */ - for (int i = 0; i < 9; i++) { - moving_sensitivities[i] = buffer[14 + i]; - } - for (int i = 0; i < 9; i++) { - still_sensitivities[i] = buffer[23 + i]; + for (std::vector::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], buffer[23 + i])); } /* None Duration: 33~34th bytes */ - // noneDuration->publish_state(this->two_byte_to_int_(buffer[32], buffer[33])); + updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); + for (auto &update : updates) { + update(); + } +#endif } break; default: break; } + + return true; } void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { @@ -256,8 +493,11 @@ void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 && buffer[pos - 1] == 0x01) { ESP_LOGV(TAG, "Will handle ACK Data"); - this->handle_ack_data_(buffer, pos); - pos = 0; // Reset position index ready for next time + if (this->handle_ack_data_(buffer, pos)) { + pos = 0; // Reset position index ready for next time + } else { + ESP_LOGV(TAG, "ACK Data incomplete"); + } } } } @@ -269,21 +509,85 @@ void LD2410Component::set_config_mode_(bool enable) { this->send_command_(cmd, enable ? cmd_value : nullptr, 2); } +void LD2410Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + uint8_t enable_cmd_value[2] = {0x01, 0x00}; + uint8_t disable_cmd_value[2] = {0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::set_distance_resolution(const std::string &state) { + this->set_config_mode_(true); + uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2410Component::set_bluetooth_password(const std::string &password) { + if (password.length() != 6) { + ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); + return; + } + this->set_config_mode_(true); + uint8_t cmd_value[6]; + std::copy(password.begin(), password.end(), std::begin(cmd_value)); + this->send_command_(CMD_BT_PASSWORD, cmd_value, 6); + this->set_config_mode_(false); +} + +void LD2410Component::set_engineering_mode(bool enable) { + this->set_config_mode_(true); + last_engineering_mode_change_millis_ = millis(); + uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2410Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_RESET, nullptr, 0); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } +void LD2410Component::get_mac_() { + uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_MAC, cmd_value, 2); +} +void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } -void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, - uint16_t timeout) { +void LD2410Component::get_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +#ifdef USE_NUMBER +void LD2410Component::set_max_distances_timeout() { + if (!this->max_move_distance_gate_number_->has_state() || !this->max_still_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } + int max_moving_distance_gate_range = static_cast(this->max_move_distance_gate_number_->state); + int max_still_distance_gate_range = static_cast(this->max_still_distance_gate_number_->state); + int timeout = static_cast(this->timeout_number_->state); uint8_t value[18] = {0x00, 0x00, - lowbyte(max_moving_distance_range), - highbyte(max_moving_distance_range), + lowbyte(max_moving_distance_gate_range), + highbyte(max_moving_distance_gate_range), 0x00, 0x00, 0x01, 0x00, - lowbyte(max_still_distance_range), - highbyte(max_still_distance_range), + lowbyte(max_still_distance_gate_range), + highbyte(max_still_distance_gate_range), 0x00, 0x00, 0x02, @@ -292,10 +596,25 @@ void LD2410Component::set_max_distances_timeout_(uint8_t max_moving_distance_ran highbyte(timeout), 0x00, 0x00}; + this->set_config_mode_(true); this->send_command_(CMD_MAXDIST_DURATION, value, 18); + delay(50); // NOLINT this->query_parameters_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); + this->set_config_mode_(false); } -void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens) { + +void LD2410Component::set_gate_threshold(uint8_t gate) { + number::Number *motionsens = this->gate_move_threshold_numbers_[gate]; + number::Number *stillsens = this->gate_still_threshold_numbers_[gate]; + + if (!motionsens->has_state() || !stillsens->has_state()) { + return; + } + int motion = static_cast(motionsens->state); + int still = static_cast(stillsens->state); + + this->set_config_mode_(true); // reference // https://drive.google.com/drive/folders/1p4dhbEJA3YubyIjIIC7wwVsSo8x29Fq-?spm=a2g0o.detail.1000023.17.93465697yFwVxH // Send data: configure the motion sensitivity of distance gate 3 to 40, and the static sensitivity of 40 @@ -305,11 +624,57 @@ void LD2410Component::set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint // 28 00 00 00 (value) // 02 00 (still sensitivtiy) // 28 00 00 00 (value) - uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, - 0x01, 0x00, lowbyte(motionsens), highbyte(motionsens), 0x00, 0x00, - 0x02, 0x00, lowbyte(stillsens), highbyte(stillsens), 0x00, 0x00}; + uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, + 0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, + 0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00}; this->send_command_(CMD_GATE_SENS, value, 18); + delay(50); // NOLINT + this->query_parameters_(); + this->set_config_mode_(false); } +void LD2410Component::set_gate_still_threshold_number(int gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2410Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = this->light_threshold_number_->state; + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = this->light_function_select_->state; + } + if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { + this->out_pin_level_ = this->out_pin_level_select_->state; + } +#endif + if (this->light_function_.empty() || this->out_pin_level_.empty() || this->light_threshold_ < 0) { + return; + } + this->set_config_mode_(true); + uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); + uint8_t light_threshold = static_cast(this->light_threshold_); + uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); + uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; + this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); + delay(50); // NOLINT + this->get_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); + this->set_config_mode_(false); +} + +#ifdef USE_SENSOR +void LD2410Component::set_gate_move_sensor(int gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } +void LD2410Component::set_gate_still_sensor(int gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } +#endif + } // namespace ld2410 } // namespace esphome diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 8edb83a8d5..8084d4c33e 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -7,10 +7,27 @@ #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace ld2410 { @@ -19,10 +36,63 @@ namespace ld2410 { // Commands static const uint8_t CMD_ENABLE_CONF = 0x00FF; static const uint8_t CMD_DISABLE_CONF = 0x00FE; +static const uint8_t CMD_ENABLE_ENG = 0x0062; +static const uint8_t CMD_DISABLE_ENG = 0x0063; static const uint8_t CMD_MAXDIST_DURATION = 0x0060; static const uint8_t CMD_QUERY = 0x0061; static const uint8_t CMD_GATE_SENS = 0x0064; static const uint8_t CMD_VERSION = 0x00A0; +static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; +static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; +static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; +static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; +static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; +static const uint8_t CMD_BT_PASSWORD = 0x00A9; +static const uint8_t CMD_MAC = 0x00A5; +static const uint8_t CMD_RESET = 0x00A2; +static const uint8_t CMD_RESTART = 0x00A3; +static const uint8_t CMD_BLUETOOTH = 0x00A4; + +enum BaudRateStructure : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +static const std::map BAUD_RATE_ENUM_TO_INT{ + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; + +enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; + +static const std::map DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.75m", DISTANCE_RESOLUTION_0_75}}; +static const std::map DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}}; + +enum LightFunctionStructure : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02 +}; + +static const std::map LIGHT_FUNCTION_ENUM_TO_INT{ + {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; +static const std::map LIGHT_FUNCTION_INT_TO_ENUM{ + {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; + +enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; + +static const std::map OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}}; +static const std::map OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}}; // Commands values static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; @@ -44,7 +114,7 @@ Target states: 9th byte Detect distance: 16~17th bytes */ enum PeriodicDataStructure : uint8_t { - DATA_TYPES = 5, + DATA_TYPES = 6, TARGET_STATES = 8, MOVING_TARGET_LOW = 9, MOVING_TARGET_HIGH = 10, @@ -54,6 +124,10 @@ enum PeriodicDataStructure : uint8_t { STILL_ENERGY = 14, DETECT_DISTANCE_LOW = 15, DETECT_DISTANCE_HIGH = 16, + MOVING_SENSOR_START = 19, + STILL_SENSOR_START = 28, + LIGHT_SENSOR = 37, + OUT_PIN_SENSOR = 38, }; enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 }; @@ -66,80 +140,97 @@ class LD2410Component : public Component, public uart::UARTDevice { SUB_SENSOR(still_target_distance) SUB_SENSOR(moving_target_energy) SUB_SENSOR(still_target_energy) + SUB_SENSOR(light) SUB_SENSOR(detection_distance) #endif +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(target) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(out_pin_presence_status) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(version) + SUB_TEXT_SENSOR(mac) +#endif +#ifdef USE_SELECT + SUB_SELECT(distance_resolution) + SUB_SELECT(baud_rate) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(engineering_mode) + SUB_SWITCH(bluetooth) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(reset) + SUB_BUTTON(restart) + SUB_BUTTON(query) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(max_still_distance_gate) + SUB_NUMBER(max_move_distance_gate) + SUB_NUMBER(timeout) + SUB_NUMBER(light_threshold) +#endif public: + LD2410Component(); void setup() override; void dump_config() override; void loop() override; - -#ifdef USE_BINARY_SENSOR - void set_target_sensor(binary_sensor::BinarySensor *sens) { this->target_binary_sensor_ = sens; }; - void set_moving_target_sensor(binary_sensor::BinarySensor *sens) { this->moving_binary_sensor_ = sens; }; - void set_still_target_sensor(binary_sensor::BinarySensor *sens) { this->still_binary_sensor_ = sens; }; + void set_light_out_control(); +#ifdef USE_NUMBER + void set_gate_still_threshold_number(int gate, number::Number *n); + void set_gate_move_threshold_number(int gate, number::Number *n); + void set_max_distances_timeout(); + void set_gate_threshold(uint8_t gate); #endif - - void set_timeout(uint16_t value) { this->timeout_ = value; }; - void set_max_move_distance(uint8_t value) { this->max_move_distance_ = value; }; - void set_max_still_distance(uint8_t value) { this->max_still_distance_ = value; }; - void set_range_config(int rg0_move, int rg0_still, int rg1_move, int rg1_still, int rg2_move, int rg2_still, - int rg3_move, int rg3_still, int rg4_move, int rg4_still, int rg5_move, int rg5_still, - int rg6_move, int rg6_still, int rg7_move, int rg7_still, int rg8_move, int rg8_still) { - this->rg0_move_threshold_ = rg0_move; - this->rg0_still_threshold_ = rg0_still; - this->rg1_move_threshold_ = rg1_move; - this->rg1_still_threshold_ = rg1_still; - this->rg2_move_threshold_ = rg2_move; - this->rg2_still_threshold_ = rg2_still; - this->rg3_move_threshold_ = rg3_move; - this->rg3_still_threshold_ = rg3_still; - this->rg4_move_threshold_ = rg4_move; - this->rg4_still_threshold_ = rg4_still; - this->rg5_move_threshold_ = rg5_move; - this->rg5_still_threshold_ = rg5_still; - this->rg6_move_threshold_ = rg6_move; - this->rg6_still_threshold_ = rg6_still; - this->rg7_move_threshold_ = rg7_move; - this->rg7_still_threshold_ = rg7_still; - this->rg8_move_threshold_ = rg8_move; - this->rg8_still_threshold_ = rg8_still; - }; - int moving_sensitivities[9] = {0}; - int still_sensitivities[9] = {0}; - - int32_t last_periodic_millis = millis(); +#ifdef USE_SENSOR + void set_gate_move_sensor(int gate, sensor::Sensor *s); + void set_gate_still_sensor(int gate, sensor::Sensor *s); +#endif + void set_throttle(uint16_t value) { this->throttle_ = value; }; + void set_bluetooth_password(const std::string &password); + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const std::string &state); + void set_baud_rate(const std::string &state); + void factory_reset(); protected: -#ifdef USE_BINARY_SENSOR - binary_sensor::BinarySensor *target_binary_sensor_{nullptr}; - binary_sensor::BinarySensor *moving_binary_sensor_{nullptr}; - binary_sensor::BinarySensor *still_binary_sensor_{nullptr}; -#endif - - std::vector rx_buffer_; int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - void send_command_(uint8_t command_str, uint8_t *command_value, int command_value_len); - - void set_max_distances_timeout_(uint8_t max_moving_distance_range, uint8_t max_still_distance_range, - uint16_t timeout); - void set_gate_threshold_(uint8_t gate, uint8_t motionsens, uint8_t stillsens); + void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); void set_config_mode_(bool enable); void handle_periodic_data_(uint8_t *buffer, int len); - void handle_ack_data_(uint8_t *buffer, int len); + bool handle_ack_data_(uint8_t *buffer, int len); void readline_(int readch, uint8_t *buffer, int len); void query_parameters_(); void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void get_light_control_(); + void restart_(); - uint16_t timeout_; - uint8_t max_move_distance_; - uint8_t max_still_distance_; - - uint8_t version_[6]; - uint8_t rg0_move_threshold_, rg0_still_threshold_, rg1_move_threshold_, rg1_still_threshold_, rg2_move_threshold_, - rg2_still_threshold_, rg3_move_threshold_, rg3_still_threshold_, rg4_move_threshold_, rg4_still_threshold_, - rg5_move_threshold_, rg5_still_threshold_, rg6_move_threshold_, rg6_still_threshold_, rg7_move_threshold_, - rg7_still_threshold_, rg8_move_threshold_, rg8_still_threshold_; + int32_t last_periodic_millis_ = millis(); + int32_t last_engineering_mode_change_millis_ = millis(); + uint16_t throttle_; + std::string version_; + std::string mac_; + std::string out_pin_level_; + std::string light_function_; + float light_threshold_ = -1; +#ifdef USE_NUMBER + std::vector gate_still_threshold_numbers_ = std::vector(9); + std::vector gate_move_threshold_numbers_ = std::vector(9); +#endif +#ifdef USE_SENSOR + std::vector gate_still_sensors_ = std::vector(9); + std::vector gate_move_sensors_ = std::vector(9); +#endif }; } // namespace ld2410 diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py new file mode 100644 index 0000000000..557b226dfe --- /dev/null +++ b/esphome/components/ld2410/number/__init__.py @@ -0,0 +1,128 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_ILLUMINANCE, + UNIT_SECOND, + UNIT_PERCENT, + ENTITY_CATEGORY_CONFIG, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + ICON_LIGHTBULB, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate" +CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate" +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_STILL_THRESHOLD = "still_threshold" +CONF_MOVE_THRESHOLD = "move_threshold" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + unit_of_measurement=UNIT_SECOND, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + ), + cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"g{x}"): cv.Schema( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + } + ) + for x in range(9) + } +) + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number( + timeout_config, min_value=0, max_value=65535, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_timeout_number(n)) + if max_move_distance_gate_config := config.get(CONF_MAX_MOVE_DISTANCE_GATE): + n = await number.new_number( + max_move_distance_gate_config, min_value=2, max_value=8, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_max_move_distance_gate_number(n)) + if max_still_distance_gate_config := config.get(CONF_MAX_STILL_DISTANCE_GATE): + n = await number.new_number( + max_still_distance_gate_config, min_value=2, max_value=8, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_max_still_distance_gate_number(n)) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_light_threshold_number(n)) + for x in range(9): + if gate_conf := config.get(f"g{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_gate_move_threshold_number(x, n)) + + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_gate_still_threshold_number(x, n)) diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..5d040554d7 --- /dev/null +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -0,0 +1,14 @@ +#include "gate_threshold_number.h" + +namespace esphome { +namespace ld2410 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(this->gate_); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h new file mode 100644 index 0000000000..2806ecce63 --- /dev/null +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/light_threshold_number.cpp b/esphome/components/ld2410/number/light_threshold_number.cpp new file mode 100644 index 0000000000..0ff35782cd --- /dev/null +++ b/esphome/components/ld2410/number/light_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "light_threshold_number.h" + +namespace esphome { +namespace ld2410 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h new file mode 100644 index 0000000000..8f014373c0 --- /dev/null +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.cpp b/esphome/components/ld2410/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..8a946f7ea9 --- /dev/null +++ b/esphome/components/ld2410/number/max_distance_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "max_distance_timeout_number.h" + +namespace esphome { +namespace ld2410 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_max_distances_timeout(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..7d91b4b5fe --- /dev/null +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/__init__.py b/esphome/components/ld2410/select/__init__.py new file mode 100644 index 0000000000..6c34a85ac6 --- /dev/null +++ b/esphome/components/ld2410/select/__init__.py @@ -0,0 +1,81 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_CONFIG, + CONF_BAUD_RATE, + ICON_THERMOMETER, + ICON_SCALE, + ICON_LIGHTBULB, + ICON_RULER, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +BaudRateSelect = ld2410_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = ld2410_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = ld2410_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_distance_resolution_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_out_pin_level_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_light_function_select(s)) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_baud_rate_select(s)) diff --git a/esphome/components/ld2410/select/baud_rate_select.cpp b/esphome/components/ld2410/select/baud_rate_select.cpp new file mode 100644 index 0000000000..f4e0b90e2e --- /dev/null +++ b/esphome/components/ld2410/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2410 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h new file mode 100644 index 0000000000..3827b6a48a --- /dev/null +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..eef34bda63 --- /dev/null +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -0,0 +1,12 @@ +#include "distance_resolution_select.h" + +namespace esphome { +namespace ld2410 { + +void DistanceResolutionSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_distance_resolution(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h new file mode 100644 index 0000000000..d6affb1020 --- /dev/null +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/light_out_control_select.cpp b/esphome/components/ld2410/select/light_out_control_select.cpp new file mode 100644 index 0000000000..ac23248a64 --- /dev/null +++ b/esphome/components/ld2410/select/light_out_control_select.cpp @@ -0,0 +1,12 @@ +#include "light_out_control_select.h" + +namespace esphome { +namespace ld2410 { + +void LightOutControlSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h new file mode 100644 index 0000000000..5d72e1774e --- /dev/null +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index b941263134..83361db58a 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -3,9 +3,15 @@ from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_DISTANCE, - DEVICE_CLASS_ENERGY, UNIT_CENTIMETER, UNIT_PERCENT, + CONF_LIGHT, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_SIGNAL, + ICON_FLASH, + ICON_MOTION_SENSOR, + ICON_LIGHTBULB, ) from . import CONF_LD2410_ID, LD2410Component @@ -15,41 +21,88 @@ CONF_STILL_DISTANCE = "still_distance" CONF_MOVING_ENERGY = "moving_energy" CONF_STILL_ENERGY = "still_energy" CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" -CONFIG_SCHEMA = { - cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), - cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), - cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), - cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT - ), - cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - device_class=DEVICE_CLASS_ENERGY, unit_of_measurement=UNIT_PERCENT - ), - cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER - ), -} +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_FLASH, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + icon=ICON_SIGNAL, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"g{x}"): cv.Schema( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_FLASH, + ), + } + ) + for x in range(9) + } +) async def to_code(config): ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) - if CONF_MOVING_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_MOVING_DISTANCE]) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) cg.add(ld2410_component.set_moving_target_distance_sensor(sens)) - if CONF_STILL_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_STILL_DISTANCE]) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) cg.add(ld2410_component.set_still_target_distance_sensor(sens)) - if CONF_MOVING_ENERGY in config: - sens = await sensor.new_sensor(config[CONF_MOVING_ENERGY]) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) cg.add(ld2410_component.set_moving_target_energy_sensor(sens)) - if CONF_STILL_ENERGY in config: - sens = await sensor.new_sensor(config[CONF_STILL_ENERGY]) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) cg.add(ld2410_component.set_still_target_energy_sensor(sens)) - if CONF_DETECTION_DISTANCE in config: - sens = await sensor.new_sensor(config[CONF_DETECTION_DISTANCE]) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(ld2410_component.set_light_sensor(sens)) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) cg.add(ld2410_component.set_detection_distance_sensor(sens)) + for x in range(9): + if gate_conf := config.get(f"g{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(ld2410_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(ld2410_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py new file mode 100644 index 0000000000..096cb5f67a --- /dev/null +++ b/esphome/components/ld2410/switch/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_SWITCH, + ICON_BLUETOOTH, + ENTITY_CATEGORY_CONFIG, + ICON_PULSE, +) +from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns + +BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" +CONF_BLUETOOTH = "bluetooth" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_engineering_mode_switch(s)) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2410_ID]) + cg.add(ld2410_component.set_bluetooth_switch(s)) diff --git a/esphome/components/ld2410/switch/bluetooth_switch.cpp b/esphome/components/ld2410/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..9bcee9b049 --- /dev/null +++ b/esphome/components/ld2410/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2410 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h new file mode 100644 index 0000000000..35ae1ec0c9 --- /dev/null +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..967c87c887 --- /dev/null +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -0,0 +1,12 @@ +#include "engineering_mode_switch.h" + +namespace esphome { +namespace ld2410 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..e521200cd6 --- /dev/null +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2410.h" + +namespace esphome { +namespace ld2410 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/text_sensor.py b/esphome/components/ld2410/text_sensor.py new file mode 100644 index 0000000000..d64472a7d3 --- /dev/null +++ b/esphome/components/ld2410/text_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + CONF_VERSION, + CONF_MAC_ADDRESS, + ICON_BLUETOOTH, + ICON_CHIP, +) +from . import CONF_LD2410_ID, LD2410Component + +DEPENDENCIES = ["ld2410"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + ld2410_component = await cg.get_variable(config[CONF_LD2410_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(ld2410_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(ld2410_component.set_mac_text_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 4eb78515c9..66caad961a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -170,6 +170,9 @@ mqtt: id: uart_0 data: !lambda |- return {}; + - bluetooth_password.set: + id: my_ld2410 + password: abcdef on_connect: - light.turn_on: ${roomname}_lights - mqtt.publish: @@ -1333,16 +1336,64 @@ sensor: speed: name: "Radiator Pump Speed" - platform: ld2410 + light: + name: light moving_distance: name: "Moving distance (cm)" still_distance: name: "Still Distance (cm)" moving_energy: - name: "Move Energy" + name: "Move Energy (%)" still_energy: - name: "Still Energy" + name: "Still Energy (%)" detection_distance: - name: "Distance Detection" + name: "Distance Detection (cm)" + g0: + move_energy: + name: g0 move energy + still_energy: + name: g0 still energy + g1: + move_energy: + name: g1 move energy + still_energy: + name: g1 still energy + g2: + move_energy: + name: g2 move energy + still_energy: + name: g2 still energy + g3: + move_energy: + name: g3 move energy + still_energy: + name: g3 still energy + g4: + move_energy: + name: g4 move energy + still_energy: + name: g4 still energy + g5: + move_energy: + name: g5 move energy + still_energy: + name: g5 still energy + g6: + move_energy: + name: g6 move energy + still_energy: + name: g6 still energy + g7: + move_energy: + name: g7 move energy + still_energy: + name: g7 still energy + g8: + move_energy: + name: g8 move energy + still_energy: + name: g8 still energy + - platform: sen21231 name: "Person Sensor" i2c_id: i2c_bus @@ -1684,6 +1735,8 @@ binary_sensor: name: movement has_still_target: name: still + out_pin_presence_status: + name: out pin presence status pca9685: frequency: 500 @@ -2626,6 +2679,11 @@ switch: id: outlet_switch optimistic: true device_class: outlet + - platform: ld2410 + engineering_mode: + name: "control ld2410 engineering mode" + bluetooth: + name: "control ld2410 bluetooth" fan: - platform: binary @@ -3207,6 +3265,11 @@ text_sensor: tag_name: OPTARIF name: optarif teleinfo_id: myteleinfo + - platform: ld2410 + version: + name: "presenece sensor version" + mac_address: + name: "presenece sensor mac address" sn74hc595: - id: sn74hc595_hub @@ -3311,6 +3374,61 @@ number: step: 1 max_value: 10 optimistic: true + - platform: ld2410 + light_threshold: + name: light threshold + timeout: + name: timeout + max_move_distance_gate: + name: max move distance gate + max_still_distance_gate: + name: max still distance gate + g0: + move_threshold: + name: g0 move threshold + still_threshold: + name: g0 still threshold + g1: + move_threshold: + name: g1 move threshold + still_threshold: + name: g1 still threshold + g2: + move_threshold: + name: g2 move threshold + still_threshold: + name: g2 still threshold + g3: + move_threshold: + name: g3 move threshold + still_threshold: + name: g3 still threshold + g4: + move_threshold: + name: g4 move threshold + still_threshold: + name: g4 still threshold + g5: + move_threshold: + name: g5 move threshold + still_threshold: + name: g5 still threshold + g6: + move_threshold: + name: g6 move threshold + still_threshold: + name: g6 still threshold + g7: + move_threshold: + name: g7 move threshold + still_threshold: + name: g7 still threshold + g8: + move_threshold: + name: g8 move threshold + still_threshold: + name: g8 still threshold + select: - platform: template @@ -3324,6 +3442,15 @@ select: - platform: copy source_id: test_select name: Test Select Copy + - platform: ld2410 + distance_resolution: + name: distance resolution + baud_rate: + name: baud rate + light_function: + name: light function + out_pin_level: + name: out ping level qr_code: - id: homepage_qr @@ -3386,19 +3513,17 @@ button: name: Midea Power Inverse on_press: midea_ac.power_toggle: + - platform: ld2410 + factory_reset: + name: "factory reset" + restart: + name: "restart" + query_params: + name: query params ld2410: id: my_ld2410 uart_id: ld2410_uart - timeout: 150s - max_move_distance: 6m - max_still_distance: 0.75m - g0_move_threshold: 10 - g0_still_threshold: 20 - g2_move_threshold: 20 - g2_still_threshold: 21 - g8_move_threshold: 80 - g8_still_threshold: 81 lcd_menu: display_id: my_lcd_gpio From 532163738e82d0b392302abb74f5f6268cb6fb01 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:49:08 +1200 Subject: [PATCH 062/270] Bump version to 2023.8.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 1442ebde9d..aff72531bb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0b2" +__version__ = "2023.8.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a27e72362ab5045bfc52616fb6d8fe8744e8155c Mon Sep 17 00:00:00 2001 From: Pierre Gordon <16200219+pierlon@users.noreply.github.com> Date: Wed, 16 Aug 2023 20:22:04 -0400 Subject: [PATCH 063/270] Add `libfreetype-dev` Debian package for armv7 Docker builds (#5262) --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e1f3c46a3e..4aaea9da89 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -35,7 +35,8 @@ RUN \ python3-dev=3.9.2-3 \ zlib1g-dev=1:1.2.11.dfsg-2+deb11u2 \ libjpeg-dev=1:2.0.6-4 \ - libcairo2=1.16.0-5; \ + libcairo2=1.16.0-5 \ + libfreetype-dev=2.10.4+dfsg-1+deb11u1; \ fi; \ rm -rf \ /tmp/* \ From cb66ce069e2f8d092095167403489a16fe408466 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Thu, 17 Aug 2023 02:22:37 +0200 Subject: [PATCH 064/270] Add delay before enabling ipv6 (#5256) --- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 995e5e587e..5bfb6bb9a8 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -486,7 +486,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); #if LWIP_IPV6 - WiFi.enableIpV6(); + this->set_timeout(100, [] { WiFi.enableIpV6(); }); #endif /* LWIP_IPV6 */ break; From 7c129a4018305ba2525e2fdca5064412eca0b49a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:23:44 +1200 Subject: [PATCH 065/270] Bump zeroconf from 0.74.0 to 0.80.0 (#5260) 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 6330a0996e..a1f73d930a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.6 esphome-dashboard==20230711.0 aioesphomeapi==15.0.0 -zeroconf==0.74.0 +zeroconf==0.80.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 2aaba1d2b855126cd32d601c1b9a04a37f6ee152 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:04:32 +1200 Subject: [PATCH 066/270] Bump version to 2023.8.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index aff72531bb..ffac22fc58 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0b3" +__version__ = "2023.8.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 7e4ee32b5404d83d6046721e5195b271df3396b1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:33:35 +1200 Subject: [PATCH 067/270] Bump version to 2023.8.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ffac22fc58..f0a6efda94 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0b4" +__version__ = "2023.8.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b566c78f001745cb65a94bbd6e3d4efc02dd3b45 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Thu, 17 Aug 2023 19:57:18 +0000 Subject: [PATCH 068/270] Fix checksum calculation for sml (#5271) --- esphome/components/sml/sml.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp index 87dc25c220..921623d4fd 100644 --- a/esphome/components/sml/sml.cpp +++ b/esphome/components/sml/sml.cpp @@ -100,14 +100,14 @@ bool check_sml_data(const bytes &buffer) { } uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1); - uint16_t crc_calculated = crc16(buffer.data(), buffer.size(), 0x6e23, 0x8408, true, true); + uint16_t crc_calculated = crc16(buffer.data(), buffer.size() - 2, 0x6e23, 0x8408, true, true); crc_calculated = (crc_calculated >> 8) | (crc_calculated << 8); if (crc_received == crc_calculated) { ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25."); return true; } - crc_calculated = crc16(buffer.data(), buffer.size(), 0xed50, 0x8408); + crc_calculated = crc16(buffer.data(), buffer.size() - 2, 0xed50, 0x8408); if (crc_received == crc_calculated) { ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT."); return true; From 0789657fd52aeacda56790504af4a91ea4034440 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:06:21 +1200 Subject: [PATCH 069/270] Change haier from AUTO to HEAT_COOL (#5267) --- esphome/components/haier/climate.py | 2 +- esphome/components/haier/haier_base.cpp | 6 +++--- esphome/components/haier/hon_climate.cpp | 4 ++-- esphome/components/haier/smartair2_climate.cpp | 4 ++-- tests/test3.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index fec39d2967..acb079c822 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -89,7 +89,7 @@ SUPPORTED_SWING_MODES_OPTIONS = { SUPPORTED_CLIMATE_MODES_OPTIONS = { "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available - "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, # always available "COOL": ClimateMode.CLIMATE_MODE_COOL, "HEAT": ClimateMode.CLIMATE_MODE_HEAT, "DRY": ClimateMode.CLIMATE_MODE_DRY, diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 5faee5207b..22899b1a70 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -72,7 +72,7 @@ HaierClimateBase::HaierClimateBase() this->traits_ = climate::ClimateTraits(); this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_AUTO}); + climate::CLIMATE_MODE_HEAT_COOL}); this->traits_.set_supported_fan_modes( {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, @@ -171,8 +171,8 @@ void HaierClimateBase::set_answer_timeout(uint32_t timeout) { void HaierClimateBase::set_supported_modes(const std::set &modes) { this->traits_.set_supported_modes(modes); - this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available - this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available + this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available } void HaierClimateBase::set_supported_presets(const std::set &presets) { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index feb1e019d8..d4944410f7 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -458,7 +458,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { case CLIMATE_MODE_OFF: out_data->ac_power = 0; break; - case CLIMATE_MODE_AUTO: + case CLIMATE_MODE_HEAT_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO; out_data->fan_mode = this->other_modes_fan_speed_; @@ -758,7 +758,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * this->mode = CLIMATE_MODE_FAN_ONLY; break; case (uint8_t) hon_protocol::ConditioningMode::AUTO: - this->mode = CLIMATE_MODE_AUTO; + this->mode = CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 8bee37dadf..f29f840088 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -270,7 +270,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { out_data->ac_power = 0; break; - case CLIMATE_MODE_AUTO: + case CLIMATE_MODE_HEAT_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; out_data->fan_mode = this->other_modes_fan_speed_; @@ -487,7 +487,7 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin this->mode = CLIMATE_MODE_FAN_ONLY; break; case (uint8_t) smartair2_protocol::ConditioningMode::AUTO: - this->mode = CLIMATE_MODE_AUTO; + this->mode = CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/tests/test3.yaml b/tests/test3.yaml index 5bda0afb1b..471b7d97b6 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -957,7 +957,7 @@ climate: temperature_step: 1 °C supported_modes: - 'OFF' - - AUTO + - HEAT_COOL - COOL - HEAT - DRY From 427866420859957be626e86799ac2edae19b333c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:12:42 +1200 Subject: [PATCH 070/270] Bump version to 2023.8.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f0a6efda94..b8c910cd9e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.0" +__version__ = "2023.8.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 5e19a3b892b4580cf5c9457dc227da483892e78e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Aug 2023 19:13:46 +1200 Subject: [PATCH 071/270] Move libcairo to all architectures in docker (#5276) --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4aaea9da89..b942b650cb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,14 +28,14 @@ RUN \ git=1:2.30.2-1+deb11u2 \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ - python3-cffi=1.14.5-1; \ + python3-cffi=1.14.5-1 \ + libcairo2=1.16.0-5; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ build-essential=12.9 \ python3-dev=3.9.2-3 \ zlib1g-dev=1:1.2.11.dfsg-2+deb11u2 \ libjpeg-dev=1:2.0.6-4 \ - libcairo2=1.16.0-5 \ libfreetype-dev=2.10.4+dfsg-1+deb11u1; \ fi; \ rm -rf \ From e600784ebfed1ff2a64e28d6a1bb3979915c3d90 Mon Sep 17 00:00:00 2001 From: mwolter805 <24851651+mwolter805@users.noreply.github.com> Date: Sun, 20 Aug 2023 12:15:45 -0700 Subject: [PATCH 072/270] Resolve offline ESPs in dashboard when using ESPHOME_DASHBOARD_USE_PING=true (#5281) --- esphome/dashboard/dashboard.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index b33cb2df5e..eae004fa09 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -868,9 +868,6 @@ class PingStatusThread(threading.Thread): entries = _list_dashboard_entries() queue = collections.deque() for entry in entries: - if entry.no_mdns is True: - continue - if entry.address is None: PING_RESULT[entry.filename] = None continue From 02a71cb6a758e25a5587b587e205203df372150f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 21 Aug 2023 06:57:02 +1000 Subject: [PATCH 073/270] Align SPI data rates in C++ code with Python (#5284) --- esphome/components/spi/__init__.py | 2 ++ esphome/components/spi/spi.h | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 1528a05734..57a4fa9f4e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -23,7 +23,9 @@ SPI_DATA_RATE_OPTIONS = { 40e6: SPIDataRate.DATA_RATE_40MHZ, 20e6: SPIDataRate.DATA_RATE_20MHZ, 10e6: SPIDataRate.DATA_RATE_10MHZ, + 8e6: SPIDataRate.DATA_RATE_8MHZ, 5e6: SPIDataRate.DATA_RATE_5MHZ, + 4e6: SPIDataRate.DATA_RATE_4MHZ, 2e6: SPIDataRate.DATA_RATE_2MHZ, 1e6: SPIDataRate.DATA_RATE_1MHZ, 2e5: SPIDataRate.DATA_RATE_200KHZ, diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f19518caae..159d117533 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -63,6 +63,7 @@ enum SPIDataRate : uint32_t { DATA_RATE_1MHZ = 1000000, DATA_RATE_2MHZ = 2000000, DATA_RATE_4MHZ = 4000000, + DATA_RATE_5MHZ = 5000000, DATA_RATE_8MHZ = 8000000, DATA_RATE_10MHZ = 10000000, DATA_RATE_20MHZ = 20000000, From e44a60e8141c90cbff263bdb0c3c6e6a7027fc22 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:16:14 +1200 Subject: [PATCH 074/270] Change htu21d sensors from required to optional (#5285) --- esphome/components/htu21d/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 37422f0329..2ed318f1c9 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -23,13 +23,13 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(HTU21DComponent), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( + 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.Required(CONF_HUMIDITY): sensor.sensor_schema( + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=1, device_class=DEVICE_CLASS_HUMIDITY, From d2bccbe8ac0d0ca62024f1faa7d22d7ae47907f5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:17:22 +1000 Subject: [PATCH 075/270] Reserve keyword "clock" (#5279) --- esphome/config_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index cf0b1d3aca..3720757828 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -125,6 +125,7 @@ RESERVED_IDS = [ "char16_t", "char32_t", "class", + "clock", "compl", "concept", "const", From 9fb8e9edef1ee65bf9b615ab69a5f61bcf2c4815 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Aug 2023 12:33:49 +1200 Subject: [PATCH 076/270] Bump version to 2023.8.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index b8c910cd9e..c22dde6986 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.1" +__version__ = "2023.8.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2cabe59c228d1863cf3616fb6edb429c9909aef6 Mon Sep 17 00:00:00 2001 From: Sebastian Rasor <92653912+sebastianrasor@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:01:34 -0500 Subject: [PATCH 077/270] Introduce cv.temperature_delta and fix problematic thermostat configuration behavior (#5297) --- esphome/components/thermostat/climate.py | 14 +++++++------- esphome/config_validation.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 9a57f6a337..cca46609db 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -591,11 +591,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( CONF_SET_POINT_MINIMUM_DIFFERENTIAL, default=0.5 - ): cv.temperature, - cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature, - cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature, - cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature, - cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature, + ): cv.temperature_delta, + cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature_delta, + cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature_delta, + cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature_delta, + cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature_delta, cv.Optional(CONF_MAX_COOLING_RUN_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MAX_HEATING_RUN_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MIN_COOLING_OFF_TIME): cv.positive_time_period_seconds, @@ -608,8 +608,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MIN_HEATING_OFF_TIME): cv.positive_time_period_seconds, cv.Optional(CONF_MIN_HEATING_RUN_TIME): cv.positive_time_period_seconds, cv.Required(CONF_MIN_IDLE_TIME): cv.positive_time_period_seconds, - cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature, - cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature, + cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature_delta, + cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature_delta, cv.Optional( CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, default=False ): cv.boolean, diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 3720757828..ed87e98078 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -929,6 +929,27 @@ def temperature(value): raise err +def temperature_delta(value): + err = None + try: + return _temperature_c(value) + except Invalid as orig_err: + err = orig_err + + try: + return _temperature_k(value) + except Invalid: + pass + + try: + fahrenheit = _temperature_f(value) + return fahrenheit * (5 / 9) + except Invalid: + pass + + raise err + + _color_temperature_mireds = float_with_unit("Color Temperature", r"(mireds|Mireds)") _color_temperature_kelvin = float_with_unit("Color Temperature", r"(K|Kelvin)") From c146712b1644deba320195530330c2892793a90f Mon Sep 17 00:00:00 2001 From: luka6000 Date: Fri, 1 Sep 2023 03:20:21 +0200 Subject: [PATCH 078/270] fix to PR # 3887 MQTT connection not using discovery: false (#5275) --- esphome/components/mqtt/mqtt_client.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index d3f759c072..1d804170f6 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -66,25 +66,28 @@ void MQTTClientComponent::setup() { } #endif - this->subscribe( - "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, - 2); + if (this->is_discovery_enabled()) { + this->subscribe( + "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, + 2); - std::string topic = "esphome/ping/"; - topic.append(App.get_name()); - this->subscribe( - topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + std::string topic = "esphome/ping/"; + topic.append(App.get_name()); + this->subscribe( + topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + } this->last_connected_ = millis(); this->start_dnslookup_(); } void MQTTClientComponent::send_device_info_() { - if (!this->is_connected()) { + if (!this->is_connected() or !this->is_discovery_enabled()) { return; } std::string topic = "esphome/discover/"; topic.append(App.get_name()); + this->publish_json( topic, [](JsonObject root) { From 3f8bad3ed1748ed1eef8c378c079466054650bbb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:41:52 +1200 Subject: [PATCH 079/270] Attempt to fix secret blurring (#5326) --- esphome/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index ca5fc1c008..697adc03a3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -371,7 +371,7 @@ def command_config(args, config): # add the console decoration so the front-end can hide the secrets if not args.show_secrets: output = re.sub( - r"(password|key|psk|ssid)\:\s(.*)", r"\1: \\033[5m\2\\033[6m", output + r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output ) safe_print(output) _LOGGER.info("Configuration is valid!") From 619787e6d2f4dbf3c860d3b70657e1d62a3cf7a6 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Fri, 1 Sep 2023 16:55:59 -0400 Subject: [PATCH 080/270] Bugfix: disable channels after IO if multiple tca9548a I2C multiplexers are configured (#5317) --- esphome/components/tca9548a/tca9548a.cpp | 27 ++++++++++++++---------- esphome/components/tca9548a/tca9548a.h | 4 +++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index caa3dd0655..770fd5e47c 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -7,23 +7,27 @@ namespace tca9548a { static const char *const TAG = "tca9548a"; i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) { - auto err = parent_->switch_to_channel(channel_); + auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->readv(address, buffers, cnt); + err = this->parent_->bus_->readv(address, buffers, cnt); + this->parent_->disable_all_channels(); + return err; } i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { - auto err = parent_->switch_to_channel(channel_); + auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->writev(address, buffers, cnt, stop); + err = this->parent_->bus_->writev(address, buffers, cnt, stop); + this->parent_->disable_all_channels(); + return err; } void TCA9548AComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { - ESP_LOGI(TAG, "TCA9548A failed"); + ESP_LOGE(TAG, "TCA9548A failed"); this->mark_failed(); return; } @@ -37,15 +41,16 @@ void TCA9548AComponent::dump_config() { i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) { if (this->is_failed()) return i2c::ERROR_NOT_INITIALIZED; - if (current_channel_ == channel) - return i2c::ERROR_OK; uint8_t channel_val = 1 << channel; - auto err = this->write(&channel_val, 1); - if (err == i2c::ERROR_OK) { - current_channel_ = channel; + return this->write(&channel_val, 1); +} + +void TCA9548AComponent::disable_all_channels() { + if (this->write(&TCA9548A_DISABLE_CHANNELS_COMMAND, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to disable all channels."); + this->status_set_error(); // couldn't disable channels, set error status } - return err; } } // namespace tca9548a diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 02553f8cd0..08f1674d11 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -6,6 +6,8 @@ namespace esphome { namespace tca9548a { +static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00; + class TCA9548AComponent; class TCA9548AChannel : public i2c::I2CBus { public: @@ -28,10 +30,10 @@ class TCA9548AComponent : public Component, public i2c::I2CDevice { void update(); i2c::ErrorCode switch_to_channel(uint8_t channel); + void disable_all_channels(); protected: friend class TCA9548AChannel; - uint8_t current_channel_ = 255; }; } // namespace tca9548a } // namespace esphome From 55df88d7ae14db3da6d5d3a93189314a0045c0cd Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:54:03 +0000 Subject: [PATCH 081/270] Fix checksum calculation for pipsolar (#5299) --- esphome/components/pipsolar/pipsolar.cpp | 20 ++++++++++++++++---- esphome/components/pipsolar/pipsolar.h | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 62e4fbd341..2cd1aeba44 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -769,7 +769,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { uint8_t Pipsolar::check_incoming_crc_() { uint16_t crc16; - crc16 = crc16be(read_buffer_, read_pos_ - 3); + crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); ESP_LOGD(TAG, "checking crc on incoming message"); if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { @@ -798,7 +798,7 @@ uint8_t Pipsolar::send_next_command_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = crc16be(byte_command, length); + crc16 = this->pipsolar_crc_(byte_command, length); this->write_str(command); // checksum this->write(((uint8_t) ((crc16) >> 8))); // highbyte @@ -825,8 +825,8 @@ void Pipsolar::send_next_poll_() { this->command_start_millis_ = millis(); this->empty_uart_buffer_(); this->read_pos_ = 0; - crc16 = crc16be(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); + crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); this->write_array(this->used_polling_commands_[this->last_polling_command_].command, this->used_polling_commands_[this->last_polling_command_].length); // checksum @@ -893,5 +893,17 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll } } +uint16_t Pipsolar::pipsolar_crc_(uint8_t *msg, uint8_t len) { + uint16_t crc = crc16be(msg, len); + uint8_t crc_low = crc & 0xff; + uint8_t crc_high = crc >> 8; + if (crc_low == 0x28 || crc_low == 0x0d || crc_low == 0x0a) + crc_low++; + if (crc_high == 0x28 || crc_high == 0x0d || crc_high == 0x0a) + crc_high++; + crc = (crc_high << 8) | crc_low; + return crc; +} + } // namespace pipsolar } // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 65fd3c670d..f20f44f095 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -193,7 +193,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { void empty_uart_buffer_(); uint8_t check_incoming_crc_(); uint8_t check_incoming_length_(uint8_t length); - uint16_t cal_crc_half_(uint8_t *msg, uint8_t len); + uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len); uint8_t send_next_command_(); void send_next_poll_(); void queue_command_(const char *command, uint8_t length); From 150c9b5fa3f43ce28391e841764828a5d8b7d7ae Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:14:19 +1200 Subject: [PATCH 082/270] Bump version to 2023.8.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index c22dde6986..bd4c48a704 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.8.2" +__version__ = "2023.8.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 68a2c45edf01e426f26350693d91669696481ab1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:05:06 +1200 Subject: [PATCH 083/270] Bump version to 2023.9.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index bbc6e71885..cbb31e0ec6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0-dev" +__version__ = "2023.9.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b3ca71c6fb0d3f68228e522824c272d671292089 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 13 Sep 2023 18:13:55 -0500 Subject: [PATCH 084/270] Add patch to apt install (#5389) --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index bf64897af7..a0bb007641 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,8 @@ RUN \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ python3-cffi=1.14.5-1 \ - libcairo2=1.16.0-5; \ + libcairo2=1.16.0-5 \ + patch=2.7.6-7; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ build-essential=12.9 \ From ec20778d83dd7bf10d8f32c49ff0dfd7102a0ade Mon Sep 17 00:00:00 2001 From: phoenixswiss <52887628+phoenixswiss@users.noreply.github.com> Date: Thu, 14 Sep 2023 08:20:21 +0200 Subject: [PATCH 085/270] Fix Waveshare 7.5v2 epaper screens are always powered on (#5283) --- .../waveshare_epaper/waveshare_epaper.cpp | 66 ++++++++++++++++--- .../waveshare_epaper/waveshare_epaper.h | 4 ++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 73c2680add..f52808d295 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1561,6 +1561,23 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +bool WaveshareEPaper7P5InV2::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_LOGE(TAG, "Timeout while displaying image!"); + return false; + } + App.feed_wdt(); + delay(10); + } + return true; +} void WaveshareEPaper7P5InV2::initialize() { // COMMAND POWER SETTING this->command(0x01); @@ -1568,10 +1585,21 @@ void WaveshareEPaper7P5InV2::initialize() { this->data(0x07); this->data(0x3f); this->data(0x3f); - this->command(0x04); + + // We don't want the display to be powered at this point delay(100); // NOLINT this->wait_until_idle_(); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // COMMAND PANEL SETTING this->command(0x00); this->data(0x1F); @@ -1582,19 +1610,30 @@ void WaveshareEPaper7P5InV2::initialize() { this->data(0x20); this->data(0x01); this->data(0xE0); - // COMMAND ...? + + // COMMAND DUAL SPI MM_EN, DUSPI_EN 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); + + // COMMAND POWER DRIVER HAT DOWN + // This command will turn off booster, controller, source driver, gate driver, VCOM, and + // temperature sensor, but register data will be kept until VDD turned OFF or Deep Sleep Mode. + // Source/Gate/Border/VCOM will be released to floating. + this->command(0x02); } void HOT WaveshareEPaper7P5InV2::display() { uint32_t buf_len = this->get_buffer_length_(); + + // COMMAND POWER ON + ESP_LOGI(TAG, "Power on the display and hat"); + + // This command will turn on booster, controller, regulators, and temperature sensor will be + // activated for one-time sensing before enabling booster. When all voltages are ready, the + // BUSY_N signal will return to high. + this->command(0x04); + delay(200); // NOLINT + this->wait_until_idle_(); + // COMMAND DATA START TRANSMISSION NEW DATA this->command(0x13); delay(2); @@ -1602,14 +1641,23 @@ void HOT WaveshareEPaper7P5InV2::display() { this->data(~(this->buffer_[i])); } + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND DISPLAY REFRESH this->command(0x12); delay(100); // NOLINT this->wait_until_idle_(); + + ESP_LOGV(TAG, "Before command(0x02) (>> power off)"); + this->command(0x02); + this->wait_until_idle_(); + ESP_LOGV(TAG, "After command(0x02) (>> power off)"); } int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +uint32_t WaveshareEPaper7P5InV2::idle_timeout_() { return 10000; } void WaveshareEPaper7P5InV2::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 7.5inV2rev2"); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 315af9ea82..b3325d69eb 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -472,6 +472,8 @@ class WaveshareEPaper7P5InBC : public WaveshareEPaper { class WaveshareEPaper7P5InV2 : public WaveshareEPaper { public: + bool wait_until_idle_(); + void initialize() override; void display() override; @@ -491,6 +493,8 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_width_internal() override; int get_height_internal() override; + + uint32_t idle_timeout_() override; }; class WaveshareEPaper7P5InV2alt : public WaveshareEPaper7P5InV2 { From d76f18b4f24fe8cd554e2e9ed2f24bcd601e63ae Mon Sep 17 00:00:00 2001 From: rmmacias <46213351+rmmacias@users.noreply.github.com> Date: Sun, 17 Sep 2023 07:18:51 +0200 Subject: [PATCH 086/270] Update radon_eye_listener.cpp (#5401) New devices identifiers do not star by the hardcoded string. FR:RE222 is the 8-char length string of my devices bought in 2023. This proposal aims at solve the topic by making the detection track devices starting only by FR:R --- esphome/components/radon_eye_ble/radon_eye_listener.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index b10986c9cb..340322c188 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "radon_eye_ble"; bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (not device.get_name().empty()) { - if (device.get_name().rfind("FR:R20:SN", 0) == 0) { + if (device.get_name().rfind("FR:R", 0) == 0) { // This is an RD200, I think ESP_LOGD(TAG, "Found Radon Eye RD200 device Name: %s (MAC: %s)", device.get_name().c_str(), device.address_str().c_str()); From 4622ef770d8a62232eddc8a1315699f91ef1952c Mon Sep 17 00:00:00 2001 From: Trevor North Date: Sun, 17 Sep 2023 06:20:31 +0100 Subject: [PATCH 087/270] Add shelly-dimmer-stm32 51.7 to known versions (#5400) This version removes support for no-neutral setups in favor of fixing flickering some users have experienced. --- esphome/components/shelly_dimmer/light.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index 467a3c3531..5bdb54baf5 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -57,6 +57,10 @@ KNOWN_FIRMWARE = { "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin", "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36", ), + "51.7": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.7/shelly-dimmer-stm32_v51.7.bin", + "7a20f1c967c469917368a79bc56498009045237080408cef7190743e08031889", + ), } From 2fa7f8c5112333a65c1a5c2de85cc28687f0d1c7 Mon Sep 17 00:00:00 2001 From: Philipp Helo Rehs Date: Sun, 17 Sep 2023 07:30:52 +0200 Subject: [PATCH 088/270] Add E-Trailer Gaslevel support to Mopeka Std Check (#5397) * Add E-Trailer Gaslevel support to Mopeka Std Check Signed-off-by: Philipp Helo Rehs * fix format --------- Signed-off-by: Philipp Helo Rehs Co-authored-by: Philipp Helo Rehs --- esphome/components/mopeka_std_check/mopeka_std_check.cpp | 3 ++- esphome/components/mopeka_std_check/mopeka_std_check.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 67e749c68b..9dd1718cb2 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -71,7 +71,8 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const auto *mopeka_data = (const mopeka_std_package *) manu_data.data.data(); const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; - if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL) { + if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && + static_cast(hardware_id) != ETRAILER) { ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index e4d81afbd7..ee588c8e5f 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -14,6 +14,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + ETRAILER = 0x46, }; // 4 values in one struct so it aligns to 8 byte. One `mopeka_std_values` is 40 bit long. From e8862620556008834356669be14f93a2b3395d62 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 20 Sep 2023 14:20:54 -0700 Subject: [PATCH 089/270] fix disabled wifi power on 8266 (#5409) Co-authored-by: Samuel Sieb --- esphome/components/wifi/wifi_component_esp8266.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 1afa439567..6e7c491967 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -98,6 +98,7 @@ bool WiFiComponent::wifi_apply_power_save_() { power_save = NONE_SLEEP_T; break; } + wifi_fpm_auto_sleep_set_in_null_mode(1); return wifi_set_sleep_type(power_save); } From e55636ed521bc2feda4ce690c9b6eba50a8cf1cb Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 20 Sep 2023 14:25:16 -0700 Subject: [PATCH 090/270] fix handling of web server version (#5405) Co-authored-by: Samuel Sieb --- esphome/components/web_server/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index c6d9c31e93..966c978836 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -59,7 +59,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(WebServer), cv.Optional(CONF_PORT, default=80): cv.port, - cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2), + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), cv.Optional(CONF_CSS_URL): cv.string, cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL): cv.string, From 8f1ce8c7f7a6a86b0e9d98e9324f9ab6ff0e41e2 Mon Sep 17 00:00:00 2001 From: Joris S <100357138+Jorre05@users.noreply.github.com> Date: Wed, 20 Sep 2023 23:28:03 +0200 Subject: [PATCH 091/270] Climate preset fix (#5407) --- esphome/components/thermostat/thermostat_climate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 51da663a0c..386e13dc37 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -986,6 +986,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; assert(trig != nullptr); + this->preset = preset; trig->trigger(); this->refresh(); @@ -1010,6 +1011,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; assert(trig != nullptr); + this->custom_preset = custom_preset; trig->trigger(); this->refresh(); From 41c829fa32ec2b13eab7647ed806575e0f5897a4 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 20 Sep 2023 16:30:22 -0500 Subject: [PATCH 092/270] Remove Wi-Fi dependency from Midea component (#5394) --- esphome/components/midea/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 80b1461576..074ab8abb2 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -35,7 +35,7 @@ from esphome.components.climate import ( ) CODEOWNERS = ["@dudanov"] -DEPENDENCIES = ["climate", "uart", "wifi"] +DEPENDENCIES = ["climate", "uart"] AUTO_LOAD = ["sensor"] CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_POWER_USAGE = "power_usage" From 7ebe6a5894da94d1dd9c4c2e711e2c0ce8a087d3 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Wed, 20 Sep 2023 18:02:29 -0400 Subject: [PATCH 093/270] http_request: Cleanups and safety improvements (#5360) --- .../components/http_request/http_request.h | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 0958c07683..b885de18e6 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -80,8 +80,6 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *, method) TEMPLATABLE_VALUE(std::string, body) - TEMPLATABLE_VALUE(const char *, useragent) - TEMPLATABLE_VALUE(uint16_t, timeout) void add_header(const char *key, TemplatableValue value) { this->headers_.insert({key, value}); } @@ -105,25 +103,18 @@ template class HttpRequestSendAction : public Action { auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); this->parent_->set_body(json::build_json(f)); } - if (this->useragent_.has_value()) { - this->parent_->set_useragent(this->useragent_.value(x...)); - } - if (this->timeout_.has_value()) { - this->parent_->set_timeout(this->timeout_.value(x...)); - } - if (!this->headers_.empty()) { - std::list
headers; - for (const auto &item : this->headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - headers.push_back(header); - } - this->parent_->set_headers(headers); + std::list
headers; + for (const auto &item : this->headers_) { + auto val = item.second; + Header header; + header.name = item.first; + header.value = val.value(x...); + headers.push_back(header); } + this->parent_->set_headers(headers); this->parent_->send(this->response_triggers_); this->parent_->close(); + this->parent_->set_body(""); } protected: From 807c47a076e202d681affe65414b49c40a0015d5 Mon Sep 17 00:00:00 2001 From: Trent Houliston Date: Thu, 21 Sep 2023 08:04:03 +1000 Subject: [PATCH 094/270] Make the pulse meter timeout on startup when no pulses are received (#5388) --- .../pulse_meter/pulse_meter_sensor.cpp | 35 +++++++++++++------ .../pulse_meter/pulse_meter_sensor.h | 3 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 7eef18e5e0..be5fad6fe5 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -11,6 +11,9 @@ void PulseMeterSensor::setup() { this->pin_->setup(); this->isr_pin_ = pin_->to_isr(); + // Set the last processed edge to now for the first timeout + this->last_processed_edge_us_ = micros(); + if (this->filter_mode_ == FILTER_EDGE) { this->pin_->attach_interrupt(PulseMeterSensor::edge_intr, this, gpio::INTERRUPT_RISING_EDGE); } else if (this->filter_mode_ == FILTER_PULSE) { @@ -38,12 +41,16 @@ void PulseMeterSensor::loop() { } // We need to detect at least two edges to have a valid pulse width - if (!this->initialized_) { - this->initialized_ = true; - } else { - uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; - float pulse_width_us = delta_us / float(this->get_->count_); - this->publish_state((60.0f * 1000000.0f) / pulse_width_us); + switch (this->meter_state_) { + case MeterState::INITIAL: + case MeterState::TIMED_OUT: { + this->meter_state_ = MeterState::RUNNING; + } break; + case MeterState::RUNNING: { + uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; + float pulse_width_us = delta_us / float(this->get_->count_); + this->publish_state((60.0f * 1000000.0f) / pulse_width_us); + } break; } this->last_processed_edge_us_ = this->get_->last_detected_edge_us_; @@ -53,10 +60,18 @@ void PulseMeterSensor::loop() { const uint32_t now = micros(); const uint32_t time_since_valid_edge_us = now - this->last_processed_edge_us_; - if (this->initialized_ && time_since_valid_edge_us > this->timeout_us_) { - ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); - this->initialized_ = false; - this->publish_state(0.0f); + switch (this->meter_state_) { + // Running and initial states can timeout + case MeterState::INITIAL: + case MeterState::RUNNING: { + if (time_since_valid_edge_us > this->timeout_us_) { + this->meter_state_ = MeterState::TIMED_OUT; + ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); + this->publish_state(0.0f); + } + } break; + default: + break; } } } diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index ddd42c2ed5..f376ea48a5 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -38,7 +38,8 @@ class PulseMeterSensor : public sensor::Sensor, public Component { InternalFilterMode filter_mode_{FILTER_EDGE}; // Variables used in the loop - bool initialized_ = false; + enum class MeterState { INITIAL, RUNNING, TIMED_OUT }; + MeterState meter_state_ = MeterState::INITIAL; uint32_t total_pulses_ = 0; uint32_t last_processed_edge_us_ = 0; From d7e267eca53e47beec3496e9458ccce151d25ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 21 Sep 2023 00:09:23 +0200 Subject: [PATCH 095/270] Wizard: fix colored text in input prompts (#5313) --- esphome/util.py | 14 ++++++++++---- esphome/wizard.py | 18 ++++++++++-------- tests/unit_tests/test_wizard.py | 12 ++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/esphome/util.py b/esphome/util.py index 0d60212f50..480618aca0 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -57,7 +57,7 @@ class SimpleRegistry(dict): return decorator -def safe_print(message=""): +def safe_print(message="", end="\n"): from esphome.core import CORE if CORE.dashboard: @@ -67,20 +67,26 @@ def safe_print(message=""): pass try: - print(message) + print(message, end=end) return except UnicodeEncodeError: pass try: - print(message.encode("utf-8", "backslashreplace")) + print(message.encode("utf-8", "backslashreplace"), end=end) except UnicodeEncodeError: try: - print(message.encode("ascii", "backslashreplace")) + print(message.encode("ascii", "backslashreplace"), end=end) except UnicodeEncodeError: print("Cannot print line because of invalid locale!") +def safe_input(prompt=""): + if prompt: + safe_print(prompt, end="") + return input() + + def shlex_quote(s): if not s: return "''" diff --git a/esphome/wizard.py b/esphome/wizard.py index aa05e513a7..1308338ad0 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -11,7 +11,7 @@ from esphome.core import CORE from esphome.helpers import get_bool_env, write_file from esphome.log import Fore, color from esphome.storage_json import StorageJSON, ext_storage_path -from esphome.util import safe_print +from esphome.util import safe_input, safe_print CORE_BIG = r""" _____ ____ _____ ______ / ____/ __ \| __ \| ____| @@ -252,7 +252,7 @@ def safe_print_step(step, big): def default_input(text, default): safe_print() safe_print(f"Press ENTER for default ({default})") - return input(text.format(default)) or default + return safe_input(text.format(default)) or default # From https://stackoverflow.com/a/518232/8924614 @@ -306,7 +306,7 @@ def wizard(path): ) safe_print() sleep(1) - name = input(color(Fore.BOLD_WHITE, "(name): ")) + name = safe_input(color(Fore.BOLD_WHITE, "(name): ")) while True: try: @@ -343,7 +343,9 @@ def wizard(path): while True: sleep(0.5) safe_print() - platform = input(color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")) + platform = safe_input( + color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ") + ) try: platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper()) break @@ -397,7 +399,7 @@ def wizard(path): boards.append(board_id) while True: - board = input(color(Fore.BOLD_WHITE, "(board): ")) + board = safe_input(color(Fore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break @@ -423,7 +425,7 @@ def wizard(path): sleep(1.5) safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".") while True: - ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) + ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break @@ -449,7 +451,7 @@ def wizard(path): safe_print() safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"") sleep(0.5) - psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) + psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): ")) safe_print( "Perfect! WiFi is now set up (you can create static IPs and so on later)." ) @@ -466,7 +468,7 @@ def wizard(path): safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = input(color(Fore.BOLD_WHITE, "(password): ")) + password = safe_input(color(Fore.BOLD_WHITE, "(password): ")) if not wizard_write( path=path, diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 8bbce08ae5..46700a3ba8 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -319,7 +319,7 @@ def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answ config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -341,7 +341,7 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -371,7 +371,7 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -394,7 +394,7 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -416,7 +416,7 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) @@ -438,7 +438,7 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): config_file = tmpdir.join("test.yaml") input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) - monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) monkeypatch.setattr(wz, "sleep", lambda _: 0) monkeypatch.setattr(wz, "wizard_write", MagicMock()) From 5b46088ae4089fa8f8c02f3a54a86d2f9da0bb24 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 20 Sep 2023 15:26:36 -0700 Subject: [PATCH 096/270] support keypads with pulldowns (#5404) Co-authored-by: Samuel Sieb --- esphome/components/matrix_keypad/__init__.py | 4 ++++ .../components/matrix_keypad/matrix_keypad.cpp | 17 +++++++++++------ .../components/matrix_keypad/matrix_keypad.h | 2 ++ tests/test5.yaml | 1 + 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 1c549007b9..5250a45732 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -21,6 +21,7 @@ CONF_COLUMNS = "columns" CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" CONF_HAS_DIODES = "has_diodes" +CONF_HAS_PULLDOWNS = "has_pulldowns" def check_keys(obj): @@ -45,6 +46,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_KEYS): cv.string, cv.Optional(CONF_DEBOUNCE_TIME, default=1): cv.int_range(min=1, max=100), cv.Optional(CONF_HAS_DIODES): cv.boolean, + cv.Optional(CONF_HAS_PULLDOWNS): cv.boolean, } ), check_keys, @@ -69,3 +71,5 @@ async def to_code(config): cg.add(var.set_debounce_time(config[CONF_DEBOUNCE_TIME])) if CONF_HAS_DIODES in config: cg.add(var.set_has_diodes(config[CONF_HAS_DIODES])) + if CONF_HAS_PULLDOWNS in config: + cg.add(var.set_has_pulldowns(config[CONF_HAS_PULLDOWNS])) diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 4f8962a782..902e574846 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -11,11 +11,16 @@ void MatrixKeypad::setup() { if (!has_diodes_) { pin->pin_mode(gpio::FLAG_INPUT); } else { - pin->digital_write(true); + pin->digital_write(!has_pulldowns_); + } + } + for (auto *pin : this->columns_) { + if (has_pulldowns_) { + pin->pin_mode(gpio::FLAG_INPUT); + } else { + pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); } } - for (auto *pin : this->columns_) - pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); } void MatrixKeypad::loop() { @@ -28,9 +33,9 @@ void MatrixKeypad::loop() { for (auto *row : this->rows_) { if (!has_diodes_) row->pin_mode(gpio::FLAG_OUTPUT); - row->digital_write(false); + row->digital_write(has_pulldowns_); for (auto *col : this->columns_) { - if (!col->digital_read()) { + if (col->digital_read() == has_pulldowns_) { if (key != -1) { error = true; } else { @@ -39,7 +44,7 @@ void MatrixKeypad::loop() { } pos++; } - row->digital_write(true); + row->digital_write(!has_pulldowns_); if (!has_diodes_) row->pin_mode(gpio::FLAG_INPUT); } diff --git a/esphome/components/matrix_keypad/matrix_keypad.h b/esphome/components/matrix_keypad/matrix_keypad.h index 9f5942be9a..d506040b7c 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.h +++ b/esphome/components/matrix_keypad/matrix_keypad.h @@ -28,6 +28,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { void set_keys(std::string keys) { keys_ = std::move(keys); }; void set_debounce_time(int debounce_time) { debounce_time_ = debounce_time; }; void set_has_diodes(int has_diodes) { has_diodes_ = has_diodes; }; + void set_has_pulldowns(int has_pulldowns) { has_pulldowns_ = has_pulldowns; }; void register_listener(MatrixKeypadListener *listener); @@ -37,6 +38,7 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component { std::string keys_; int debounce_time_ = 0; bool has_diodes_{false}; + bool has_pulldowns_{false}; int pressed_key_ = -1; std::vector listeners_{}; diff --git a/tests/test5.yaml b/tests/test5.yaml index 274570aad6..5727d30e61 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -667,6 +667,7 @@ matrix_keypad: - pin: 17 - pin: 16 keys: "1234" + has_pulldowns: true key_collector: - id: reader From 90835ab917ec52e2e9de464477f9e24988392be0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Sep 2023 10:35:38 +1200 Subject: [PATCH 097/270] Bump version to 2023.9.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index cbb31e0ec6..8a10776c4a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0b1" +__version__ = "2023.9.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b07a038bc8f17afe9aa6bdde6b8ec929b1120945 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:15:50 +1000 Subject: [PATCH 098/270] Fix SPI inverted clock on ESP8266 (#5416) --- esphome/components/spi/spi.h | 6 ++---- esphome/components/spi/spi_arduino.cpp | 5 +++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 2761c2d604..56aa746fc9 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -351,6 +351,7 @@ class SPIClient { : bit_order_(bit_order), mode_(mode), data_rate_(data_rate) {} virtual void spi_setup() { + esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000)); this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); } @@ -398,10 +399,7 @@ class SPIDevice : public SPIClient { void set_data_rate(uint32_t data_rate) { this->data_rate_ = data_rate; } - void set_bit_order(SPIBitOrder order) { - this->bit_order_ = order; - esph_log_d("spi.h", "bit order set to %d", order); - } + void set_bit_order(SPIBitOrder order) { this->bit_order_ = order; } void set_mode(SPIMode mode) { this->mode_ = mode; } diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 40ed9e6062..2e6b2d6064 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -15,6 +15,11 @@ class SPIDelegateHw : public SPIDelegate { void begin_transaction() override { #ifdef USE_RP2040 SPISettings const settings(this->data_rate_, static_cast(this->bit_order_), this->mode_); +#elif defined(ESP8266) + // Arduino ESP8266 library has mangled values for SPI modes :-( + auto mode = (this->mode_ & 0x01) + ((this->mode_ & 0x02) << 3); + ESP_LOGV(TAG, "8266 mangled SPI mode 0x%X", mode); + SPISettings const settings(this->data_rate_, this->bit_order_, mode); #else SPISettings const settings(this->data_rate_, this->bit_order_, this->mode_); #endif From a42788812e5136e8235c76df2bb0526d1436a417 Mon Sep 17 00:00:00 2001 From: Ilia Sotnikov Date: Sun, 24 Sep 2023 12:44:55 +0300 Subject: [PATCH 099/270] [RP2040W] Fix WiFi bootloop upon LibreTiny support (#5414) --- esphome/components/rp2040/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b31192f73f..5d8608c44d 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -152,6 +152,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add(rp2040_ns.setup_preferences()) + # Allow LDF to properly discover dependency including those in preprocessor + # conditionals + cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) From 33e2aa341e63a925123a27b758769a70ff012195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Sun, 24 Sep 2023 23:15:28 +0200 Subject: [PATCH 100/270] dallas: limit addresses to 64 bits (#5413) --- esphome/components/dallas/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 9288f0a3a6..c6ebda62c8 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.All( ).extend( { cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), - cv.Optional(CONF_ADDRESS): cv.hex_int, + cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, cv.Optional(CONF_INDEX): cv.positive_int, cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), } From 0aeebdd2896cd5cf6d262fea41269c1dc21c3307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 10:25:51 +1300 Subject: [PATCH 101/270] Bump zeroconf from 0.108.0 to 0.112.0 (#5392) 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 19c05bf8f7..63199680cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20230904.0 aioesphomeapi==15.0.0 -zeroconf==0.108.0 +zeroconf==0.112.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 5f5ee9c9204bda8b38d86eac065781a87b2ea27a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:10:35 +1300 Subject: [PATCH 102/270] Bump version to 2023.9.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8a10776c4a..f5319adfa7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0b2" +__version__ = "2023.9.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b30bab8c1b310b5cb03bde7f86d8d8fc2e6ae3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 26 Sep 2023 23:23:21 +0200 Subject: [PATCH 103/270] LibreTiny: enable MQTT, bump to v1.4.1 (#5419) --- esphome/components/libretiny/__init__.py | 2 +- esphome/components/mqtt/__init__.py | 8 +- .../components/mqtt/mqtt_backend_libretiny.h | 74 +++++++++++++++++++ esphome/components/mqtt/mqtt_client.cpp | 5 +- esphome/components/mqtt/mqtt_client.h | 4 + 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 esphome/components/mqtt/mqtt_backend_libretiny.h diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 3c1c0ac3f0..b01d342a87 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -170,7 +170,7 @@ def _notify_old_style(config): ARDUINO_VERSIONS = { "dev": (cv.Version(0, 0, 0), "https://github.com/libretiny-eu/libretiny.git"), "latest": (cv.Version(0, 0, 0), None), - "recommended": (cv.Version(1, 4, 0), None), + "recommended": (cv.Version(1, 4, 1), None), } diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 9df2067832..10ae8ac40d 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -250,7 +250,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_on(["esp32", "esp8266"]), + cv.only_on(["esp32", "esp8266", "bk72xx"]), ) @@ -271,10 +271,10 @@ def exp_mqtt_message(config): async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Add required libraries for ESP8266 - if CORE.is_esp8266: + # Add required libraries for ESP8266 and LibreTiny + if CORE.is_esp8266 or CORE.is_libretiny: # https://github.com/heman/async-mqtt-client/blob/master/library.json - cg.add_library("heman/AsyncMqttClient-esphome", "1.0.0") + cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h new file mode 100644 index 0000000000..5373a1926a --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -0,0 +1,74 @@ +#pragma once + +#ifdef USE_LIBRETINY + +#include "mqtt_backend.h" +#include + +namespace esphome { +namespace mqtt { + +class MQTTBackendLibreTiny final : public MQTTBackend { + public: + void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); } + void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); } + void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); } + void set_credentials(const char *username, const char *password) final { + mqtt_client_.setCredentials(username, password); + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + mqtt_client_.setWill(topic, qos, retain, payload); + } + void set_server(network::IPAddress ip, uint16_t port) final { + mqtt_client_.setServer(IPAddress(static_cast(ip)), port); + } + void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } +#if ASYNC_TCP_SSL_ENABLED + void set_secure(bool secure) { mqtt_client.setSecure(secure); } + void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } +#endif + + void set_on_connect(std::function &&callback) final { + this->mqtt_client_.onConnect(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { + // int based enum so casting isn't a problem + callback(static_cast(reason)); + }; + this->mqtt_client_.onDisconnect(std::move(async_callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->mqtt_client_.onSubscribe(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->mqtt_client_.onUnsubscribe(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + auto async_callback = [callback](const char *topic, const char *payload, + AsyncMqttClientMessageProperties async_properties, size_t len, size_t index, + size_t total) { callback(topic, payload, len, index, total); }; + mqtt_client_.onMessage(std::move(async_callback)); + } + void set_on_publish(std::function &&callback) final { + this->mqtt_client_.onPublish(std::move(callback)); + } + + bool connected() const final { return mqtt_client_.connected(); } + void connect() final { mqtt_client_.connect(); } + void disconnect() final { mqtt_client_.disconnect(true); } + bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; } + bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; } + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { + return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0; + } + using MQTTBackend::publish; + + protected: + AsyncMqttClient mqtt_client_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // defined(USE_LIBRETINY) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 0c6da42328..fd5e13ecc7 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -106,6 +106,9 @@ void MQTTClientComponent::send_device_info_() { #ifdef USE_ESP32 root["platform"] = "ESP32"; #endif +#ifdef USE_LIBRETINY + root["platform"] = lt_cpu_get_model_name(); +#endif root["board"] = ESPHOME_BOARD; #if defined(USE_WIFI) @@ -156,7 +159,7 @@ void MQTTClientComponent::start_dnslookup_() { this->dns_resolve_error_ = false; this->dns_resolved_ = false; ip_addr_t addr; -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_LIBRETINY) err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 00eb3fdd40..bcb44ab4c2 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -13,6 +13,8 @@ #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) #include "mqtt_backend_esp8266.h" +#elif defined(USE_LIBRETINY) +#include "mqtt_backend_libretiny.h" #endif #include "lwip/ip_addr.h" @@ -300,6 +302,8 @@ class MQTTClientComponent : public Component { MQTTBackendESP32 mqtt_backend_; #elif defined(USE_ESP8266) MQTTBackendESP8266 mqtt_backend_; +#elif defined(USE_LIBRETINY) + MQTTBackendLibreTiny mqtt_backend_; #endif MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; From 7dabbb65d081dbea8c266d7fbc9ac8bfb42dc461 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 26 Sep 2023 20:25:00 -0300 Subject: [PATCH 104/270] Wireguard keepalive remove uint16 type (#5430) --- esphome/components/wireguard/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 717fe50d2c..acb5f690ec 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_REBOOT_TIMEOUT, ) from esphome.components import time +from esphome.core import TimePeriod CONF_NETMASK = "netmask" CONF_PRIVATE_KEY = "private_key" @@ -59,9 +60,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list( _cidr_network ), - cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default=0): cv.Any( + cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default="0s"): cv.All( cv.positive_time_period_seconds, - cv.uint16_t, + cv.Range(max=TimePeriod(seconds=65535)), ), cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" From 69adebfefa13e5f50512a114fed10b54b0ce20b6 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 27 Sep 2023 09:25:14 +1000 Subject: [PATCH 105/270] Fix #4896 and #4903 (#5433) --- CODEOWNERS | 2 +- esphome/components/ili9xxx/display.py | 9 +++---- .../components/ili9xxx/ili9xxx_display.cpp | 12 ++++++++++ esphome/components/ili9xxx/ili9xxx_display.h | 6 +++++ esphome/components/ili9xxx/ili9xxx_init.h | 24 +++++++++++++++++++ tests/test1.yaml | 1 + 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 22e46aa2f0..3920a9100e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,7 +131,7 @@ esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz -esphome/components/ili9xxx/* @nielsnl68 +esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @mreditor97 diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 0435460b6a..89a6b2d1b9 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -25,7 +25,7 @@ def AUTO_LOAD(): return [] -CODEOWNERS = ["@nielsnl68"] +CODEOWNERS = ["@nielsnl68", "@clydebarrow"] ili9XXX_ns = cg.esphome_ns.namespace("ili9xxx") ili9XXXSPI = ili9XXX_ns.class_( @@ -42,6 +42,7 @@ MODELS = { "ILI9341": ili9XXX_ns.class_("ILI9XXXILI9341", ili9XXXSPI), "ILI9342": ili9XXX_ns.class_("ILI9XXXILI9342", ili9XXXSPI), "ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI), + "ILI9481-18": ili9XXX_ns.class_("ILI9XXXILI948118", ili9XXXSPI), "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), "ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI), @@ -140,8 +141,6 @@ async def to_code(config): rhs = [] for x in range(256): rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.add(var.set_palette(prog_arr)) elif config[CONF_COLOR_PALETTE] == "IMAGE_ADAPTIVE": cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8_INDEXED)) from PIL import Image @@ -178,6 +177,4 @@ async def to_code(config): if rhs is not None: prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) - - spi_data_rate = str(spi.SPI_DATA_RATE_OPTIONS[config[CONF_DATA_RATE]]) - cg.add_define("ILI9XXXDisplay_DATA_RATE", cg.RawExpression(spi_data_rate)) + cg.add(var.set_data_rate(config[CONF_DATA_RATE])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 750f629db2..abe01ea8c3 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -59,6 +59,7 @@ void ILI9XXXDisplay::dump_config() { if (this->is_18bitdisplay_) { ESP_LOGCONFIG(TAG, " 18-Bit Mode: YES"); } + ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); @@ -387,6 +388,17 @@ void ILI9XXXILI9481::initialize() { } } +void ILI9XXXILI948118::initialize() { + this->init_lcd_(INITCMD_ILI9481_18); + if (this->width_ == 0) { + this->width_ = 320; + } + if (this->height_ == 0) { + this->height_ = 480; + } + this->is_18bitdisplay_ = true; +} + // 35_TFT display void ILI9XXXILI9486::initialize() { this->init_lcd_(INITCMD_ILI9486); diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 15b08e6c76..4e8355b9a5 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -120,6 +120,12 @@ class ILI9XXXILI9481 : public ILI9XXXDisplay { void initialize() override; }; +//----------- ILI9481 in 18 bit mode -------------- +class ILI9XXXILI948118 : public ILI9XXXDisplay { + protected: + void initialize() override; +}; + //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9486 : public ILI9XXXDisplay { protected: diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 1856fb06ab..031dc25f91 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -94,12 +94,36 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_IFCTR , 1, 0x83, ILI9XXX_GMCTR ,12, 0x00, 0x26, 0x21, 0x00, 0x00, 0x1F, 0x65, 0x23, 0x77, 0x00, 0x0F, 0x00, ILI9XXX_IFMODE , 1, 0x00, // CommandAccessProtect + ILI9XXX_PTLAR , 4, 0, 0, 1, 0xDF, 0xE4 , 1, 0xA0, + ILI9XXX_MADCTL , 1, MADCTL_MV | MADCTL_BGR, // Memory Access Control ILI9XXX_CSCON , 1, 0x01, + ILI9XXX_PIXFMT, 1, 0x55, // 16 bit mode + ILI9XXX_INVON, 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; +static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { + ILI9XXX_SLPOUT , 0x80, // Exit sleep mode + ILI9XXX_PWSET , 3, 0x07, 0x41, 0x1D, + ILI9XXX_VMCTR , 3, 0x00, 0x1C, 0x1F, + ILI9XXX_PWSETN , 2, 0x01, 0x11, + ILI9XXX_PWCTR1 , 5, 0x10, 0x3B, 0x00, 0x02, 0x11, + ILI9XXX_VMCTR1 , 1, 0x03, + ILI9XXX_IFCTR , 1, 0x83, + ILI9XXX_GMCTR ,12, 0x00, 0x26, 0x21, 0x00, 0x00, 0x1F, 0x65, 0x23, 0x77, 0x00, 0x0F, 0x00, + ILI9XXX_IFMODE , 1, 0x00, // CommandAccessProtect + ILI9XXX_PTLAR , 4, 0, 0, 1, 0xDF, + 0xE4 , 1, 0xA0, + ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control + ILI9XXX_CSCON , 1, 0x01, + ILI9XXX_PIXFMT, 1, 0x66, // 18 bit mode + ILI9XXX_INVON, 0, + ILI9XXX_DISPON, 0x80, // Set display on + 0x00 // end +}; + static const uint8_t PROGMEM INITCMD_ILI9486[] = { ILI9XXX_SLPOUT, 0x80, ILI9XXX_PIXFMT, 1, 0x55, diff --git a/tests/test1.yaml b/tests/test1.yaml index fe983cf421..350057e3cc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2972,6 +2972,7 @@ display: model: TFT 2.4 cs_pin: GPIO5 dc_pin: GPIO4 + color_palette: GRAYSCALE reset_pin: GPIO22 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); From e5bae8187f0886360a0807eaec6a83acf591a5db Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:28:12 +1300 Subject: [PATCH 106/270] Bump version to 2023.9.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f5319adfa7..c5cc7010a9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0b3" +__version__ = "2023.9.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From cc1b7a7a569e89c509d7810b87b56690cc23ee36 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:21:35 +1300 Subject: [PATCH 107/270] Bump version to 2023.9.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index c5cc7010a9..bf703d9595 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0b4" +__version__ = "2023.9.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From dae8ab563c92288bc017be8567fe41f9f93ec159 Mon Sep 17 00:00:00 2001 From: Marc J Date: Wed, 27 Sep 2023 00:45:21 -0700 Subject: [PATCH 108/270] Tuya Number Scaling by step value (#5108) --- esphome/components/tuya/number/tuya_number.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 5c7cafbf7a..30ef8b8f72 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -10,7 +10,7 @@ void TuyaNumber::setup() { this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::INTEGER) { ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); - this->publish_state(datapoint.value_int); + this->publish_state(datapoint.value_int * this->traits.get_step()); } else if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); this->publish_state(datapoint.value_enum); @@ -22,7 +22,8 @@ void TuyaNumber::setup() { void TuyaNumber::control(float value) { ESP_LOGV(TAG, "Setting number %u: %f", this->number_id_, value); if (this->type_ == TuyaDatapointType::INTEGER) { - this->parent_->set_integer_datapoint_value(this->number_id_, value); + int integer_value = lround(value / this->traits.get_step()); + this->parent_->set_integer_datapoint_value(this->number_id_, integer_value); } else if (this->type_ == TuyaDatapointType::ENUM) { this->parent_->set_enum_datapoint_value(this->number_id_, value); } From b5b654e0546f3fdfd835de3930b91f7e6760be69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:40:36 +1300 Subject: [PATCH 109/270] Migrate dashboard json files to /data folder instead of wiping out (#5441) --- docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index 775c2fa0d6..edb98a8d9b 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -42,7 +42,12 @@ fi mkdir -p "${pio_cache_base}" if bashio::fs.directory_exists '/config/esphome/.esphome'; then - bashio::log.info "Removing old .esphome directory..." + bashio::log.info "Migrating old .esphome directory..." + if bashio::fs.file_exists '/config/esphome/.esphome/esphome.json'; then + mv /config/esphome/.esphome/esphome.json /data/esphome.json + fi + mkdir -p "/data/storage" + mv /config/esphome/.esphome/*.json /data/storage/ || true rm -rf /config/esphome/.esphome fi From d262548d2e00bf7e9b3354430f1deca4ea79edf4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:52:35 +1300 Subject: [PATCH 110/270] Bump version to 2023.9.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index bf703d9595..65757da130 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.0" +__version__ = "2023.9.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ec4777b8d03845d01e5ed87f72a19093f205d277 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:36:31 +1000 Subject: [PATCH 111/270] SPI fixes for buggy components (#5446) --- CODEOWNERS | 2 +- esphome/components/max7219/max7219.cpp | 5 +---- esphome/components/spi/__init__.py | 2 +- esphome/components/spi/spi.h | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3920a9100e..ce19f14c05 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -270,7 +270,7 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/speaker/* @jesserockz -esphome/components/spi/* @esphome/core +esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow esphome/components/sprinkler/* @kbx81 diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 38b4a165cb..b08723f1d4 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -217,10 +217,7 @@ uint8_t MAX7219Component::printf(const char *format, ...) { return 0; } void MAX7219Component::set_writer(max7219_writer_t &&writer) { this->writer_ = writer; } -void MAX7219Component::set_intensity(uint8_t intensity) { - this->intensity_ = intensity; - this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); -} +void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 79e7a5b034..a5aa610462 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -28,7 +28,7 @@ from esphome.const import ( ) from esphome.core import coroutine_with_priority, CORE -CODEOWNERS = ["@esphome/core"] +CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 56aa746fc9..107ffb7cb5 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -248,6 +248,7 @@ class SPIDelegateDummy : public SPIDelegate { SPIDelegateDummy() = default; uint8_t transfer(uint8_t data) override { return 0; } + void end_transaction() override{}; void begin_transaction() override; }; From e9bda2810f5b9aaf867ced8c465d89401a0e1e00 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Fri, 29 Sep 2023 04:17:32 +0300 Subject: [PATCH 112/270] Fix an Issue with IR Remote Climate and Whirlpool protocol toggle (#5447) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/whirlpool/whirlpool.cpp | 7 +++++++ esphome/components/whirlpool/whirlpool.h | 2 ++ 2 files changed, 9 insertions(+) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 225423b4db..1ac32f30da 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -33,6 +33,7 @@ const uint8_t WHIRLPOOL_SWING_MASK = 128; const uint8_t WHIRLPOOL_POWER = 0x04; void WhirlpoolClimate::transmit_state() { + this->last_transmit_time_ = millis(); // setting the time of the last transmission. uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; remote_state[0] = 0x83; remote_state[1] = 0x06; @@ -149,6 +150,12 @@ void WhirlpoolClimate::transmit_state() { } bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { + // Check if the esp isn't currently transmitting. + if (millis() - this->last_transmit_time_ < 500) { + ESP_LOGV(TAG, "Blocked receive because of current trasmittion"); + return false; + } + // Validate header if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) { ESP_LOGV(TAG, "Header fail"); diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 7f31894df9..907a21225c 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -47,6 +47,8 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; + /// Set the time of the last transmission. + int32_t last_transmit_time_{}; bool send_swing_cmd_{false}; Model model_; From efd31be21cd347950fe3ef4089e23109c1d81353 Mon Sep 17 00:00:00 2001 From: Angel Nunez Mencias Date: Sat, 30 Sep 2023 01:34:56 +0200 Subject: [PATCH 113/270] Fix SPI support for second bus on 2023.9.1 (#5456) --- esphome/components/spi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index a5aa610462..fb30755511 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -187,7 +187,7 @@ def get_spi_interface(index): # Following code can't apply to C2, H2 or 8266 since they have only one SPI if get_target_variant() in (VARIANT_ESP32S3, VARIANT_ESP32S2): return "new SPIClass(FSPI)" - return "return new SPIClass(HSPI)" + return "new SPIClass(HSPI)" SPI_SCHEMA = cv.All( From af005a6554265cb0f5f2f14fb3302880dfc89429 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:45:24 +1300 Subject: [PATCH 114/270] Ensure esphome directory exists on addon startup (#5464) --- docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run index edb98a8d9b..f973dfcaf8 100755 --- a/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/run @@ -41,6 +41,8 @@ fi mkdir -p "${pio_cache_base}" +mkdir -p /config/esphome + if bashio::fs.directory_exists '/config/esphome/.esphome'; then bashio::log.info "Migrating old .esphome directory..." if bashio::fs.file_exists '/config/esphome/.esphome/esphome.json'; then From 5e1472185cb478cdd8e60dbc1bb01d1902c921cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:01:22 +1300 Subject: [PATCH 115/270] Bump version to 2023.9.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 65757da130..496880ac88 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.1" +__version__ = "2023.9.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From f73fd975250364ae783593ef927393015fbe8577 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:58:51 +1300 Subject: [PATCH 116/270] Bump zeroconf from 0.112.0 to 0.115.0 (#5432) 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 63199680cf..821e7c5786 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20230904.0 aioesphomeapi==15.0.0 -zeroconf==0.112.0 +zeroconf==0.115.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 689c2f11a39f7b1c0411160714802c15ffaef74b Mon Sep 17 00:00:00 2001 From: Maxime Gauduin Date: Mon, 2 Oct 2023 23:15:29 +0200 Subject: [PATCH 117/270] add pin config for denky_d4 (#5471) --- esphome/components/esp32/boards.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 61cb8cdc3f..e6c23c4d96 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -235,6 +235,7 @@ ESP32_BOARD_PINS = { "SDA": 5, "SS": 15, }, + "denky_d4": {"RX": 8, "LED": 14}, "esp-wrover-kit": {}, "esp32-devkitlipo": {}, "esp32-evb": { From f5dfbaff4b2938e2015434dc05e4213dfd43077d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:24:20 +1100 Subject: [PATCH 118/270] Support RP2040 hardware SPI (#5466) --- esphome/components/spi/__init__.py | 64 +++++++++++++++++++++++++----- tests/test6.yaml | 8 ++++ 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index fb30755511..07e8982f6e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -54,6 +54,21 @@ CONF_FORCE_SW = "force_sw" CONF_INTERFACE = "interface" CONF_INTERFACE_INDEX = "interface_index" +# RP2040 SPI pin assignments are complicated. Refer to https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + +RP_SPI_PINSETS = [ + { + CONF_MISO_PIN: [0, 4, 16, 20, -1], + CONF_CLK_PIN: [2, 6, 18, 22], + CONF_MOSI_PIN: [3, 7, 19, 23, -1], + }, + { + CONF_MISO_PIN: [8, 12, 24, 28, -1], + CONF_CLK_PIN: [10, 14, 26], + CONF_MOSI_PIN: [11, 23, 27, -1], + }, +] + def get_target_platform(): return ( @@ -85,7 +100,7 @@ def get_hw_interface_list(): return [["spi", "spi2"]] return [["spi", "spi2"], ["spi3"]] if target_platform == "rp2040": - return [["spi"]] + return [["spi"], ["spi1"]] return [] @@ -99,8 +114,10 @@ def get_spi_index(name): # Check that pins are suitable for HW spi +# \param spi the config data for the spi instance +# \param index the selected hw interface number, -1 if not yet known # TODO verify that the pins are internal -def validate_hw_pins(spi): +def validate_hw_pins(spi, index=-1): clk_pin = spi[CONF_CLK_PIN] if clk_pin[CONF_INVERTED]: return False @@ -129,9 +146,30 @@ def validate_hw_pins(spi): if target_platform == "esp32": return clk_pin_no >= 0 + if target_platform == "rp2040": + pin_set = ( + list(filter(lambda s: clk_pin_no in s[CONF_CLK_PIN], RP_SPI_PINSETS))[0] + if index == -1 + else RP_SPI_PINSETS[index] + ) + if pin_set is None: + return False + if sdo_pin_no not in pin_set[CONF_MOSI_PIN]: + return False + if sdi_pin_no not in pin_set[CONF_MISO_PIN]: + return False + return True return False +def get_hw_spi(config, available): + """Get an available hardware spi interface suitable for this config""" + matching = list(filter(lambda idx: validate_hw_pins(config, idx), available)) + if len(matching) != 0: + return matching[0] + return None + + def validate_spi_config(config): available = list(range(len(get_hw_interface_list()))) for spi in config: @@ -147,9 +185,10 @@ def validate_spi_config(config): if not validate_hw_pins(spi): spi[CONF_INTERFACE] = "software" elif interface == "hardware": - if len(available) == 0: - raise cv.Invalid("No hardware interface available") - index = spi[CONF_INTERFACE_INDEX] = available[0] + index = get_hw_spi(spi, available) + if index is None: + raise cv.Invalid("No suitable hardware interface available") + spi[CONF_INTERFACE_INDEX] = index available.remove(index) else: # Must be a specific name @@ -164,11 +203,14 @@ def validate_spi_config(config): # Any specific names and any 'hardware' requests will have already been filled, # so just need to assign remaining hardware to 'any' requests. for spi in config: - if spi[CONF_INTERFACE] == "any" and len(available) != 0: - index = available[0] - spi[CONF_INTERFACE_INDEX] = index - available.remove(index) - if CONF_INTERFACE_INDEX in spi and not validate_hw_pins(spi): + if spi[CONF_INTERFACE] == "any": + index = get_hw_spi(spi, available) + if index is not None: + spi[CONF_INTERFACE_INDEX] = index + available.remove(index) + if CONF_INTERFACE_INDEX in spi and not validate_hw_pins( + spi, spi[CONF_INTERFACE_INDEX] + ): raise cv.Invalid("Invalid pin selections for hardware SPI interface") return config @@ -181,7 +223,7 @@ def get_spi_interface(index): # Arduino code follows platform = get_target_platform() if platform == "rp2040": - return "&spi1" + return ["&SPI", "&SPI1"][index] if index == 0: return "&SPI" # Following code can't apply to C2, H2 or 8266 since they have only one SPI diff --git a/tests/test6.yaml b/tests/test6.yaml index 3d6a1ceb1f..c6d9c6feba 100644 --- a/tests/test6.yaml +++ b/tests/test6.yaml @@ -39,6 +39,14 @@ switch: output: pin_4 id: pin_4_switch + +spi: # Pins are for SPI1 on the RP2040 Pico-W + miso_pin: 8 + clk_pin: 10 + mosi_pin: 11 + id: spi_0 + interface: hardware + #light: # - platform: rp2040_pio_led_strip # id: led_strip From 85c5928baa6bddc3668f20af3b32ac6052e77f9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:51:29 +1300 Subject: [PATCH 119/270] Bump zeroconf from 0.115.0 to 0.115.1 (#5470) 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 821e7c5786..97e42663d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20230904.0 aioesphomeapi==15.0.0 -zeroconf==0.115.0 +zeroconf==0.115.1 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From f709350b04ebb52320492fb450fbeded051b6c8e Mon Sep 17 00:00:00 2001 From: dwildstr <65917913+dwildstr@users.noreply.github.com> Date: Mon, 2 Oct 2023 19:06:54 -0400 Subject: [PATCH 120/270] Sleep mode fix for BP5758D driver (#5461) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/bp5758d/bp5758d.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp index 111fd6b68e..71a81f7e6c 100644 --- a/esphome/components/bp5758d/bp5758d.cpp +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -39,10 +39,14 @@ void BP5758D::loop() { uint8_t data[17]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; for (int i = 1; i < 16; i++) data[i] = 0; + + // First turn all channels off + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_3CH; + this->write_buffer_(data, 17); + // Then sleep + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; this->write_buffer_(data, 17); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { From 7dfc4c74da6d20de03ee01254e81bf7c5a36509e Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Tue, 3 Oct 2023 03:23:18 +0300 Subject: [PATCH 121/270] Tuya Number: split "multiply" to a separate option (#5458) --- esphome/components/tuya/number/__init__.py | 7 +++++-- esphome/components/tuya/number/tuya_number.cpp | 4 ++-- esphome/components/tuya/number/tuya_number.h | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 42ac9fcfbe..4dae6d8d60 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_NUMBER_DATAPOINT, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_MULTIPLY, CONF_STEP, ) from .. import tuya_ns, CONF_TUYA_ID, Tuya @@ -31,6 +32,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, } ) .extend(cv.COMPONENT_SCHEMA), @@ -49,7 +51,8 @@ async def to_code(config): step=config[CONF_STEP], ) - paren = await cg.get_variable(config[CONF_TUYA_ID]) - cg.add(var.set_tuya_parent(paren)) + cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) + parent = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(parent)) cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 30ef8b8f72..e883c72d3d 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -10,7 +10,7 @@ void TuyaNumber::setup() { this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::INTEGER) { ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); - this->publish_state(datapoint.value_int * this->traits.get_step()); + this->publish_state(datapoint.value_int / multiply_by_); } else if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); this->publish_state(datapoint.value_enum); @@ -22,7 +22,7 @@ void TuyaNumber::setup() { void TuyaNumber::control(float value) { ESP_LOGV(TAG, "Setting number %u: %f", this->number_id_, value); if (this->type_ == TuyaDatapointType::INTEGER) { - int integer_value = lround(value / this->traits.get_step()); + int integer_value = lround(value * multiply_by_); this->parent_->set_integer_datapoint_value(this->number_id_, integer_value); } else if (this->type_ == TuyaDatapointType::ENUM) { this->parent_->set_enum_datapoint_value(this->number_id_, value); diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h index 7cca9fc646..f64dac8957 100644 --- a/esphome/components/tuya/number/tuya_number.h +++ b/esphome/components/tuya/number/tuya_number.h @@ -12,6 +12,7 @@ class TuyaNumber : public number::Number, public Component { void setup() override; void dump_config() override; void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } + void set_write_multiply(float factor) { multiply_by_ = factor; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } @@ -20,6 +21,7 @@ class TuyaNumber : public number::Number, public Component { Tuya *parent_; uint8_t number_id_{0}; + float multiply_by_{1.0}; TuyaDatapointType type_{}; }; From 471533d0418290814216f84ab500ba2582ad99b2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:35:19 +1300 Subject: [PATCH 122/270] Bump version to 2023.9.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 496880ac88..15558da081 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.9.2" +__version__ = "2023.9.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 54363f1246eeab37375b0c37748b18e8c7570d80 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:14:43 +1300 Subject: [PATCH 123/270] Bump version to 2023.10.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fc67651193..5a5b683c2d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0-dev" +__version__ = "2023.10.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d27e5e9c97ccb1035c3ef738f0fd22cb36227c7a Mon Sep 17 00:00:00 2001 From: Nippey Date: Thu, 12 Oct 2023 19:56:30 +0200 Subject: [PATCH 124/270] Update htu21d.cpp, fix publishing of heater level (#5520) --- esphome/components/htu21d/htu21d.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index 5030ac4d0f..a8133ae32e 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -76,7 +76,7 @@ void HTU21DComponent::update() { if (this->humidity_ != nullptr) this->humidity_->publish_state(humidity); if (this->heater_ != nullptr) - this->heater_->publish_state(humidity); + this->heater_->publish_state(heater_level); this->status_clear_warning(); } From 6cce6d4c364104082b77a6ec12ede5243cbe11bb Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:09:07 -0500 Subject: [PATCH 125/270] BD5758D - Add delays and ACKs (#5524) --- esphome/components/bp5758d/bp5758d.cpp | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp index 71a81f7e6c..87c4165275 100644 --- a/esphome/components/bp5758d/bp5758d.cpp +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -17,12 +17,16 @@ static const uint8_t BP5758D_ADDR_START_2CH = 0b00100000; static const uint8_t BP5758D_ADDR_START_5CH = 0b00110000; static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; +static const uint8_t BP5758D_DELAY = 2; + void BP5758D::setup() { ESP_LOGCONFIG(TAG, "Setting up BP5758D Output Component..."); this->data_pin_->setup(); this->data_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); this->clock_pin_->setup(); this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); this->channel_current_.resize(5, 0); this->pwm_amounts_.resize(5, 0); } @@ -39,11 +43,11 @@ void BP5758D::loop() { uint8_t data[17]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - for (int i = 1; i < 16; i++) + for (int i = 1; i < 17; i++) data[i] = 0; // First turn all channels off - data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_3CH; + data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_START_5CH; this->write_buffer_(data, 17); // Then sleep data[0] = BP5758D_MODEL_ID + BP5758D_ADDR_STANDBY; @@ -123,28 +127,42 @@ void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) { void BP5758D::set_channel_current_(uint8_t channel, uint8_t current) { this->channel_current_[channel] = current; } void BP5758D::write_bit_(bool value) { - this->clock_pin_->digital_write(false); this->data_pin_->digital_write(value); + delayMicroseconds(BP5758D_DELAY); this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); } void BP5758D::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); + + // ack bit + this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void BP5758D::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(BP5758D_DELAY); + for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); } - this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(BP5758D_DELAY); } } // namespace bp5758d From 969f6dbe131824ce0c9e6c23fe9a8b207887f991 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:46:46 +1300 Subject: [PATCH 126/270] Update Improv BLE component (#5518) --- .../esp32_ble_server/ble_service.cpp | 2 + .../components/esp32_ble_server/ble_service.h | 2 +- esphome/components/esp32_improv/__init__.py | 2 +- .../esp32_improv/esp32_improv_component.cpp | 79 +++++++++++-------- .../esp32_improv/esp32_improv_component.h | 29 +++++-- esphome/components/output/__init__.py | 1 + esphome/core/defines.h | 3 +- 7 files changed, 79 insertions(+), 39 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 4fcd2e3e79..e5aaebc137 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -90,6 +90,8 @@ void BLEService::stop() { ESP_LOGE(TAG, "esp_ble_gatts_stop_service failed: %d", err); return; } + esp32_ble::global_ble->get_advertising()->remove_service_uuid(this->uuid_); + esp32_ble::global_ble->get_advertising()->start(); this->running_state_ = STOPPING; } diff --git a/esphome/components/esp32_ble_server/ble_service.h b/esphome/components/esp32_ble_server/ble_service.h index 2766c931a7..93b4217517 100644 --- a/esphome/components/esp32_ble_server/ble_service.h +++ b/esphome/components/esp32_ble_server/ble_service.h @@ -7,11 +7,11 @@ #ifdef USE_ESP32 +#include #include #include #include #include -#include namespace esphome { namespace esp32_ble_server { diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index ae7f0b6427..fba2e55ae8 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -4,7 +4,7 @@ from esphome.components import binary_sensor, output, esp32_ble_server from esphome.const import CONF_ID -AUTO_LOAD = ["binary_sensor", "output", "esp32_ble_server"] +AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_beacon"] DEPENDENCIES = ["wifi", "esp32"] diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 85013c006b..5bdf7d19fe 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -18,6 +18,17 @@ ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } void ESP32ImprovComponent::setup() { this->service_ = global_ble_server->create_service(improv::SERVICE_UUID, true); this->setup_characteristics(); + +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ != nullptr) { + this->authorizer_->add_on_state_callback([this](bool state) { + if (state) { + this->authorized_start_ = millis(); + this->identify_start_ = 0; + } + }); + } +#endif } void ESP32ImprovComponent::setup_characteristics() { @@ -50,8 +61,10 @@ void ESP32ImprovComponent::setup_characteristics() { BLEDescriptor *capabilities_descriptor = new BLE2902(); this->capabilities_->add_descriptor(capabilities_descriptor); uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT if (this->status_indicator_ != nullptr) capabilities |= improv::CAPABILITY_IDENTIFY; +#endif this->capabilities_->set_value(capabilities); this->setup_complete_ = true; } @@ -63,8 +76,7 @@ void ESP32ImprovComponent::loop() { switch (this->state_) { case improv::STATE_STOPPED: - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); if (this->service_->is_created() && this->should_start_ && this->setup_complete_) { if (this->service_->is_running()) { @@ -80,14 +92,17 @@ void ESP32ImprovComponent::loop() { } break; case improv::STATE_AWAITING_AUTHORIZATION: { - if (this->authorizer_ == nullptr || this->authorizer_->state) { +#ifdef USE_BINARY_SENSOR + if (this->authorizer_ == nullptr || + (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) { this->set_state_(improv::STATE_AUTHORIZED); - this->authorized_start_ = now; - } else { - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) - this->status_indicator_->turn_on(); - } + } else +#else + this->set_state_(improv::STATE_AUTHORIZED); +#endif + { + if (!this->check_identify_()) + this->set_status_indicator_state_(true); } break; } @@ -99,25 +114,13 @@ void ESP32ImprovComponent::loop() { return; } } - if (this->status_indicator_ != nullptr) { - if (!this->check_identify_()) { - if ((now % 1000) < 500) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } + if (!this->check_identify_()) { + this->set_status_indicator_state_((now % 1000) < 500); } break; } case improv::STATE_PROVISIONING: { - if (this->status_indicator_ != nullptr) { - if ((now % 200) < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } - } + this->set_status_indicator_state_((now % 200) < 100); if (wifi::global_wifi_component->is_connected()) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); @@ -142,13 +145,27 @@ void ESP32ImprovComponent::loop() { } case improv::STATE_PROVISIONED: { this->incoming_data_.clear(); - if (this->status_indicator_ != nullptr) - this->status_indicator_->turn_off(); + this->set_status_indicator_state_(false); break; } } } +void ESP32ImprovComponent::set_status_indicator_state_(bool state) { +#ifdef USE_OUTPUT + if (this->status_indicator_ == nullptr) + return; + if (this->status_indicator_state_ == state) + return; + this->status_indicator_state_ = state; + if (state) { + this->status_indicator_->turn_on(); + } else { + this->status_indicator_->turn_off(); + } +#endif +} + bool ESP32ImprovComponent::check_identify_() { uint32_t now = millis(); @@ -156,11 +173,7 @@ bool ESP32ImprovComponent::check_identify_() { if (identify) { uint32_t time = now % 1000; - if (time < 600 && time % 200 < 100) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } + this->set_status_indicator_state_(time < 600 && time % 200 < 100); } return identify; } @@ -213,8 +226,12 @@ float ESP32ImprovComponent::get_setup_priority() const { return setup_priority:: void ESP32ImprovComponent::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 Improv:"); +#ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); +#endif +#ifdef USE_OUTPUT ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr)); +#endif } void ESP32ImprovComponent::process_incoming_data_() { diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 1a142c94b6..ba9892d6a5 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -1,14 +1,22 @@ #pragma once -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/esp32_ble_server/ble_characteristic.h" -#include "esphome/components/esp32_ble_server/ble_server.h" -#include "esphome/components/output/binary_output.h" -#include "esphome/components/wifi/wifi_component.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/components/esp32_ble_server/ble_characteristic.h" +#include "esphome/components/esp32_ble_server/ble_server.h" +#include "esphome/components/wifi/wifi_component.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_OUTPUT +#include "esphome/components/output/binary_output.h" +#endif + #include #ifdef USE_ESP32 @@ -34,8 +42,12 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void stop() override; bool is_active() const { return this->state_ != improv::STATE_STOPPED; } +#ifdef USE_BINARY_SENSOR void set_authorizer(binary_sensor::BinarySensor *authorizer) { this->authorizer_ = authorizer; } +#endif +#ifdef USE_OUTPUT void set_status_indicator(output::BinaryOutput *status_indicator) { this->status_indicator_ = status_indicator; } +#endif void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } @@ -58,12 +70,19 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { BLECharacteristic *rpc_response_; BLECharacteristic *capabilities_; +#ifdef USE_BINARY_SENSOR binary_sensor::BinarySensor *authorizer_{nullptr}; +#endif +#ifdef USE_OUTPUT output::BinaryOutput *status_indicator_{nullptr}; +#endif improv::State state_{improv::STATE_STOPPED}; improv::Error error_state_{improv::ERROR_NONE}; + bool status_indicator_state_{false}; + void set_status_indicator_state_(bool state); + void set_state_(improv::State state); void set_error_(improv::Error error); void send_response_(std::vector &response); diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4f1fb33fe7..726d1ac084 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -106,4 +106,5 @@ async def output_set_level_to_code(config, action_id, template_arg, args): async def to_code(config): + cg.add_define("USE_OUTPUT") cg.add_global(output_ns.using) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1e0df74eec..71493119c0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -37,6 +37,7 @@ #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK +#define USE_OUTPUT #define USE_POWER_SUPPLY #define USE_QR_CODE #define USE_SELECT @@ -117,6 +118,6 @@ #endif // Disabled feature flags -//#define USE_BSEC // Requires a library with proprietary license. +// #define USE_BSEC // Requires a library with proprietary license. #define USE_DASHBOARD_IMPORT From 8c1ad1e9a6816d2d59cb768d8d9638359625a195 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Thu, 12 Oct 2023 18:51:19 -0500 Subject: [PATCH 127/270] SM10BIT_BASE - Add delays and ACKs, clear all channels before sleeping. (#5526) --- .../components/sm10bit_base/sm10bit_base.cpp | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/esphome/components/sm10bit_base/sm10bit_base.cpp b/esphome/components/sm10bit_base/sm10bit_base.cpp index 9c7abb48e2..d380f31c6f 100644 --- a/esphome/components/sm10bit_base/sm10bit_base.cpp +++ b/esphome/components/sm10bit_base/sm10bit_base.cpp @@ -11,6 +11,8 @@ static const uint8_t SM10BIT_ADDR_START_3CH = 0x8; static const uint8_t SM10BIT_ADDR_START_2CH = 0x10; static const uint8_t SM10BIT_ADDR_START_5CH = 0x18; +static const uint8_t SM10BIT_DELAY = 2; + // Power current values // HEX | Binary | RGB level | White level | Config value // 0x0 | 0000 | RGB 10mA | CW 5mA | 0 @@ -37,10 +39,13 @@ void Sm10BitBase::loop() { uint8_t data[12]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; for (int i = 1; i < 12; i++) data[i] = 0; + // First turn all channels off + data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH; + this->write_buffer_(data, 12); + // Then sleep + data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY; this->write_buffer_(data, 12); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { @@ -84,28 +89,42 @@ void Sm10BitBase::set_channel_value_(uint8_t channel, uint16_t value) { this->pwm_amounts_[channel] = value; } void Sm10BitBase::write_bit_(bool value) { - this->clock_pin_->digital_write(false); this->data_pin_->digital_write(value); + delayMicroseconds(SM10BIT_DELAY); this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); } void Sm10BitBase::write_byte_(uint8_t data) { for (uint8_t mask = 0x80; mask; mask >>= 1) { this->write_bit_(data & mask); } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); + + // ack bit + this->data_pin_->pin_mode(gpio::FLAG_INPUT); this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); } void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) { this->data_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + this->clock_pin_->digital_write(false); + delayMicroseconds(SM10BIT_DELAY); + for (uint32_t i = 0; i < size; i++) { this->write_byte_(buffer[i]); } - this->clock_pin_->digital_write(false); + this->clock_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); this->data_pin_->digital_write(true); + delayMicroseconds(SM10BIT_DELAY); } } // namespace sm10bit_base From 5d7c3d16224ff4ce8b7afaa91907b95b9c29b7c2 Mon Sep 17 00:00:00 2001 From: Cossid <83468485+Cossid@users.noreply.github.com> Date: Thu, 12 Oct 2023 18:51:28 -0500 Subject: [PATCH 128/270] BP1658CJ - Clear all channels before sleeping. (#5525) --- esphome/components/bp1658cj/bp1658cj.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index d3f3e71fed..05c3f790c2 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -37,10 +37,14 @@ void BP1658CJ::loop() { uint8_t data[12]; if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) { - // Off / Sleep - data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; for (int i = 1; i < 12; i++) data[i] = 0; + + // First turn all channels off + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_START_5CH; + this->write_buffer_(data, 12); + // Then sleep + data[0] = BP1658CJ_MODEL_ID + BP1658CJ_ADDR_STANDBY; this->write_buffer_(data, 12); } else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 && (this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) { From 90315b3c401f608ae3dbe6f3cf89d6ef000c7510 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:16:22 +1300 Subject: [PATCH 129/270] Bump version to 2023.10.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5a5b683c2d..cef733b2f9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0b1" +__version__ = "2023.10.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 261c271d60caa35a496cca256f667157c3ee2a4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:41:59 +1300 Subject: [PATCH 130/270] Prometheus fix for esp-idf and fix newlines (#5536) --- .../prometheus/prometheus_handler.cpp | 22 ++++++++----------- .../prometheus/prometheus_handler.h | 8 ++----- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index abb5111aaf..68bca95a21 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "prometheus_handler.h" #include "esphome/core/application.h" @@ -89,7 +87,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(obj->get_unit_of_measurement().c_str()); stream->print(F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); - stream->print('\n'); + stream->print(F("\n")); } else { // Invalid state stream->print(F("esphome_sensor_failed{id=\"")); @@ -124,7 +122,7 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); } else { // Invalid state stream->print(F("esphome_binary_sensor_failed{id=\"")); @@ -158,7 +156,7 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); // Speed if available if (obj->get_traits().supports_speed()) { stream->print(F("esphome_fan_speed{id=\"")); @@ -167,7 +165,7 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->speed); - stream->print('\n'); + stream->print(F("\n")); } // Oscillation if available if (obj->get_traits().supports_oscillation()) { @@ -177,7 +175,7 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->oscillating); - stream->print('\n'); + stream->print(F("\n")); } } #endif @@ -281,7 +279,7 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->position); - stream->print('\n'); + stream->print(F("\n")); if (obj->get_traits().get_supports_tilt()) { stream->print(F("esphome_cover_tilt{id=\"")); stream->print(relabel_id_(obj).c_str()); @@ -289,7 +287,7 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->tilt); - stream->print('\n'); + stream->print(F("\n")); } } else { // Invalid state @@ -322,7 +320,7 @@ void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); } #endif @@ -346,11 +344,9 @@ void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); stream->print(obj->state); - stream->print('\n'); + stream->print(F("\n")); } #endif } // namespace prometheus } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 0ae2856ce4..a9505a3572 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -1,14 +1,12 @@ #pragma once -#ifdef USE_ARDUINO - #include #include -#include "esphome/core/entity_base.h" #include "esphome/components/web_server_base/web_server_base.h" -#include "esphome/core/controller.h" #include "esphome/core/component.h" +#include "esphome/core/controller.h" +#include "esphome/core/entity_base.h" namespace esphome { namespace prometheus { @@ -119,5 +117,3 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } // namespace prometheus } // namespace esphome - -#endif // USE_ARDUINO From 52e8a2e9e4ec73620c3a31dc8925eb9cae19e3c5 Mon Sep 17 00:00:00 2001 From: raineth Date: Mon, 16 Oct 2023 02:42:18 -0400 Subject: [PATCH 131/270] Make IPAddress's operator!= compare values, not memory addresses. (#5537) Co-authored-by: Ben Winslow --- esphome/components/network/ip_address.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 8b05237c05..7a4d394805 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -87,7 +87,7 @@ struct IPAddress { bool is_ip6() { return IP_IS_V6(&ip_addr_); } std::string str() const { return ipaddr_ntoa(&ip_addr_); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } - bool operator!=(const IPAddress &other) const { return !(&ip_addr_ == &other.ip_addr_); } + bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } IPAddress &operator+=(uint8_t increase) { if (IP_IS_V4(&ip_addr_)) { #if LWIP_IPV6 From 97d624114d8722e84fd4ac62f841cccba5e7f725 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 16 Oct 2023 20:47:35 +0100 Subject: [PATCH 132/270] Add change i2c address and allow multi conf for TB6612FNG (#5492) --- .../components/grove_tb6612fng/__init__.py | 25 +++++++++++++++++++ .../grove_tb6612fng/grove_tb6612fng.h | 11 ++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 75610ce9d3..7db0198a89 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -8,12 +8,15 @@ from esphome.const import ( CONF_CHANNEL, CONF_SPEED, CONF_DIRECTION, + CONF_ADDRESS, ) DEPENDENCIES = ["i2c"] CODEOWNERS = ["@max246"] +MULTI_CONF = True + grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng") GROVE_TB6612FNG = grove_tb6612fng_ns.class_( "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice @@ -33,6 +36,9 @@ GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_( GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_( "GROVETB6612FNGMotorNoStandbyAction", automation.Action ) +GROVETB6612FNGMotorChangeAddressAction = grove_tb6612fng_ns.class_( + "GROVETB6612FNGMotorChangeAddressAction", automation.Action +) DIRECTION_TYPE = { "FORWARD": 1, @@ -150,3 +156,22 @@ async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, ar await cg.register_parented(var, config[CONF_ID]) return var + + +@automation.register_action( + "grove_tb6612fng.change_address", + GROVETB6612FNGMotorChangeAddressAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), + cv.Required(CONF_ADDRESS): cv.i2c_address, + } + ), +) +async def grove_tb6612fng_change_address_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_channel = await cg.templatable(config[CONF_ADDRESS], args, int) + cg.add(var.set_address(template_channel)) + return var diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index ccdab6472a..2743ef4ed7 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -84,8 +84,7 @@ class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice { *************************************************************/ void set_i2c_addr(uint8_t addr); - /************************************************************* - Description + /***********************************change_address Drive a motor. Parameter chl: MOTOR_CHA or MOTOR_CHB @@ -204,5 +203,13 @@ class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented void play(Ts... x) override { this->parent_->not_standby(); } }; +template +class GROVETB6612FNGMotorChangeAddressAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, address) + + void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } +}; + } // namespace grove_tb6612fng } // namespace esphome From 61cf566560539ba0549843d8e645242833879ddd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:18:05 +1300 Subject: [PATCH 133/270] Add stream start and end events (#5545) --- esphome/components/api/api.proto | 2 + esphome/components/api/api_pb2.cpp | 4 ++ esphome/components/api/api_pb2.h | 2 + .../i2s_audio/speaker/i2s_audio_speaker.cpp | 6 ++ .../voice_assistant/voice_assistant.cpp | 69 +++++++++++++++++-- .../voice_assistant/voice_assistant.h | 3 +- 6 files changed, 81 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index ec4a0f7cc9..69765c7a94 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1459,6 +1459,8 @@ enum VoiceAssistantEvent { VOICE_ASSISTANT_WAKE_WORD_END = 10; VOICE_ASSISTANT_STT_VAD_START = 11; VOICE_ASSISTANT_STT_VAD_END = 12; + VOICE_ASSISTANT_TTS_STREAM_START = 98; + VOICE_ASSISTANT_TTS_STREAM_END = 99; } message VoiceAssistantEventData { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 225b213a67..65df2312e1 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -452,6 +452,10 @@ template<> const char *proto_enum_to_string(enums::V return "VOICE_ASSISTANT_STT_VAD_START"; case enums::VOICE_ASSISTANT_STT_VAD_END: return "VOICE_ASSISTANT_STT_VAD_END"; + case enums::VOICE_ASSISTANT_TTS_STREAM_START: + return "VOICE_ASSISTANT_TTS_STREAM_START"; + case enums::VOICE_ASSISTANT_TTS_STREAM_END: + return "VOICE_ASSISTANT_TTS_STREAM_END"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index a4826f09d2..4c70facf3d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -184,6 +184,8 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_WAKE_WORD_END = 10, VOICE_ASSISTANT_STT_VAD_START = 11, VOICE_ASSISTANT_STT_VAD_END = 12, + VOICE_ASSISTANT_TTS_STREAM_START = 98, + VOICE_ASSISTANT_TTS_STREAM_END = 99, }; enum AlarmControlPanelState : uint32_t { ALARM_STATE_DISARMED = 0, diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 592a27b739..ed13e6b458 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -158,8 +158,13 @@ void I2SAudioSpeaker::watch_() { if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) { switch (event.type) { case TaskEventType::STARTING: + ESP_LOGD(TAG, "Starting I2S Audio Speaker"); + break; case TaskEventType::STARTED: + ESP_LOGD(TAG, "Started I2S Audio Speaker"); + break; case TaskEventType::STOPPING: + ESP_LOGD(TAG, "Stopping I2S Audio Speaker"); break; case TaskEventType::PLAYING: this->status_clear_warning(); @@ -170,6 +175,7 @@ void I2SAudioSpeaker::watch_() { this->player_task_handle_ = nullptr; this->parent_->unlock(); xQueueReset(this->buffer_queue_); + ESP_LOGD(TAG, "Stopped I2S Audio Speaker"); break; case TaskEventType::WARNING: ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(event.err)); diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 448df61d80..12fbdc97b4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -281,11 +281,14 @@ void VoiceAssistant::loop() { memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); this->speaker_buffer_size_ -= written; this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 1000, [this]() { this->speaker_->stop(); }); + this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { ESP_LOGW(TAG, "Speaker buffer full."); } } + if (this->wait_for_stream_end_) { + break; // We dont want to timeout here as the STREAM_END event will take care of that. + } playing = this->speaker_->is_running(); } #endif @@ -295,28 +298,77 @@ void VoiceAssistant::loop() { } #endif if (playing) { - this->set_timeout("playing", 100, [this]() { + this->set_timeout("playing", 2000, [this]() { this->cancel_timeout("speaker-timeout"); this->set_state_(State::IDLE, State::IDLE); }); } break; } + case State::RESPONSE_FINISHED: { +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + this->speaker_->stop(); + this->cancel_timeout("speaker-timeout"); + this->cancel_timeout("playing"); + this->speaker_buffer_size_ = 0; + this->speaker_buffer_index_ = 0; + memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); + } +#endif + this->wait_for_stream_end_ = false; + this->set_state_(State::IDLE, State::IDLE); + break; + } default: break; } } +static const LogString *voice_assistant_state_to_string(State state) { + switch (state) { + case State::IDLE: + return LOG_STR("IDLE"); + case State::START_MICROPHONE: + return LOG_STR("START_MICROPHONE"); + case State::STARTING_MICROPHONE: + return LOG_STR("STARTING_MICROPHONE"); + case State::WAIT_FOR_VAD: + return LOG_STR("WAIT_FOR_VAD"); + case State::WAITING_FOR_VAD: + return LOG_STR("WAITING_FOR_VAD"); + case State::START_PIPELINE: + return LOG_STR("START_PIPELINE"); + case State::STARTING_PIPELINE: + return LOG_STR("STARTING_PIPELINE"); + case State::STREAMING_MICROPHONE: + return LOG_STR("STREAMING_MICROPHONE"); + case State::STOP_MICROPHONE: + return LOG_STR("STOP_MICROPHONE"); + case State::STOPPING_MICROPHONE: + return LOG_STR("STOPPING_MICROPHONE"); + case State::AWAITING_RESPONSE: + return LOG_STR("AWAITING_RESPONSE"); + case State::STREAMING_RESPONSE: + return LOG_STR("STREAMING_RESPONSE"); + case State::RESPONSE_FINISHED: + return LOG_STR("RESPONSE_FINISHED"); + default: + return LOG_STR("UNKNOWN"); + } +}; + void VoiceAssistant::set_state_(State state) { State old_state = this->state_; this->state_ = state; - ESP_LOGD(TAG, "State changed from %d to %d", static_cast(old_state), static_cast(state)); + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(voice_assistant_state_to_string(old_state)), + LOG_STR_ARG(voice_assistant_state_to_string(state))); } void VoiceAssistant::set_state_(State state, State desired_state) { this->set_state_(state); this->desired_state_ = desired_state; - ESP_LOGD(TAG, "Desired state set to %d", static_cast(desired_state)); + ESP_LOGD(TAG, "Desired state set to %s", LOG_STR_ARG(voice_assistant_state_to_string(desired_state))); } void VoiceAssistant::failed_to_start() { @@ -400,6 +452,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: case State::STREAMING_RESPONSE: + case State::RESPONSE_FINISHED: break; // Let the incoming audio stream finish then it will go to idle. } } @@ -531,6 +584,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->error_trigger_->trigger(code, message); break; } + case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { + this->wait_for_stream_end_ = true; + break; + } + case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { + this->set_state_(State::RESPONSE_FINISHED, State::IDLE); + break; + } default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index ce22538a85..cd448293db 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -46,6 +46,7 @@ enum class State { STOPPING_MICROPHONE, AWAITING_RESPONSE, STREAMING_RESPONSE, + RESPONSE_FINISHED, }; class VoiceAssistant : public Component { @@ -132,10 +133,10 @@ class VoiceAssistant : public Component { uint8_t *speaker_buffer_; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; + bool wait_for_stream_end_{false}; #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; - bool playing_tts_{false}; #endif bool local_output_{false}; From fd7d3c4332537f58a2a52518c1bfefdca5513551 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:03:39 +1300 Subject: [PATCH 134/270] Fix esp32_improv authorizer with no binary sensors in config (#5546) --- esphome/components/esp32_improv/esp32_improv_component.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 5bdf7d19fe..8a901a79e5 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -107,6 +107,7 @@ void ESP32ImprovComponent::loop() { break; } case improv::STATE_AUTHORIZED: { +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) { if (now - this->authorized_start_ > this->authorized_duration_) { ESP_LOGD(TAG, "Authorization timeout"); @@ -114,6 +115,7 @@ void ESP32ImprovComponent::loop() { return; } } +#endif if (!this->check_identify_()) { this->set_status_indicator_state_((now % 1000) < 500); } @@ -290,8 +292,10 @@ void ESP32ImprovComponent::process_incoming_data_() { void ESP32ImprovComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); +#ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) this->authorized_start_ = millis(); +#endif ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); wifi::global_wifi_component->clear_sta(); } From 1f02096edb29095c8ee5462c4a46a705bba163eb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:11:37 +1300 Subject: [PATCH 135/270] More voice assistant fixes (#5547) --- .../components/i2s_audio/microphone/i2s_audio_microphone.cpp | 2 ++ esphome/components/voice_assistant/voice_assistant.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 44c73eb8fd..ec2fe258c9 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -37,6 +37,8 @@ void I2SAudioMicrophone::setup() { void I2SAudioMicrophone::start() { if (this->is_failed()) return; + if (this->state_ == microphone::STATE_RUNNING) + return; // Already running this->state_ = microphone::STATE_STARTING; } void I2SAudioMicrophone::start_() { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 12fbdc97b4..27dc201073 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -287,6 +287,7 @@ void VoiceAssistant::loop() { } } if (this->wait_for_stream_end_) { + this->cancel_timeout("playing"); break; // We dont want to timeout here as the STREAM_END event will take care of that. } playing = this->speaker_->is_running(); From 5e7ce610a030307ca4cbe9347c42c6dc6fbec7d9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:15:14 +1300 Subject: [PATCH 136/270] Bump version to 2023.10.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index cef733b2f9..cf267a2424 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0b2" +__version__ = "2023.10.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From cc4c0e3e0bee6a88aa898e86bba48755b4c71d80 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Oct 2023 07:23:34 +1300 Subject: [PATCH 137/270] Fix default libretiny manufacturer reported to HA (#5549) --- esphome/components/libretiny/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index b01d342a87..e36c08d522 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -251,7 +251,7 @@ async def component_to_code(config): # setup board config cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_LIBRETINY") - cg.add_build_flag(f"-DUSE_{config[CONF_COMPONENT_ID]}") + cg.add_build_flag(f"-DUSE_{config[CONF_COMPONENT_ID].upper()}") cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) From 51688d40780f2ef203e701b4d224c3abee283c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Bl=C3=A4se?= Date: Tue, 17 Oct 2023 20:39:05 +0200 Subject: [PATCH 138/270] SML: fix incomplete sign extension for abbreviated transmissions (#5544) --- esphome/components/sml/sml_parser.cpp | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index 91b320a30e..3b23522b21 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -88,11 +88,6 @@ uint64_t bytes_to_uint(const bytes &buffer) { for (auto const value : buffer) { val = (val << 8) + value; } - // Some smart meters send 24 bit signed integers. Sign extend to 64 bit if the - // 24 bit value is negative. - if (buffer.size() == 3 && buffer[0] & 0x80) { - val |= 0xFFFFFFFFFF000000; - } return val; } @@ -100,19 +95,15 @@ 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; + // sign extension for abbreviations of leading ones (e.g. 3 byte transmissions, see 6.2.2 of SML protocol definition) + // see https://stackoverflow.com/questions/42534749/signed-extension-from-24-bit-to-32-bit-in-c + if (buffer.size() < 8) { + const int bits = buffer.size() * 8; + const uint64_t m = 1u << (bits - 1); + tmp = (tmp ^ m) - m; } + + val = (int64_t) tmp; return val; } From 2189a40a39951f931b692a5f9aa8326ec41e3726 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:26:47 +1300 Subject: [PATCH 139/270] esp32_improv advertise capabilities and state in ble service data (#5553) --- .../components/esp32_ble/ble_advertising.cpp | 21 +++++++++++++------ .../components/esp32_ble/ble_advertising.h | 1 + .../esp32_improv/esp32_improv_component.cpp | 19 +++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 072bb38c07..59d2398829 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -2,9 +2,9 @@ #ifdef USE_ESP32 -#include "ble_uuid.h" -#include #include +#include +#include "ble_uuid.h" #include "esphome/core/log.h" namespace esphome { @@ -16,8 +16,8 @@ BLEAdvertising::BLEAdvertising() { this->advertising_data_.set_scan_rsp = false; this->advertising_data_.include_name = true; this->advertising_data_.include_txpower = true; - this->advertising_data_.min_interval = 0x20; - this->advertising_data_.max_interval = 0x40; + this->advertising_data_.min_interval = 0; + this->advertising_data_.max_interval = 0; this->advertising_data_.appearance = 0x00; this->advertising_data_.manufacturer_len = 0; this->advertising_data_.p_manufacturer_data = nullptr; @@ -42,6 +42,17 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } +void BLEAdvertising::set_service_data(const std::vector &data) { + delete[] this->advertising_data_.p_service_data; + this->advertising_data_.p_service_data = nullptr; + this->advertising_data_.service_data_len = data.size(); + if (!data.empty()) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->advertising_data_.p_service_data = new uint8_t[data.size()]; + memcpy(this->advertising_data_.p_service_data, data.data(), data.size()); + } +} + void BLEAdvertising::set_manufacturer_data(const std::vector &data) { delete[] this->advertising_data_.p_manufacturer_data; this->advertising_data_.p_manufacturer_data = nullptr; @@ -85,8 +96,6 @@ void BLEAdvertising::start() { this->scan_response_data_.set_scan_rsp = true; this->scan_response_data_.include_name = true; this->scan_response_data_.include_txpower = true; - this->scan_response_data_.min_interval = 0; - this->scan_response_data_.max_interval = 0; this->scan_response_data_.manufacturer_len = 0; this->scan_response_data_.appearance = 0; this->scan_response_data_.flag = 0; diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 9e4e2b7701..16a7dd1d8e 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -21,6 +21,7 @@ class BLEAdvertising { void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_manufacturer_data(const std::vector &data); + void set_service_data(const std::vector &data); void start(); void stop(); diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 8a901a79e5..19340c3dd8 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -189,6 +189,25 @@ void ESP32ImprovComponent::set_state_(improv::State state) { if (state != improv::STATE_STOPPED) this->status_->notify(); } + std::vector service_data(8, 0); + service_data[0] = 0x77; // PR + service_data[1] = 0x46; // IM + service_data[2] = static_cast(state); + + uint8_t capabilities = 0x00; +#ifdef USE_OUTPUT + if (this->status_indicator_ != nullptr) + capabilities |= improv::CAPABILITY_IDENTIFY; +#endif + + service_data[3] = capabilities; + service_data[4] = 0x00; // Reserved + service_data[5] = 0x00; // Reserved + service_data[6] = 0x00; // Reserved + service_data[7] = 0x00; // Reserved + + esp32_ble::global_ble->get_advertising()->set_service_data(service_data); + esp32_ble::global_ble->get_advertising()->start(); } void ESP32ImprovComponent::set_error_(improv::Error error) { From 2aa787f5f0546fbb550ececaffe9d43f019b3053 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:28:03 +1300 Subject: [PATCH 140/270] Bump version to 2023.10.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index cf267a2424..fead0a5f1f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0b3" +__version__ = "2023.10.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 582b8383d248cd1a8cfd17c3bba39b6341868c52 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Oct 2023 16:47:03 +1300 Subject: [PATCH 141/270] Bump version to 2023.10.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fead0a5f1f..30e7fd7b8e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0b4" +__version__ = "2023.10.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 02449f24c9718aa959dbd5401a815685215acd59 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 Oct 2023 22:12:35 +1300 Subject: [PATCH 142/270] Fix voice_assistant without a speaker (#5558) --- esphome/components/voice_assistant/voice_assistant.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 27dc201073..df7853156d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -316,8 +316,8 @@ void VoiceAssistant::loop() { this->speaker_buffer_index_ = 0; memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE); } -#endif this->wait_for_stream_end_ = false; +#endif this->set_state_(State::IDLE, State::IDLE); break; } @@ -586,7 +586,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { +#ifdef USE_SPEAKER this->wait_for_stream_end_ = true; +#endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { From 9579423b24545c2921e83897a96ca2370dff44d7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:42:52 +1300 Subject: [PATCH 143/270] esp32_improv add timeout (#5556) --- esphome/components/esp32_improv/__init__.py | 5 +++++ .../esp32_improv/esp32_improv_component.h | 5 +++++ esphome/components/wifi/wifi_component.cpp | 14 +++++++------- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index fba2e55ae8..49d95d89e5 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -36,6 +36,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_WIFI_TIMEOUT, default="1min" + ): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -53,6 +56,8 @@ async def to_code(config): cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) + cg.add(var.set_wifi_timeout(config[CONF_WIFI_TIMEOUT])) + if CONF_AUTHORIZER in config and config[CONF_AUTHORIZER] is not None: activator = await cg.get_variable(config[CONF_AUTHORIZER]) cg.add(var.set_authorizer(activator)) diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index ba9892d6a5..00c6cf885a 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -51,6 +51,9 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { void set_identify_duration(uint32_t identify_duration) { this->identify_duration_ = identify_duration; } void set_authorized_duration(uint32_t authorized_duration) { this->authorized_duration_ = authorized_duration; } + void set_wifi_timeout(uint32_t wifi_timeout) { this->wifi_timeout_ = wifi_timeout; } + uint32_t get_wifi_timeout() const { return this->wifi_timeout_; } + protected: bool should_start_{false}; bool setup_complete_{false}; @@ -60,6 +63,8 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { uint32_t authorized_start_{0}; uint32_t authorized_duration_; + uint32_t wifi_timeout_{}; + std::vector incoming_data_; wifi::WiFiAP connecting_sta_; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2cb36fe8ea..b08f20de21 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -8,16 +8,16 @@ #include #endif -#include #include -#include "lwip/err.h" +#include #include "lwip/dns.h" +#include "lwip/err.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/hal.h" #include "esphome/core/util.h" -#include "esphome/core/application.h" #ifdef USE_CAPTIVE_PORTAL #include "esphome/components/captive_portal/captive_portal.h" @@ -96,7 +96,7 @@ void WiFiComponent::start() { #endif } #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr) { + if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } @@ -163,8 +163,8 @@ void WiFiComponent::loop() { } #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr) { - if (!this->is_connected()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); } From fa4ba43eb95b22223863916b2b2f19713d00a35b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:25:43 +1300 Subject: [PATCH 144/270] Create IPv4 sockets if ipv6 is not enabled (#5565) --- esphome/components/socket/socket.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 824e04150b..d0fce9198f 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -10,7 +10,7 @@ namespace socket { Socket::~Socket() {} std::unique_ptr socket_ip(int type, int protocol) { -#if LWIP_IPV6 +#if ENABLE_IPV6 return socket(AF_INET6, type, protocol); #else return socket(AF_INET, type, protocol); @@ -18,7 +18,7 @@ std::unique_ptr socket_ip(int type, int protocol) { } socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { -#if LWIP_IPV6 +#if ENABLE_IPV6 if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; @@ -51,7 +51,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri } socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { -#if LWIP_IPV6 +#if ENABLE_IPV6 if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; From f077a5962d6f452bf0ba54cc93904e03784fbc0c Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Wed, 18 Oct 2023 19:00:15 -0500 Subject: [PATCH 145/270] Incorrect ESP32 Strapping PIN Defined (#5563) Co-authored-by: descipher <120155735+GelidusResearch@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 66ba2ffa62..91f006444c 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -18,7 +18,7 @@ _ESP_SDIO_PINS = { 11: "Flash Command", } -_ESP32_STRAPPING_PINS = {0, 2, 4, 12, 15} +_ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15} _LOGGER = logging.getLogger(__name__) From db02c4ea21f034d902872f90b58e8e4eb3ab1f57 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:30:13 +1300 Subject: [PATCH 146/270] Bump version to 2023.10.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 30e7fd7b8e..bb77fa578d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.0" +__version__ = "2023.10.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8c2d9101d5c874cb0e46f09adf7839aa62c0b86e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:36:01 +1300 Subject: [PATCH 147/270] Fix XOR condition (#5567) --- esphome/core/base_automation.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 9b3377f694..50087f3efd 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -52,12 +52,12 @@ template class XorCondition : public Condition { public: explicit XorCondition(const std::vector *> &conditions) : conditions_(conditions) {} bool check(Ts... x) override { - bool xor_state = false; + size_t result = 0; for (auto *condition : this->conditions_) { - xor_state = xor_state ^ condition->check(x...); + result += condition->check(x...); } - return xor_state; + return result == 1; } protected: From 11dba3147d6b5d1e6775e5cfa875dbc2ce6796dc Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 19 Oct 2023 01:53:09 -0500 Subject: [PATCH 148/270] Improv Serial support via USB CDC and JTAG (#5559) --- esphome/components/improv_serial/__init__.py | 13 ++- .../improv_serial/improv_serial_component.cpp | 102 +++++++++++++---- .../improv_serial/improv_serial_component.h | 11 +- esphome/components/logger/logger.cpp | 107 +++++++++++++----- esphome/components/logger/logger.h | 10 ++ 5 files changed, 189 insertions(+), 54 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 311256804b..2b377d77b8 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,10 +1,14 @@ -from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG +from esphome.components import improv_base +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32S3, +) +from esphome.components.logger import USB_CDC from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE import esphome.final_validate as fv -from esphome.components import improv_base AUTO_LOAD = ["improv_base"] CODEOWNERS = ["@esphome/core"] @@ -30,7 +34,10 @@ def validate_logger(config): if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") if CORE.using_esp_idf: - if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]: + if ( + logger_conf[CONF_HARDWARE_UART] == USB_CDC + and get_esp32_variant() == VARIANT_ESP32S3 + ): raise cv.Invalid( "improv_serial does not support the selected logger hardware_uart" ) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 1dd1c9cf6f..600069b781 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -31,26 +31,57 @@ void ImprovSerialComponent::setup() { void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } -int ImprovSerialComponent::available_() { +optional ImprovSerialComponent::read_byte_() { + optional byte; + uint8_t data = 0; #ifdef USE_ARDUINO - return this->hw_serial_->available(); + if (this->hw_serial_->available()) { + this->hw_serial_->readBytes(&data, 1); + byte = data; + } #endif #ifdef USE_ESP_IDF - size_t available; - uart_get_buffered_data_len(this->uart_num_, &available); - return available; + switch (logger::global_logger->get_uart()) { + case logger::UART_SELECTION_UART0: + case logger::UART_SELECTION_UART1: +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ + !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) + case logger::UART_SELECTION_UART2: +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 + if (this->uart_num_ >= 0) { + size_t available; + uart_get_buffered_data_len(this->uart_num_, &available); + if (available) { + uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_PERIOD_MS); + byte = data; + } + } + break; +#if defined(CONFIG_ESP_CONSOLE_USB_CDC) && (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) + case logger::UART_SELECTION_USB_CDC: +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + if (esp_usb_console_available_for_read()) { +#else + if (esp_usb_console_read_available()) { #endif -} - -uint8_t ImprovSerialComponent::read_byte_() { - uint8_t data; -#ifdef USE_ARDUINO - this->hw_serial_->readBytes(&data, 1); + esp_usb_console_read_buf((char *) &data, 1); + byte = data; + } + break; +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) + case logger::UART_SELECTION_USB_SERIAL_JTAG: { + if (usb_serial_jtag_read_bytes((char *) &data, 1, 20 / portTICK_PERIOD_MS)) { + byte = data; + } + break; + } +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 + default: + break; + } #endif -#ifdef USE_ESP_IDF - uart_read_bytes(this->uart_num_, &data, 1, 20 / portTICK_PERIOD_MS); -#endif - return data; + return byte; } void ImprovSerialComponent::write_data_(std::vector &data) { @@ -59,24 +90,49 @@ void ImprovSerialComponent::write_data_(std::vector &data) { this->hw_serial_->write(data.data(), data.size()); #endif #ifdef USE_ESP_IDF - uart_write_bytes(this->uart_num_, data.data(), data.size()); + switch (logger::global_logger->get_uart()) { + case logger::UART_SELECTION_UART0: + case logger::UART_SELECTION_UART1: +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ + !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) + case logger::UART_SELECTION_UART2: +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 + uart_write_bytes(this->uart_num_, data.data(), data.size()); + break; +#if defined(CONFIG_ESP_CONSOLE_USB_CDC) && (defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) + case logger::UART_SELECTION_USB_CDC: { + const char *msg = (char *) data.data(); + esp_usb_console_write_buf(msg, data.size()); + break; + } +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) + case logger::UART_SELECTION_USB_SERIAL_JTAG: + usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); + break; +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 + default: + break; + } #endif } void ImprovSerialComponent::loop() { - const uint32_t now = millis(); - if (now - this->last_read_byte_ > 50) { + if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { + this->last_read_byte_ = 0; this->rx_buffer_.clear(); - this->last_read_byte_ = now; + ESP_LOGV(TAG, "Improv Serial timeout"); } - while (this->available_()) { - uint8_t byte = this->read_byte_(); - if (this->parse_improv_serial_byte_(byte)) { - this->last_read_byte_ = now; + auto byte = this->read_byte_(); + while (byte.has_value()) { + if (this->parse_improv_serial_byte_(byte.value())) { + this->last_read_byte_ = millis(); } else { + this->last_read_byte_ = 0; this->rx_buffer_.clear(); } + byte = this->read_byte_(); } if (this->state_ == improv::STATE_PROVISIONING) { diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 731f9f9984..8583d0762b 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -14,6 +14,13 @@ #endif #ifdef USE_ESP_IDF #include +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32H2) +#include +#endif +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include +#endif #endif namespace esphome { @@ -26,6 +33,7 @@ enum ImprovSerialType : uint8_t { TYPE_RPC_RESPONSE = 0x04 }; +static const uint16_t IMPROV_SERIAL_TIMEOUT = 100; static const uint8_t IMPROV_SERIAL_VERSION = 1; class ImprovSerialComponent : public Component, public improv_base::ImprovBase { @@ -48,8 +56,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { std::vector build_rpc_settings_response_(improv::Command command); std::vector build_version_info_(); - int available_(); - uint8_t read_byte_(); + optional read_byte_(); void write_data_(std::vector &data); #ifdef USE_ARDUINO diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index d1f3149d84..2d2524b5f4 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -3,8 +3,21 @@ #ifdef USE_ESP_IDF #include + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32H2) +#include +#include +#include +#endif + #include "freertos/FreeRTOS.h" #include "esp_idf_version.h" + +#include +#include +#include + #endif // USE_ESP_IDF #if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) @@ -93,6 +106,58 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif +#ifdef USE_ESP_IDF +void Logger::init_uart_() { + 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; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + uart_config.source_clk = UART_SCLK_DEFAULT; +#endif + uart_param_config(this->uart_num_, &uart_config); + const int uart_buffer_size = tx_buffer_size_; + // Install UART driver using an event queue here + uart_driver_install(this->uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); +} + +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +void Logger::init_usb_cdc_() {} +#endif + +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32H2) +void Logger::init_usb_serial_jtag_() { + setvbuf(stdin, NULL, _IONBF, 0); // Disable buffering on stdin + + // Minicom, screen, idf_monitor send CR when ENTER key is pressed + esp_vfs_dev_usb_serial_jtag_set_rx_line_endings(ESP_LINE_ENDINGS_CR); + // Move the caret to the beginning of the next line on '\n' + esp_vfs_dev_usb_serial_jtag_set_tx_line_endings(ESP_LINE_ENDINGS_CRLF); + + // Enable non-blocking mode on stdin and stdout + fcntl(fileno(stdout), F_SETFL, 0); + fcntl(fileno(stdin), F_SETFL, 0); + + usb_serial_jtag_driver_config_t usb_serial_jtag_config{}; + usb_serial_jtag_config.rx_buffer_size = 512; + usb_serial_jtag_config.tx_buffer_size = 512; + + esp_err_t ret = ESP_OK; + // Install USB-SERIAL-JTAG driver for interrupt-driven reads and writes + ret = usb_serial_jtag_driver_install(&usb_serial_jtag_config); + if (ret != ESP_OK) { + return; + } + + // Tell vfs to use usb-serial-jtag driver + esp_vfs_usb_serial_jtag_use_driver(); +} +#endif +#endif + int HOT Logger::level_for(const char *tag) { // Uses std::vector<> for low memory footprint, though the vector // could be sorted to minimize lookup times. This feature isn't used that @@ -120,19 +185,19 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { #ifdef USE_ESP_IDF if ( #if defined(USE_ESP32_VARIANT_ESP32S2) - uart_ == UART_SELECTION_USB_CDC + this->uart_ == UART_SELECTION_USB_CDC #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) - uart_ == UART_SELECTION_USB_SERIAL_JTAG + this->uart_ == UART_SELECTION_USB_SERIAL_JTAG #elif defined(USE_ESP32_VARIANT_ESP32S3) - uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG + this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG #else /* DISABLES CODE */ (false) // NOLINT #endif ) { puts(msg); } else { - uart_write_bytes(uart_num_, msg, strlen(msg)); - uart_write_bytes(uart_num_, "\n", 1); + uart_write_bytes(this->uart_num_, msg, strlen(msg)); + uart_write_bytes(this->uart_num_, "\n", 1); } #endif } @@ -209,48 +274,38 @@ void Logger::pre_setup() { } #endif // USE_ARDUINO #ifdef USE_ESP_IDF - uart_num_ = UART_NUM_0; - switch (uart_) { + this->uart_num_ = UART_NUM_0; + switch (this->uart_) { case UART_SELECTION_UART0: - uart_num_ = UART_NUM_0; + this->uart_num_ = UART_NUM_0; break; case UART_SELECTION_UART1: - uart_num_ = UART_NUM_1; + this->uart_num_ = UART_NUM_1; break; #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) && !defined(USE_ESP32_VARIANT_ESP32H2) case UART_SELECTION_UART2: - uart_num_ = UART_NUM_2; + this->uart_num_ = UART_NUM_2; break; #endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 && // !USE_ESP32_VARIANT_ESP32H2 #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) case UART_SELECTION_USB_CDC: - uart_num_ = -1; + this->uart_num_ = -1; + this->init_usb_cdc_(); break; #endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32H2) case UART_SELECTION_USB_SERIAL_JTAG: - uart_num_ = -1; + this->uart_num_ = -1; + this->init_usb_serial_jtag_(); break; #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32H2 } - 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; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - uart_config.source_clk = UART_SCLK_DEFAULT; -#endif - 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); + if (this->uart_num_ >= 0) { + this->init_uart_(); } #endif // USE_ESP_IDF } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index de272934bf..3816b1dd14 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -107,6 +107,16 @@ class Logger : public Component { #endif protected: +#ifdef USE_ESP_IDF + void init_uart_(); +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + void init_usb_cdc_(); +#endif +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32H2) + void init_usb_serial_jtag_(); +#endif +#endif void write_header_(int level, const char *tag, int line); void write_footer_(); void log_message_(int level, const char *tag, int offset = 0); From 76ab9237807114e58abaab9754b586aca4955566 Mon Sep 17 00:00:00 2001 From: Trent Houliston Date: Fri, 20 Oct 2023 08:28:05 +1100 Subject: [PATCH 149/270] Publish the `pulse_meter` total when setting the total (#5475) --- CODEOWNERS | 2 +- esphome/components/pulse_meter/pulse_meter_sensor.cpp | 7 +++++++ esphome/components/pulse_meter/pulse_meter_sensor.h | 3 ++- esphome/components/pulse_meter/sensor.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d7cf7269ab..0ec9f66eb1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -225,7 +225,7 @@ esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @cstaahl @stevebaxter +esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index be5fad6fe5..4afecb95a3 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -7,6 +7,13 @@ namespace pulse_meter { static const char *const TAG = "pulse_meter"; +void PulseMeterSensor::set_total_pulses(uint32_t pulses) { + this->total_pulses_ = pulses; + if (this->total_sensor_ != nullptr) { + this->total_sensor_->publish_state(this->total_pulses_); + } +} + void PulseMeterSensor::setup() { this->pin_->setup(); this->isr_pin_ = pin_->to_isr(); diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index f376ea48a5..6c1c4353db 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -20,7 +20,8 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; } void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; } void set_filter_mode(InternalFilterMode mode) { this->filter_mode_ = mode; } - void set_total_pulses(uint32_t pulses) { this->total_pulses_ = pulses; } + + void set_total_pulses(uint32_t pulses); void setup() override; void loop() override; diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 26bc6b189b..59ffa58c21 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -19,7 +19,7 @@ from esphome.const import ( ) from esphome.core import CORE -CODEOWNERS = ["@stevebaxter", "@cstaahl"] +CODEOWNERS = ["@stevebaxter", "@cstaahl", "@TrentHouliston"] pulse_meter_ns = cg.esphome_ns.namespace("pulse_meter") From c47f8fc02c993f34f14c592653b9d21af7941697 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sat, 21 Oct 2023 06:04:07 +0200 Subject: [PATCH 150/270] Remove explicit cast for IPAddress (#5574) * Remove explicit cast for IPAddress * Make linter happy --- esphome/components/mqtt/mqtt_backend_libretiny.h | 4 +--- tests/test9.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 5373a1926a..ac4d4298fc 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -19,9 +19,7 @@ class MQTTBackendLibreTiny final : public MQTTBackend { void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { mqtt_client_.setWill(topic, qos, retain, payload); } - void set_server(network::IPAddress ip, uint16_t port) final { - mqtt_client_.setServer(IPAddress(static_cast(ip)), port); - } + void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } #if ASYNC_TCP_SSL_ENABLED void set_secure(bool secure) { mqtt_client.setSecure(secure); } diff --git a/tests/test9.yaml b/tests/test9.yaml index ccf5f4b5b0..d660b4f24a 100644 --- a/tests/test9.yaml +++ b/tests/test9.yaml @@ -26,3 +26,9 @@ sensor: name: ADC pin: GPIO23 update_interval: 1s + +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant From f018fde36904273409b7da97103bdd7d0e2253b6 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 23 Oct 2023 08:41:16 +0200 Subject: [PATCH 151/270] Set addr type when copy from ip4_addr_t (#5583) --- esphome/components/network/ip_address.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 7a4d394805..03ba6e85d5 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -34,7 +34,12 @@ struct IPAddress { } IPAddress(const ip_addr_t *other_ip) { ip_addr_copy(ip_addr_, *other_ip); } IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); } - IPAddress(ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); } + IPAddress(ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); +#if USE_ESP32 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } #if USE_ARDUINO IPAddress(const arduino_ns::IPAddress &other_ip) { ip_addr_set_ip4_u32(&ip_addr_, other_ip); } #endif From 0807d60c6a84a6b5200f1383bcd61f6fba1ff21c Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 23 Oct 2023 11:26:23 -0700 Subject: [PATCH 152/270] fix canbus send config (#5585) Co-authored-by: Samuel Sieb --- esphome/components/canbus/__init__.py | 25 +++++++++++-------------- tests/test1.yaml | 4 ++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index f49398858c..76e77021ad 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -17,11 +17,12 @@ CONF_ON_FRAME = "on_frame" def validate_id(config): - can_id = config[CONF_CAN_ID] - id_ext = config[CONF_USE_EXTENDED_ID] - if not id_ext: - if can_id > 0x7FF: - raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + if CONF_CAN_ID in config: + can_id = config[CONF_CAN_ID] + id_ext = config[CONF_USE_EXTENDED_ID] + if not id_ext: + if can_id > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") return config @@ -151,22 +152,18 @@ async def canbus_action_to_code(config, action_id, template_arg, args): if can_id := config.get(CONF_CAN_ID): can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) - use_extended_id = await cg.templatable( - config[CONF_USE_EXTENDED_ID], args, cg.uint32 - ) - cg.add(var.set_use_extended_id(use_extended_id)) + cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) - remote_transmission_request = await cg.templatable( - config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool + cg.add( + var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST]) ) - cg.add(var.set_remote_transmission_request(remote_transmission_request)) data = config[CONF_DATA] - if isinstance(data, bytes): - data = [int(x) for x in data] if cg.is_template(data): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: + if isinstance(data, bytes): + data = [int(x) for x in data] cg.add(var.set_data_static(data)) return var diff --git a/tests/test1.yaml b/tests/test1.yaml index c504012481..ceafe33b9d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3326,6 +3326,10 @@ text_sensor: canbus_id: mcp2515_can can_id: 23 data: [0x10, 0x20, 0x30] + - canbus.send: + canbus_id: mcp2515_can + can_id: 23 + data: !lambda return {0x10, 0x20, 0x30}; - canbus.send: canbus_id: esp32_internal_can can_id: 23 From 33e0f16b3b1fefbf9f0189844ef7f6b74014310b Mon Sep 17 00:00:00 2001 From: dentra Date: Mon, 23 Oct 2023 21:29:32 +0300 Subject: [PATCH 153/270] Allow set climate preset to NONE (#5588) --- esphome/components/climate/climate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 1680601279..ea24cab954 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -213,6 +213,8 @@ ClimateCall &ClimateCall::set_preset(const std::string &preset) { this->set_preset(CLIMATE_PRESET_SLEEP); } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { this->set_preset(CLIMATE_PRESET_ACTIVITY); + } else if (str_equals_case_insensitive(preset, "NONE")) { + this->set_preset(CLIMATE_PRESET_NONE); } else { if (this->parent_->get_traits().supports_custom_preset(preset)) { this->custom_preset_ = preset; From 9b1e1bf56c3e7882af6013e1bed4600fcaea91d9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 24 Oct 2023 08:32:26 +1300 Subject: [PATCH 154/270] Bump version to 2023.10.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index bb77fa578d..6d15e304a5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.1" +__version__ = "2023.10.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 96dc7f025967f0ad1e396f2c58eec92187a1d5cf Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 23 Oct 2023 18:43:54 -0500 Subject: [PATCH 155/270] Set IP address `type` only when IPv4 and IPv6 are both enabled (#5595) --- esphome/components/network/ip_address.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 03ba6e85d5..7bf09078be 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -36,7 +36,7 @@ struct IPAddress { IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); } IPAddress(ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); -#if USE_ESP32 +#if USE_ESP32 && LWIP_IPV6 ip_addr_.type = IPADDR_TYPE_V4; #endif } From 899d280ac77bb91123d54bf1ca1755ffb59f4a38 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:56:25 +1300 Subject: [PATCH 156/270] Bump version to 2023.10.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6d15e304a5..e8692dc43c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.2" +__version__ = "2023.10.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 35039b45e41cdb72d60bcab3a9c6da6c7fc1d908 Mon Sep 17 00:00:00 2001 From: Roger Busser Date: Wed, 25 Oct 2023 20:31:39 +0200 Subject: [PATCH 157/270] Update current_based_cover bugfix (#5587) --- esphome/components/current_based/current_based_cover.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 17f67002a3..8404e07894 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -104,7 +104,8 @@ void CurrentBasedCover::loop() { ESP_LOGD(TAG, "'%s' - Close position reached. Took %.1fs.", this->name_.c_str(), dur); this->direction_idle_(COVER_CLOSED); } - } else if (now - this->start_dir_time_ > this->max_duration_) { + } + if (now - this->start_dir_time_ > this->max_duration_) { ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); this->direction_idle_(); } From 1282a15b14dad0d7b7b15ffddddbc58d6c6f50d3 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 29 Oct 2023 19:35:33 +0100 Subject: [PATCH 158/270] Fixes ip include on arduino 2.7.4 (#5620) --- esphome/components/network/ip_address.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 7bf09078be..709524c9d1 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -3,7 +3,11 @@ #include #include #include +#include "esphome/core/macros.h" + +#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) #include +#endif #if USE_ARDUINO #include From f96a839bcf79e3e6b86dfc87627d9b8db47f2bde Mon Sep 17 00:00:00 2001 From: Dewet Diener Date: Sun, 29 Oct 2023 19:05:18 +0000 Subject: [PATCH 159/270] Fix bug when requesting italic gfonts (#5623) --- esphome/components/font/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 2bd6beeaeb..7e34dff22d 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -137,11 +137,10 @@ def validate_weight_name(value): def download_gfonts(value): - wght = value[CONF_WEIGHT] - if value[CONF_ITALIC]: - wght = f"1,{wght}" - name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" - url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}" + name = ( + f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}" + ) + url = f"https://fonts.googleapis.com/css2?family={name}" path = _compute_gfonts_local_path(value) if path.is_file(): From a1845e1e721efedcb2c0c4cdb509bbb4b85d5dfa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:00:47 +1300 Subject: [PATCH 160/270] Handle enum type in tuya text_sensor (#5626) --- esphome/components/tuya/text_sensor/tuya_text_sensor.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 602595e89d..fbe511811f 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -1,5 +1,5 @@ -#include "esphome/core/log.h" #include "tuya_text_sensor.h" +#include "esphome/core/log.h" namespace esphome { namespace tuya { @@ -19,6 +19,12 @@ void TuyaTextSensor::setup() { this->publish_state(data); break; } + case TuyaDatapointType::ENUM: { + std::string data = to_string(datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); + this->publish_state(data); + break; + } default: ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type); break; From 6d991a1fc8eff9badab9b50ab6ec0f702f14c1f3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Oct 2023 13:59:58 +1300 Subject: [PATCH 161/270] Bump version to 2023.10.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e8692dc43c..47555cc144 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.3" +__version__ = "2023.10.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 0ea4de5f4cd3543569cfb174896daa5a4a6f2150 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:02:49 +1300 Subject: [PATCH 162/270] Add connection triggers to api (#5628) --- esphome/components/api/__init__.py | 22 ++++++++ esphome/components/api/api_connection.cpp | 62 +++++++++++++---------- esphome/components/api/api_connection.h | 2 + esphome/components/api/api_server.cpp | 1 + esphome/components/api/api_server.h | 8 +++ 5 files changed, 68 insertions(+), 27 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 1076ebc707..ec1a56bd2c 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -45,6 +45,8 @@ SERVICE_ARG_NATIVE_TYPES = { "string[]": cg.std_vector.template(cg.std_string), } CONF_ENCRYPTION = "encryption" +CONF_ON_CLIENT_CONNECTED = "on_client_connected" +CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" def validate_encryption_key(value): @@ -87,6 +89,12 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_KEY): validate_encryption_key, } ), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -116,6 +124,20 @@ async def to_code(config): cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) + if CONF_ON_CLIENT_CONNECTED in config: + await automation.build_automation( + var.get_client_connected_trigger(), + [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + config[CONF_ON_CLIENT_CONNECTED], + ) + + if CONF_ON_CLIENT_DISCONNECTED in config: + await automation.build_automation( + var.get_client_disconnected_trigger(), + [(cg.std_string, "client_info"), (cg.std_string, "client_address")], + config[CONF_ON_CLIENT_DISCONNECTED], + ) + if encryption_config := config.get(CONF_ENCRYPTION): decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3172b71fa2..65ba941c1c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -31,9 +31,9 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) - helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -41,14 +41,16 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa void APIConnection::start() { this->last_traffic_ = millis(); - APIError err = helper_->init(); + APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); return; } - client_info_ = helper_->getpeername(); - helper_->set_log_info(client_info_); + this->client_info_ = helper_->getpeername(); + this->client_peername_ = this->client_info_; + this->helper_->set_log_info(this->client_info_); } APIConnection::~APIConnection() { @@ -67,7 +69,7 @@ void APIConnection::loop() { // when network is disconnected force disconnect immediately // don't wait for timeout this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", this->client_combined_info_.c_str()); return; } if (this->next_close_) { @@ -77,24 +79,26 @@ void APIConnection::loop() { return; } - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + api_error_to_str(err), errno); return; } ReadPacketBuffer buffer; - err = helper_->read_packet(&buffer); + err = this->helper_->read_packet(&buffer); if (err == APIError::WOULD_BLOCK) { // pass } else if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); } return; } else { @@ -114,7 +118,7 @@ void APIConnection::loop() { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > (keepalive * 5) / 2) { on_fatal_error(); - ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); + ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); } } else if (now - this->last_traffic_ > keepalive) { ESP_LOGVV(TAG, "Sending keepalive PING..."); @@ -168,7 +172,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str()); + ESP_LOGD(TAG, "%s requested disconnected", this->client_combined_info_.c_str()); this->next_close_ = true; DisconnectResponse resp; return resp; @@ -1006,12 +1010,14 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin } HelloResponse APIConnection::hello(const HelloRequest &msg) { - this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; - this->helper_->set_log_info(client_info_); + this->client_info_ = msg.client_info; + this->client_peername_ = this->helper_->getpeername(); + this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; + this->helper_->set_log_info(this->client_combined_info_); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), - this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), + this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1029,9 +1035,9 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: Connected successfully", this->client_combined_info_.c_str()); this->connection_state_ = ConnectionState::AUTHENTICATED; - + this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); @@ -1105,10 +1111,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) return false; if (!this->helper_->can_write_without_blocking()) { delay(0); - APIError err = helper_->loop(); + APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + api_error_to_str(err), errno); return false; } if (!this->helper_->can_write_without_blocking()) { @@ -1127,9 +1134,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), + errno); } return false; } @@ -1138,11 +1146,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_combined_info_.c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); + ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_combined_info_.c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 2a62c2faff..0c7d6ebaf4 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -202,6 +202,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr helper_; std::string client_info_; + std::string client_peername_; + std::string client_combined_info_; uint32_t client_api_version_major_{0}; uint32_t client_api_version_minor_{0}; #ifdef USE_ESP32_CAMERA diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 54266ff0f0..eb9af2dcea 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -111,6 +111,7 @@ void APIServer::loop() { [](const std::unique_ptr &conn) { return !conn->remove_; }); // print disconnection messages for (auto it = new_end; it != this->clients_.end(); ++it) { + this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_); ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str()); } // resize vector diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a4454d4b84..a84c1225e5 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -4,6 +4,7 @@ #include "api_pb2.h" #include "api_pb2_service.h" #include "esphome/components/socket/socket.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/defines.h" @@ -103,6 +104,11 @@ class APIServer : public Component, public Controller { const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } + Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_disconnected_trigger() const { + return this->client_disconnected_trigger_; + } + protected: std::unique_ptr socket_ = nullptr; uint16_t port_{6053}; @@ -112,6 +118,8 @@ class APIServer : public Component, public Controller { std::string password_; std::vector state_subs_; std::vector user_services_; + Trigger *client_connected_trigger_ = new Trigger(); + Trigger *client_disconnected_trigger_ = new Trigger(); #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); From eae3089201f21a8624198080c95264267c87bd3e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:16:42 +1300 Subject: [PATCH 163/270] Add on_client_connected and disconnected to voice assistant (#5629) --- esphome/components/api/__init__.py | 4 +- esphome/components/api/api_connection.cpp | 22 +++++++-- esphome/components/api/api_connection.h | 10 ++-- esphome/components/api/api_server.cpp | 24 ---------- esphome/components/api/api_server.h | 6 --- .../components/voice_assistant/__init__.py | 22 +++++++++ .../voice_assistant/voice_assistant.cpp | 46 ++++++++++++++++--- .../voice_assistant/voice_assistant.h | 13 +++++- esphome/const.py | 2 + 9 files changed, 98 insertions(+), 51 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index ec1a56bd2c..d6b4416af8 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -18,6 +18,8 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_EVENT, CONF_TAG, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, ) from esphome.core import coroutine_with_priority @@ -45,8 +47,6 @@ SERVICE_ARG_NATIVE_TYPES = { "string[]": cg.std_vector.template(cg.std_string), } CONF_ENCRYPTION = "encryption" -CONF_ON_CLIENT_CONNECTED = "on_client_connected" -CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" def validate_encryption_key(value): diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 65ba941c1c..ea7a53266f 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -59,6 +59,11 @@ APIConnection::~APIConnection() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } #endif +#ifdef USE_VOICE_ASSISTANT + if (voice_assistant::global_voice_assistant->get_api_connection() == this) { + voice_assistant::global_voice_assistant->client_subscription(this, false); + } +#endif } void APIConnection::loop() { @@ -911,14 +916,17 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(const VoiceAssistantRequest &msg) { - if (!this->voice_assistant_subscription_) - return false; - - return this->send_voice_assistant_request(msg); +void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); + } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + if (msg.error) { voice_assistant::global_voice_assistant->failed_to_start(); return; @@ -931,6 +939,10 @@ void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &ms }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + voice_assistant::global_voice_assistant->on_event(msg); } } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0c7d6ebaf4..82872c75de 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -121,10 +121,7 @@ class APIConnection : public APIServerConnection { #endif #ifdef USE_VOICE_ASSISTANT - void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { - this->voice_assistant_subscription_ = msg.subscribe; - } - bool request_voice_assistant(const VoiceAssistantRequest &msg); + void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif @@ -183,6 +180,8 @@ class APIConnection : public APIServerConnection { } bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; + std::string get_client_combined_info() const { return this->client_combined_info_; } + protected: friend APIServer; @@ -215,9 +214,6 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; -#ifdef USE_VOICE_ASSISTANT - bool voice_assistant_subscription_{false}; -#endif bool next_close_ = false; APIServer *parent_; InitialStateIterator initial_state_iterator_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index eb9af2dcea..4b113dbd07 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -323,30 +323,6 @@ void APIServer::on_shutdown() { delay(10); } -#ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant(const std::string &conversation_id, uint32_t flags, - const api::VoiceAssistantAudioSettings &audio_settings) { - VoiceAssistantRequest msg; - msg.start = true; - msg.conversation_id = conversation_id; - msg.flags = flags; - msg.audio_settings = audio_settings; - for (auto &c : this->clients_) { - if (c->request_voice_assistant(msg)) - return true; - } - return false; -} -void APIServer::stop_voice_assistant() { - VoiceAssistantRequest msg; - msg.start = false; - for (auto &c : this->clients_) { - if (c->request_voice_assistant(msg)) - return; - } -} -#endif - #ifdef USE_ALARM_CONTROL_PANEL void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { if (obj->is_internal()) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a84c1225e5..55b18a0f60 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -81,12 +81,6 @@ class APIServer : public Component, public Controller { void request_time(); #endif -#ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(const std::string &conversation_id, uint32_t flags, - const api::VoiceAssistantAudioSettings &audio_settings); - void stop_voice_assistant(); -#endif - #ifdef USE_ALARM_CONTROL_PANEL void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 14176ad7cf..3270b9f370 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -6,6 +6,8 @@ from esphome.const import ( CONF_MICROPHONE, CONF_SPEAKER, CONF_MEDIA_PLAYER, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, ) from esphome import automation from esphome.automation import register_action, register_condition @@ -80,6 +82,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_TTS_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), ) @@ -155,6 +163,20 @@ async def to_code(config): config[CONF_ON_ERROR], ) + if CONF_ON_CLIENT_CONNECTED in config: + await automation.build_automation( + var.get_client_connected_trigger(), + [], + config[CONF_ON_CLIENT_CONNECTED], + ) + + if CONF_ON_CLIENT_DISCONNECTED in config: + await automation.build_automation( + var.get_client_disconnected_trigger(), + [], + config[CONF_ON_CLIENT_DISCONNECTED], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index df7853156d..d15d702d4b 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -127,8 +127,8 @@ int VoiceAssistant::read_microphone_() { } void VoiceAssistant::loop() { - if (this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && - this->state_ != State::STOPPING_MICROPHONE && !api::global_api_server->is_connected()) { + if (this->api_client_ == nullptr && this->state_ != State::IDLE && this->state_ != State::STOP_MICROPHONE && + this->state_ != State::STOPPING_MICROPHONE) { if (this->mic_->is_running() || this->state_ == State::STARTING_MICROPHONE) { this->set_state_(State::STOP_MICROPHONE, State::IDLE); } else { @@ -213,7 +213,14 @@ void VoiceAssistant::loop() { audio_settings.noise_suppression_level = this->noise_suppression_level_; audio_settings.auto_gain = this->auto_gain_; audio_settings.volume_multiplier = this->volume_multiplier_; - if (!api::global_api_server->start_voice_assistant(this->conversation_id_, flags, audio_settings)) { + + api::VoiceAssistantRequest msg; + msg.start = true; + msg.conversation_id = this->conversation_id_; + msg.flags = flags; + msg.audio_settings = audio_settings; + + if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); this->continuous_ = false; @@ -326,6 +333,28 @@ void VoiceAssistant::loop() { } } +void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscribe) { + if (!subscribe) { + if (this->api_client_ == nullptr || client != this->api_client_) { + ESP_LOGE(TAG, "Client attempting to unsubscribe that is not the current API Client"); + return; + } + this->api_client_ = nullptr; + this->client_disconnected_trigger_->trigger(); + return; + } + + if (this->api_client_ != nullptr) { + ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); + ESP_LOGE(TAG, "Current client: %s", this->api_client_->get_client_combined_info().c_str()); + ESP_LOGE(TAG, "New client: %s", client->get_client_combined_info().c_str()); + return; + } + + this->api_client_ = client; + this->client_connected_trigger_->trigger(); +} + static const LogString *voice_assistant_state_to_string(State state) { switch (state) { case State::IDLE: @@ -408,7 +437,7 @@ void VoiceAssistant::start_streaming(struct sockaddr_storage *addr, uint16_t por } void VoiceAssistant::request_start(bool continuous, bool silence_detection) { - if (!api::global_api_server->is_connected()) { + if (this->api_client_ == nullptr) { ESP_LOGE(TAG, "No API client connected"); this->set_state_(State::IDLE, State::IDLE); this->continuous_ = false; @@ -459,9 +488,14 @@ void VoiceAssistant::request_stop() { } void VoiceAssistant::signal_stop_() { - ESP_LOGD(TAG, "Signaling stop..."); - api::global_api_server->stop_voice_assistant(); memset(&this->dest_addr_, 0, sizeof(this->dest_addr_)); + if (this->api_client_ == nullptr) { + return; + } + ESP_LOGD(TAG, "Signaling stop..."); + api::VoiceAssistantRequest msg; + msg.start = false; + this->api_client_->send_voice_assistant_request(msg); } void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index cd448293db..a265522bca 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -8,8 +8,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" -#include "esphome/components/api/api_server.h" #include "esphome/components/microphone/microphone.h" #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" @@ -109,6 +109,12 @@ class VoiceAssistant : public Component { Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } + + void client_subscription(api::APIConnection *client, bool subscribe); + api::APIConnection *get_api_connection() const { return this->api_client_; } + protected: int read_microphone_(); void set_state_(State state); @@ -127,6 +133,11 @@ class VoiceAssistant : public Component { Trigger<> *end_trigger_ = new Trigger<>(); Trigger *error_trigger_ = new Trigger(); + Trigger<> *client_connected_trigger_ = new Trigger<>(); + Trigger<> *client_disconnected_trigger_ = new Trigger<>(); + + api::APIConnection *api_client_{nullptr}; + microphone::Microphone *mic_{nullptr}; #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; diff --git a/esphome/const.py b/esphome/const.py index 47555cc144..32e7221d0c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -481,6 +481,8 @@ CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +CONF_ON_CLIENT_CONNECTED = "on_client_connected" +CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" CONF_ON_CONNECT = "on_connect" CONF_ON_CONTROL = "on_control" CONF_ON_DISCONNECT = "on_disconnect" From ef2531edf3f729a14e01ac4ef235e3727246fc3b Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Tue, 31 Oct 2023 17:30:42 -0400 Subject: [PATCH 164/270] Ensure that all uses of strncpy in wifi component are safe. (#5636) --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 8 ++++---- esphome/components/wifi/wifi_component_esp8266.cpp | 8 ++++---- esphome/components/wifi/wifi_component_esp_idf.cpp | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b08f20de21..bd267fb0fd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -261,8 +261,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; - strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid)); - strncpy(save.password, password.c_str(), sizeof(save.password)); + strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); + strncpy(save.password, password.c_str(), sizeof(save.password) - 1); this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 17b15757ef..c68c1b950c 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -164,8 +164,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid) - 1); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password) - 1); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -661,7 +661,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid) - 1); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -672,7 +672,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid) - 1); } conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a48c6c711d..b289157bee 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -230,8 +230,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); + strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid) - 1); + strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password) - 1); if (ap.get_bssid().has_value()) { conf.bssid_set = 1; @@ -759,7 +759,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; struct softap_config conf {}; - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); + strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid) - 1); conf.ssid_len = static_cast(ap.get_ssid().size()); conf.channel = ap.get_channel().value_or(1); conf.ssid_hidden = ap.get_hidden(); @@ -771,7 +771,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); + strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password) - 1); } ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 34ecaf887d..9d88848567 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -275,8 +275,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid) - 1); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password) - 1); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -823,7 +823,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid) - 1); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -834,7 +834,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password)); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password) - 1); } // pairwise cipher of SoftAP, group cipher will be derived using this. From b9d4e2e5014a08c98374494150f8c626c775e65b Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Tue, 31 Oct 2023 22:32:29 +0100 Subject: [PATCH 165/270] Remove some explicit IPAddress casts (#5639) --- esphome/components/captive_portal/captive_portal.cpp | 2 +- esphome/components/mqtt/mqtt_backend_esp8266.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index cc78528e46..78eee4b226 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -48,7 +48,7 @@ void CaptivePortal::start() { this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", IPAddress(ip)); + this->dns_server_->start(53, "*", ip); #endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 981d27693f..06d4993bdf 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -19,7 +19,7 @@ class MQTTBackendESP8266 final : public MQTTBackend { void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { mqtt_client_.setWill(topic, qos, retain, payload); } - void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(IPAddress(ip), port); } + void set_server(network::IPAddress ip, uint16_t port) final { mqtt_client_.setServer(ip, port); } void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } #if ASYNC_TCP_SSL_ENABLED void set_secure(bool secure) { mqtt_client.setSecure(secure); } From b99be250a06c031013fc4891533f0225138114a8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:19:16 +1300 Subject: [PATCH 166/270] Bump version to 2023.10.5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 32e7221d0c..046895447c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.4" +__version__ = "2023.10.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4e65aac7ae2a04daee264142fa51238e1383b18d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:09:17 +1300 Subject: [PATCH 167/270] Revert "Ensure that all uses of strncpy in wifi component are safe." (#5662) --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 8 ++++---- esphome/components/wifi/wifi_component_esp8266.cpp | 8 ++++---- esphome/components/wifi/wifi_component_esp_idf.cpp | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index bd267fb0fd..b08f20de21 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -261,8 +261,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; - strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); - strncpy(save.password, password.c_str(), sizeof(save.password) - 1); + strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid)); + strncpy(save.password, password.c_str(), sizeof(save.password)); this->pref_.save(&save); // ensure it's written immediately global_preferences->sync(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index c68c1b950c..17b15757ef 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -164,8 +164,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid) - 1); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password) - 1); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -661,7 +661,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid) - 1); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -672,7 +672,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid) - 1); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); } conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b289157bee..a48c6c711d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -230,8 +230,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { struct station_config conf {}; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid) - 1); - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password) - 1); + strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); + strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); if (ap.get_bssid().has_value()) { conf.bssid_set = 1; @@ -759,7 +759,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; struct softap_config conf {}; - strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid) - 1); + strncpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), sizeof(conf.ssid)); conf.ssid_len = static_cast(ap.get_ssid().size()); conf.channel = ap.get_channel().value_or(1); conf.ssid_hidden = ap.get_hidden(); @@ -771,7 +771,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.password = 0; } else { conf.authmode = AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password) - 1); + strncpy(reinterpret_cast(conf.password), ap.get_password().c_str(), sizeof(conf.password)); } ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 9d88848567..34ecaf887d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -275,8 +275,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid) - 1); - strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password) - 1); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -823,7 +823,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid) - 1); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -834,7 +834,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password) - 1); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.password)); } // pairwise cipher of SoftAP, group cipher will be derived using this. From aa176610024a794a55679c96e4ea823e919d38af Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:11:58 +1300 Subject: [PATCH 168/270] Bump version to 2023.10.6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 046895447c..6742eb00f8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.5" +__version__ = "2023.10.6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fff2d01420a8b76cdfaeb79aa7d9098440e6bdca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:13:56 +1300 Subject: [PATCH 169/270] Bump version to 2023.11.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 11197ef6c8..83c596a071 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.10.6" +__version__ = "2023.11.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From dbb1263a361a8c80bc201cef0ea5f3d07130e67d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:34:44 +1300 Subject: [PATCH 170/270] Handle nanoseconds in config (#5695) --- esphome/config_validation.py | 21 +++++++++++++++++++- esphome/core/__init__.py | 37 +++++++++++++++++++++++++++-------- esphome/cpp_generator.py | 3 +++ tests/unit_tests/test_core.py | 8 +++++--- 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index d2b381215e..eb347d0a4d 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -66,6 +66,7 @@ from esphome.core import ( TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, + TimePeriodNanoseconds, TimePeriodSeconds, TimePeriodMinutes, ) @@ -718,6 +719,8 @@ def time_period_str_unit(value): raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { + "ns": "nanoseconds", + "nanoseconds": "nanoseconds", "us": "microseconds", "microseconds": "microseconds", "ms": "milliseconds", @@ -739,7 +742,10 @@ def time_period_str_unit(value): raise Invalid(f"Expected time period with unit, got {value}") kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] - return TimePeriod(**{kwarg: float(match.group(1))}) + try: + return TimePeriod(**{kwarg: float(match.group(1))}) + except ValueError as e: + raise Invalid(e) from e def time_period_in_milliseconds_(value): @@ -749,10 +755,18 @@ def time_period_in_milliseconds_(value): def time_period_in_microseconds_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is microseconds") return TimePeriodMicroseconds(**value.as_dict()) +def time_period_in_nanoseconds_(value): + return TimePeriodNanoseconds(**value.as_dict()) + + def time_period_in_seconds_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is seconds") if value.microseconds is not None and value.microseconds != 0: raise Invalid("Maximum precision is seconds") if value.milliseconds is not None and value.milliseconds != 0: @@ -761,6 +775,8 @@ def time_period_in_seconds_(value): def time_period_in_minutes_(value): + if value.nanoseconds is not None and value.nanoseconds != 0: + raise Invalid("Maximum precision is minutes") if value.microseconds is not None and value.microseconds != 0: raise Invalid("Maximum precision is minutes") if value.milliseconds is not None and value.milliseconds != 0: @@ -787,6 +803,9 @@ time_period_microseconds = All(time_period, time_period_in_microseconds_) positive_time_period_microseconds = All( positive_time_period, time_period_in_microseconds_ ) +positive_time_period_nanoseconds = All( + positive_time_period, time_period_in_nanoseconds_ +) positive_not_null_time_period = All( time_period, Range(min=TimePeriod(), min_included=False) ) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 0b597e0c9e..60bd17b481 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -87,6 +87,7 @@ def is_approximately_integer(value): class TimePeriod: def __init__( self, + nanoseconds=None, microseconds=None, milliseconds=None, seconds=None, @@ -136,13 +137,23 @@ class TimePeriod: if microseconds is not None: if not is_approximately_integer(microseconds): - raise ValueError("Maximum precision is microseconds") + frac_microseconds, microseconds = math.modf(microseconds) + nanoseconds = (nanoseconds or 0) + frac_microseconds * 1000 self.microseconds = int(round(microseconds)) else: self.microseconds = None + if nanoseconds is not None: + if not is_approximately_integer(nanoseconds): + raise ValueError("Maximum precision is nanoseconds") + self.nanoseconds = int(round(nanoseconds)) + else: + self.nanoseconds = None + def as_dict(self): out = OrderedDict() + if self.nanoseconds is not None: + out["nanoseconds"] = self.nanoseconds if self.microseconds is not None: out["microseconds"] = self.microseconds if self.milliseconds is not None: @@ -158,6 +169,8 @@ class TimePeriod: return out def __str__(self): + if self.nanoseconds is not None: + return f"{self.total_nanoseconds}ns" if self.microseconds is not None: return f"{self.total_microseconds}us" if self.milliseconds is not None: @@ -173,7 +186,11 @@ class TimePeriod: return "0s" def __repr__(self): - return f"TimePeriod<{self.total_microseconds}>" + return f"TimePeriod<{self.total_nanoseconds}ns>" + + @property + def total_nanoseconds(self): + return self.total_microseconds * 1000 + (self.nanoseconds or 0) @property def total_microseconds(self): @@ -201,35 +218,39 @@ class TimePeriod: def __eq__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds == other.total_microseconds + return self.total_nanoseconds == other.total_nanoseconds return NotImplemented def __ne__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds != other.total_microseconds + return self.total_nanoseconds != other.total_nanoseconds return NotImplemented def __lt__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds < other.total_microseconds + return self.total_nanoseconds < other.total_nanoseconds return NotImplemented def __gt__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds > other.total_microseconds + return self.total_nanoseconds > other.total_nanoseconds return NotImplemented def __le__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds <= other.total_microseconds + return self.total_nanoseconds <= other.total_nanoseconds return NotImplemented def __ge__(self, other): if isinstance(other, TimePeriod): - return self.total_microseconds >= other.total_microseconds + return self.total_nanoseconds >= other.total_nanoseconds return NotImplemented +class TimePeriodNanoseconds(TimePeriod): + pass + + class TimePeriodMicroseconds(TimePeriod): pass diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 2841be1546..909a786917 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -17,6 +17,7 @@ from esphome.core import ( TimePeriodMicroseconds, TimePeriodMilliseconds, TimePeriodMinutes, + TimePeriodNanoseconds, TimePeriodSeconds, ) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last @@ -351,6 +352,8 @@ def safe_exp(obj: SafeExpType) -> Expression: return IntLiteral(obj) if isinstance(obj, float): return FloatLiteral(obj) + if isinstance(obj, TimePeriodNanoseconds): + return IntLiteral(int(obj.total_nanoseconds)) if isinstance(obj, TimePeriodMicroseconds): return IntLiteral(int(obj.total_microseconds)) if isinstance(obj, TimePeriodMilliseconds): diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 9a15bf0b9c..efa9ff5677 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -116,14 +116,16 @@ class TestTimePeriod: assert actual == expected - def test_init__microseconds_with_fraction(self): - with pytest.raises(ValueError, match="Maximum precision is microseconds"): - core.TimePeriod(microseconds=1.1) + def test_init__nanoseconds_with_fraction(self): + with pytest.raises(ValueError, match="Maximum precision is nanoseconds"): + core.TimePeriod(nanoseconds=1.1) @pytest.mark.parametrize( "kwargs, expected", ( ({}, "0s"), + ({"nanoseconds": 1}, "1ns"), + ({"nanoseconds": 1.0001}, "1ns"), ({"microseconds": 1}, "1us"), ({"microseconds": 1.0001}, "1us"), ({"milliseconds": 2}, "2ms"), From 1bd2e558d6920d086390394f194a693523c7939b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:01:26 +1300 Subject: [PATCH 171/270] Fix esp32_rmt_led_strip custom timing units (#5696) --- esphome/components/esp32_rmt_led_strip/light.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 43629bec51..122ee132a7 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -93,19 +93,19 @@ CONFIG_SCHEMA = cv.All( cv.Inclusive( CONF_BIT0_HIGH, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT0_LOW, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT1_HIGH, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, cv.Inclusive( CONF_BIT1_LOW, "custom", - ): cv.positive_time_period_microseconds, + ): cv.positive_time_period_nanoseconds, } ), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), From 6c62c00963a658422742742010b62cbdd24d1ffb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Nov 2023 15:04:01 -0600 Subject: [PATCH 172/270] Fix static assets cache logic (#5700) --- esphome/dashboard/dashboard.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 119094de28..5967c95aba 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -4,6 +4,7 @@ import base64 import binascii import codecs import collections +import datetime import functools import gzip import hashlib @@ -1406,15 +1407,17 @@ def make_app(debug=get_bool_env(ENV_DEV)): ) class StaticFileHandler(tornado.web.StaticFileHandler): - def set_extra_headers(self, path): - if "favicon.ico" in path: - self.set_header("Cache-Control", "max-age=84600, public") - else: - if debug: - self.set_header( - "Cache-Control", - "no-store, no-cache, must-revalidate, max-age=0", - ) + def get_cache_time( + self, path: str, modified: datetime.datetime | None, mime_type: str + ) -> int: + """Override to customize cache control behavior.""" + if debug: + return 0 + # Assets that are hashed have ?hash= in the URL, all javascript + # filenames hashed so we can cache them for a long time + if "hash" in self.request.arguments or "/javascript" in mime_type: + return self.CACHE_MAX_AGE + return super().get_cache_time(path, modified, mime_type) app_settings = { "debug": debug, From c40519ec6f7ac1fc632e37a8fba1b34474826f57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Nov 2023 16:50:08 -0600 Subject: [PATCH 173/270] Use piwheels for armv7 docker image builds (#5703) --- docker/Dockerfile | 7 ++++--- docker/build.py | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 72aa9d9a9c..262827dea4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,6 +14,7 @@ FROM base-${BASEIMGTYPE} AS base ARG TARGETARCH ARG TARGETVARIANT +ARG PIP_EXTRA_INDEX_URL # Note that --break-system-packages is used below because # https://peps.python.org/pep-0668/ added a safety check that prevents @@ -46,7 +47,7 @@ RUN \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ cargo=0.66.0+ds1-1 \ - pkg-config=1.8.1-1; \ + pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ fi; \ rm -rf \ @@ -58,8 +59,8 @@ ENV \ # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/ LANG=C.UTF-8 LC_ALL=C.UTF-8 \ # Store globally installed pio libs in /piolibs - PLATFORMIO_GLOBALLIB_DIR=/piolibs - + PLATFORMIO_GLOBALLIB_DIR=/piolibs \ + PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} # Support legacy binaries on Debian multiarch system. There is no "correct" way # to do this, other than using properly built toolchains... diff --git a/docker/build.py b/docker/build.py index 47461ddf97..ae0f5088d8 100755 --- a/docker/build.py +++ b/docker/build.py @@ -143,15 +143,25 @@ def main(): imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push] imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push] + build_args = [ + "--build-arg", + f"BASEIMGTYPE={params.baseimgtype}", + "--build-arg", + f"BUILD_VERSION={args.tag}", + ] + + if args.arch == ARCH_ARMV7: + build_args += [ + "--build-arg", + "PIP_EXTRA_INDEX_URL=https://www.piwheels.org/simple", + ] + # 3. build cmd = [ "docker", "buildx", "build", - "--build-arg", - f"BASEIMGTYPE={params.baseimgtype}", - "--build-arg", - f"BUILD_VERSION={args.tag}", + *build_args, "--cache-from", f"type=registry,ref={cache_img}", "--file", From 29aa15b253c9f5d81d384166b1550408958fff66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Thu, 9 Nov 2023 00:35:37 +0100 Subject: [PATCH 174/270] fix: Fix broken bluetooth_proxy and ble_clients after BLE enable/disable (#5704) --- .../esp32_ble_client/ble_client_base.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 40eff49266..cc6d3d7d4d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -20,16 +20,21 @@ static const esp_bt_uuid_t NOTIFY_DESC_UUID = { void BLEClientBase::setup() { static uint8_t connection_index = 0; this->connection_index_ = connection_index++; - - auto ret = esp_ble_gattc_app_register(this->app_id); - if (ret) { - ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); - this->mark_failed(); - } - this->set_state(espbt::ClientState::IDLE); } void BLEClientBase::loop() { + if (!esp32_ble::global_ble->is_active()) { + this->set_state(espbt::ClientState::INIT); + return; + } + if (this->state_ == espbt::ClientState::INIT) { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_state(espbt::ClientState::IDLE); + } // READY_TO_CONNECT means we have discovered the device // and the scanner has been stopped by the tracker. if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { From 693242210452def6eee67e8457bfb96d47123862 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:46:57 +1300 Subject: [PATCH 175/270] Bump version to 2023.11.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 83c596a071..ac55accd11 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b1" +__version__ = "2023.11.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 1e55764d528923e05ff787ff35e9a5dafcd9d42c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Nov 2023 20:52:08 -0600 Subject: [PATCH 176/270] Bump aioesphomeapi to 18.2.7 (#5706) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69c73fa952..33c5e6988e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.2.4 +aioesphomeapi==18.2.7 zeroconf==0.120.0 # esp-idf requires this, but doesn't bundle it by default From 3d30f1f733bdf8a37615ac18df99c68afe024df1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Nov 2023 02:04:39 -0600 Subject: [PATCH 177/270] Update Dockerfile to use piwheels for armv7 (#5709) --- docker/Dockerfile | 41 +++++++++++++++++++++++++++++++---------- docker/build.py | 18 ++++-------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 262827dea4..7ca633a982 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,6 +5,7 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker + # https://github.com/hassio-addons/addon-debian-base/releases FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio # https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm @@ -12,9 +13,10 @@ FROM debian:12.2-slim AS base-docker FROM base-${BASEIMGTYPE} AS base + ARG TARGETARCH ARG TARGETVARIANT -ARG PIP_EXTRA_INDEX_URL + # Note that --break-system-packages is used below because # https://peps.python.org/pep-0668/ added a safety check that prevents @@ -59,8 +61,7 @@ ENV \ # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/ LANG=C.UTF-8 LC_ALL=C.UTF-8 \ # Store globally installed pio libs in /piolibs - PLATFORMIO_GLOBALLIB_DIR=/piolibs \ - PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} + PLATFORMIO_GLOBALLIB_DIR=/piolibs # Support legacy binaries on Debian multiarch system. There is no "correct" way # to do this, other than using properly built toolchains... @@ -72,8 +73,12 @@ RUN \ RUN \ # Ubuntu python3-pip is missing wheel - pip3 install --break-system-packages --no-cache-dir \ - platformio==6.1.11 \ + if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + pip3 install \ + --break-system-packages --no-cache-dir \ + platformio==6.1.11 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ @@ -84,8 +89,12 @@ RUN \ # tmpfs is for https://github.com/rust-lang/cargo/issues/8719 COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / -RUN --mount=type=tmpfs,target=/root/.cargo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ - pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ +RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ + pip3 install \ + --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ && /platformio_install_deps.py /platformio.ini --libraries @@ -94,7 +103,11 @@ FROM base AS docker # Copy esphome and install COPY . /esphome -RUN pip3 install --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome +RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + pip3 install \ + --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome # Settings for dashboard ENV USERNAME="" PASSWORD="" @@ -140,7 +153,11 @@ COPY docker/ha-addon-rootfs/ / # Copy esphome and install COPY . /esphome -RUN pip3 install --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome +RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + pip3 install \ + --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome # Labels LABEL \ @@ -176,7 +193,11 @@ RUN \ /var/lib/apt/lists/* COPY requirements_test.txt / -RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt +RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ + export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + fi; \ + pip3 install \ + --break-system-packages --no-cache-dir -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome diff --git a/docker/build.py b/docker/build.py index ae0f5088d8..47461ddf97 100755 --- a/docker/build.py +++ b/docker/build.py @@ -143,25 +143,15 @@ def main(): imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push] imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push] - build_args = [ - "--build-arg", - f"BASEIMGTYPE={params.baseimgtype}", - "--build-arg", - f"BUILD_VERSION={args.tag}", - ] - - if args.arch == ARCH_ARMV7: - build_args += [ - "--build-arg", - "PIP_EXTRA_INDEX_URL=https://www.piwheels.org/simple", - ] - # 3. build cmd = [ "docker", "buildx", "build", - *build_args, + "--build-arg", + f"BASEIMGTYPE={params.baseimgtype}", + "--build-arg", + f"BUILD_VERSION={args.tag}", "--cache-from", f"type=registry,ref={cache_img}", "--file", From 7a9866f1b6e6338bc405c8e943a059ec32baa1f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:14:22 +1300 Subject: [PATCH 178/270] Bump version to 2023.11.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ac55accd11..2e660c0626 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b2" +__version__ = "2023.11.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 799851a83a8cf747156db6bb195bc66804f58ff0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:26:05 -0600 Subject: [PATCH 179/270] Bump zeroconf from 0.120.0 to 0.122.3 (#5715) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 33c5e6988e..f974967a00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.2.7 -zeroconf==0.120.0 +zeroconf==0.122.3 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 63a277ba8038856448c7be96216e4028a9699163 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:20:34 -0600 Subject: [PATCH 180/270] Bump zeroconf to 0.123.0 (#5736) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51799bc685..1d53c2761c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.4.0 -zeroconf==0.122.3 +zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 684cf102306d7e5b5ba6d80d92796796b2cd32a8 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 12 Nov 2023 19:28:02 +0100 Subject: [PATCH 181/270] Bump Arduino Pico Framework to 3.6.0 and Platform to 1.10.0 (#5731) --- esphome/components/rp2040/__init__.py | 4 ++-- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 62f199b040..d027f48244 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -74,12 +74,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 4, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 6, 0) # The platformio/raspberrypi version to use for arduino frameworks # - https://github.com/platformio/platform-raspberrypi/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi -ARDUINO_PLATFORM_VERSION = cv.Version(1, 9, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(1, 10, 0) def _arduino_check_versions(value): diff --git a/platformio.ini b/platformio.ini index 73cd7c65c8..51c9f4d0c1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -159,7 +159,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.4.0/rp2040-3.4.0.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.4.0/rp2040-3.6.0.zip framework = arduino lib_deps = From 3b486084c82b17cb777888b06293970081c91c0c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:40:07 +1300 Subject: [PATCH 182/270] Add resistance_sampler interface for config validation (#5718) --- CODEOWNERS | 1 + esphome/components/ntc/sensor.py | 6 ++++-- esphome/components/resistance/resistance_sensor.h | 5 +++-- esphome/components/resistance/sensor.py | 11 +++++++++-- esphome/components/resistance_sampler/__init__.py | 6 ++++++ .../resistance_sampler/resistance_sampler.h | 10 ++++++++++ 6 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 esphome/components/resistance_sampler/__init__.py create mode 100644 esphome/components/resistance_sampler/resistance_sampler.h diff --git a/CODEOWNERS b/CODEOWNERS index 2dcef6c514..dd1586d039 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,6 +246,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet +esphome/components/resistance_sampler/* @jesserockz esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index ba8d3df9d8..06fc55fc43 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -2,7 +2,7 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor +from esphome.components import sensor, resistance_sampler from esphome.const import ( CONF_CALIBRATION, CONF_REFERENCE_RESISTANCE, @@ -15,6 +15,8 @@ from esphome.const import ( UNIT_CELSIUS, ) +AUTO_LOAD = ["resistance_sampler"] + ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) @@ -124,7 +126,7 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler), cv.Required(CONF_CALIBRATION): process_calibration, } ) diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..8fa1f8b570 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -1,7 +1,8 @@ #pragma once -#include "esphome/core/component.h" +#include "esphome/components/resistance_sampler/resistance_sampler.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace resistance { @@ -11,7 +12,7 @@ enum ResistanceConfiguration { DOWNSTREAM, }; -class ResistanceSensor : public Component, public sensor::Sensor { +class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index 55e7ddfc81..a84b439497 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor +from esphome.components import sensor, resistance_sampler from esphome.const import ( CONF_SENSOR, STATE_CLASS_MEASUREMENT, @@ -8,8 +8,15 @@ from esphome.const import ( ICON_FLASH, ) +AUTO_LOAD = ["resistance_sampler"] + resistance_ns = cg.esphome_ns.namespace("resistance") -ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) +ResistanceSensor = resistance_ns.class_( + "ResistanceSensor", + cg.Component, + sensor.Sensor, + resistance_sampler.ResistanceSampler, +) CONF_REFERENCE_VOLTAGE = "reference_voltage" CONF_CONFIGURATION = "configuration" diff --git a/esphome/components/resistance_sampler/__init__.py b/esphome/components/resistance_sampler/__init__.py new file mode 100644 index 0000000000..d2032848aa --- /dev/null +++ b/esphome/components/resistance_sampler/__init__.py @@ -0,0 +1,6 @@ +import esphome.codegen as cg + +resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler") +ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler") + +CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/resistance_sampler/resistance_sampler.h b/esphome/components/resistance_sampler/resistance_sampler.h new file mode 100644 index 0000000000..9e300bebcc --- /dev/null +++ b/esphome/components/resistance_sampler/resistance_sampler.h @@ -0,0 +1,10 @@ +#pragma once + +namespace esphome { +namespace resistance_sampler { + +/// Abstract interface to mark components that provide resistance values. +class ResistanceSampler {}; + +} // namespace resistance_sampler +} // namespace esphome From 163b38e153033cca2294161e854c64a896f57c0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Nov 2023 03:55:21 -0600 Subject: [PATCH 183/270] Fix zeroconf name resolution refactoring error (#5725) --- esphome/zeroconf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index d20111ce20..f4cb7f080b 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -147,12 +147,13 @@ class DashboardImportDiscovery: class EsphomeZeroconf(Zeroconf): - def resolve_host(self, host: str, timeout=3.0): + def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: """Resolve a host name to an IP address.""" name = host.partition(".")[0] - info = HostResolver(f"{name}.{ESPHOME_SERVICE_TYPE}", ESPHOME_SERVICE_TYPE) - if (info.load_from_cache(self) or info.request(self, timeout * 1000)) and ( - addresses := info.ip_addresses_by_version(IPVersion.V4Only) - ): + info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + if ( + info.load_from_cache(self) + or (timeout and info.request(self, timeout * 1000)) + ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): return str(addresses[0]) return None From 30e5ff9fffffbfca9313af1e864c77591be3822b Mon Sep 17 00:00:00 2001 From: Mike La Spina Date: Fri, 10 Nov 2023 18:37:39 -0600 Subject: [PATCH 184/270] Missed ifdefs (#5727) --- esphome/components/ld2420/ld2420.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 6130617457..bda1764cfc 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -68,6 +68,7 @@ void LD2420Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2420:"); ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_); ESP_LOGCONFIG(TAG, "LD2420 Number:"); +#ifdef USE_NUMBER LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); @@ -76,10 +77,13 @@ void LD2420Component::dump_config() { LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); } +#endif +#ifdef USE_BUTTON LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_); LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_); LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); +#endif ESP_LOGCONFIG(TAG, "LD2420 Select:"); LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { @@ -183,9 +187,11 @@ void LD2420Component::factory_reset_action() { return; } this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT); +#ifdef USE_NUMBER this->gate_timeout_number_->state = FACTORY_TIMEOUT; this->min_gate_distance_number_->state = FACTORY_MIN_GATE; this->max_gate_distance_number_->state = FACTORY_MAX_GATE; +#endif for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate]; this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate]; From 91299f05f715297845d40f84786d747c21f2b7b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 Nov 2023 21:59:36 +0000 Subject: [PATCH 185/270] Bump aioesphomeapi from 18.2.7 to 18.4.0 (#5735) 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 f974967a00..51799bc685 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.2.7 +aioesphomeapi==18.4.0 zeroconf==0.122.3 # esp-idf requires this, but doesn't bundle it by default From bd5905c59a0b7309b1366663d8936313d3fc35da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Nov 2023 19:36:56 -0600 Subject: [PATCH 186/270] Migrate to using aioesphomeapi for the log runner to fix multiple issues (#5733) --- esphome/components/api/client.py | 76 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 819055ccf4..2c43eca70c 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -1,71 +1,65 @@ +from __future__ import annotations + import asyncio import logging from datetime import datetime -from typing import Optional +from typing import Any -from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel -import zeroconf +from aioesphomeapi import APIClient +from aioesphomeapi.api_pb2 import SubscribeLogsResponse +from aioesphomeapi.log_runner import async_run +from zeroconf.asyncio import AsyncZeroconf + +from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ +from esphome.core import CORE -from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__ -from esphome.util import safe_print from . import CONF_ENCRYPTION _LOGGER = logging.getLogger(__name__) async def async_run_logs(config, address): + """Run the logs command in the event loop.""" conf = config["api"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] - noise_psk: Optional[str] = None + noise_psk: str | None = None if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) + aiozc = AsyncZeroconf() + cli = APIClient( address, port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, + zeroconf_instance=aiozc.zeroconf, ) - first_connect = True + dashboard = CORE.dashboard - def on_log(msg): - time_ = datetime.now().time().strftime("[%H:%M:%S]") - text = msg.message.decode("utf8", "backslashreplace") - safe_print(time_ + text) - - async def on_connect(): - nonlocal first_connect - try: - await cli.subscribe_logs( - on_log, - log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE, - dump_config=first_connect, - ) - first_connect = False - except APIConnectionError: - cli.disconnect() - - async def on_disconnect(expected_disconnect: bool) -> None: - _LOGGER.warning("Disconnected from API") - - zc = zeroconf.Zeroconf() - reconnect = ReconnectLogic( - client=cli, - on_connect=on_connect, - on_disconnect=on_disconnect, - zeroconf_instance=zc, - ) - await reconnect.start() + def on_log(msg: SubscribeLogsResponse) -> None: + """Handle a new log message.""" + time_ = datetime.now() + message: bytes = msg.message + text = message.decode("utf8", "backslashreplace") + if dashboard: + text = text.replace("\033", "\\033") + print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") + stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc) try: while True: await asyncio.sleep(60) + finally: + await aiozc.async_close() + await stop() + + +def run_logs(config: dict[str, Any], address: str) -> None: + """Run the logs command.""" + try: + asyncio.run(async_run_logs(config, address)) except KeyboardInterrupt: - await reconnect.stop() - zc.close() - - -def run_logs(config, address): - asyncio.run(async_run_logs(config, address)) + pass From 908f56ff46dd18774dec3930599c9655ea2e5e1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 12:20:34 -0600 Subject: [PATCH 187/270] Bump zeroconf to 0.123.0 (#5736) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51799bc685..1d53c2761c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.4.0 -zeroconf==0.122.3 +zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From f094702a162dda10cb3454fb76906b1f7fc684d4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:23:28 +1300 Subject: [PATCH 188/270] Bump version to 2023.11.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2e660c0626..3b8b847de6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b3" +__version__ = "2023.11.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 45276cc24452b7ea1a7a0c6f88ed11ca38a4a1e6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:48:26 +1300 Subject: [PATCH 189/270] Handle wake word not set up internally (#5738) --- esphome/components/voice_assistant/voice_assistant.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d15d702d4b..fc5dd6e4e4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -610,6 +610,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") { // Don't change state here since either the "tts-end" or "run-end" events will do it. return; + } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { + // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. + this->request_stop(); + this->error_trigger_->trigger(code, message); + return; } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); if (this->state_ != State::IDLE) { From a9772ebf3fcbaf7900b6689cb3b3f3be25386268 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:48:26 +1300 Subject: [PATCH 190/270] Handle wake word not set up internally (#5738) --- esphome/components/voice_assistant/voice_assistant.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index d15d702d4b..fc5dd6e4e4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -610,6 +610,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") { // Don't change state here since either the "tts-end" or "run-end" events will do it. return; + } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { + // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. + this->request_stop(); + this->error_trigger_->trigger(code, message); + return; } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); if (this->state_ != State::IDLE) { From c1eb5bd675fbe189de4b1176f1fbdea61825042d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:26:04 +1300 Subject: [PATCH 191/270] Bump version to 2023.11.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3b8b847de6..ddba48921e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b4" +__version__ = "2023.11.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 0a4853ba7bd0b94cbfd63a877cac6a1018766560 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Mon, 13 Nov 2023 18:38:08 +0100 Subject: [PATCH 192/270] Correct url for Arduino platform (#5744) --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 51c9f4d0c1..cbd87155be 100644 --- a/platformio.ini +++ b/platformio.ini @@ -159,7 +159,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.4.0/rp2040-3.6.0.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.6.0/rp2040-3.6.0.zip framework = arduino lib_deps = From 00eedeb8b3a3e26370bd67493b972948f56be542 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 13 Nov 2023 21:55:36 +0400 Subject: [PATCH 193/270] remote_base: added helper class and schemas (#5169) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/climate_ir/__init__.py | 51 ++++++++-------- esphome/components/climate_ir/climate_ir.h | 9 ++- esphome/components/coolix/coolix.cpp | 6 +- .../components/heatpumpir/ir_sender_esphome.h | 6 +- esphome/components/remote_base/__init__.py | 30 ++++++++-- .../components/remote_base/midea_protocol.h | 15 +---- .../remote_base/rc_switch_protocol.h | 4 +- esphome/components/remote_base/remote_base.h | 58 ++++++++++++------- tests/test1.yaml | 3 + 9 files changed, 99 insertions(+), 83 deletions(-) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 0cf1339971..c7c286d679 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -1,38 +1,37 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import ( - climate, - remote_transmitter, - remote_receiver, - sensor, - remote_base, -) -from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID +from esphome.components import climate, sensor, remote_base from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR +DEPENDENCIES = ["remote_transmitter"] AUTO_LOAD = ["sensor", "remote_base"] CODEOWNERS = ["@glmnet"] climate_ir_ns = cg.esphome_ns.namespace("climate_ir") ClimateIR = climate_ir_ns.class_( - "ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener + "ClimateIR", + climate.Climate, + cg.Component, + remote_base.RemoteReceiverListener, + remote_base.RemoteTransmittable, ) -CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( - remote_transmitter.RemoteTransmitterComponent - ), - cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, - cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, - cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), - } -).extend(cv.COMPONENT_SCHEMA) +CLIMATE_IR_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) +) CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( { - cv.Optional(CONF_RECEIVER_ID): cv.use_id( - remote_receiver.RemoteReceiverComponent + cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( + remote_base.RemoteReceiverBase ), } ) @@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( async def register_climate_ir(var, config): await cg.register_component(var, config) await climate.register_climate(var, config) - + await remote_base.register_transmittable(var, config) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + if remote_base.CONF_RECEIVER_ID in config: + await remote_base.register_listener(var, config) if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) - if receiver_id := config.get(CONF_RECEIVER_ID): - receiver = await cg.get_variable(receiver_id) - cg.add(receiver.register_listener(var)) - - transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 5be4fc06f5..ea0656121f 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -18,7 +18,10 @@ namespace climate_ir { Likewise to decode a IR into the AC state, implement bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true */ -class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { +class ClimateIR : public Component, + public climate::Climate, + public remote_base::RemoteReceiverListener, + public remote_base::RemoteTransmittable { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, @@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: void setup() override; void dump_config() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } @@ -64,7 +64,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: std::set swing_modes_ = {}; std::set presets_ = {}; - remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; }; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index f4309419a4..22b3431c3e 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -102,11 +102,7 @@ void CoolixClimate::transmit_state() { } } ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state); - - auto transmit = this->transmitter_->transmit(); - auto *data = transmit.get_data(); - remote_base::CoolixProtocol().encode(data, remote_state); - transmit.perform(); + this->transmit_(remote_state); } bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { diff --git a/esphome/components/heatpumpir/ir_sender_esphome.h b/esphome/components/heatpumpir/ir_sender_esphome.h index 7546d990ea..944d0e859c 100644 --- a/esphome/components/heatpumpir/ir_sender_esphome.h +++ b/esphome/components/heatpumpir/ir_sender_esphome.h @@ -3,7 +3,6 @@ #ifdef USE_ARDUINO #include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" #include // arduino-heatpump library namespace esphome { @@ -11,14 +10,13 @@ namespace heatpumpir { class IRSenderESPHome : public IRSender { public: - IRSenderESPHome(remote_transmitter::RemoteTransmitterComponent *transmitter) - : IRSender(0), transmit_(transmitter->transmit()){}; + IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){}; void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming) void space(int space_length) override; void mark(int mark_length) override; protected: - remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_; + remote_base::RemoteTransmitterBase::TransmitCall transmit_; }; } // namespace heatpumpir diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 25dedd71d8..a2411b1b12 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -52,8 +52,9 @@ RemoteReceiverTrigger = ns.class_( "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener ) RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteTransmittable = ns.class_("RemoteTransmittable") RemoteTransmitterActionBase = ns.class_( - "RemoteTransmitterActionBase", automation.Action + "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action ) RemoteReceiverBase = ns.class_("RemoteReceiverBase") RemoteTransmitterBase = ns.class_("RemoteTransmitterBase") @@ -68,11 +69,30 @@ def templatize(value): return cv.Schema(ret) +REMOTE_LISTENER_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + } +) + + +REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), + } +) + + async def register_listener(var, config): receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) cg.add(receiver.register_listener(var)) +async def register_transmittable(var, config): + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + + def register_binary_sensor(name, type, schema): return BINARY_SENSOR_REGISTRY.register(name, type, schema) @@ -129,10 +149,9 @@ def validate_repeat(value): BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema( { - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, } -) +).extend(REMOTE_TRANSMITTABLE_SCHEMA) def register_action(name, type_, schema): @@ -143,9 +162,8 @@ def register_action(name, type_, schema): def decorator(func): async def new_func(config, action_id, template_arg, args): - transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) var = cg.new_Pvariable(action_id, template_arg) - cg.add(var.set_parent(transmitter)) + await register_transmittable(var, config) if CONF_REPEAT in config: conf = config[CONF_REPEAT] template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32) @@ -1539,7 +1557,7 @@ MIDEA_SCHEMA = cv.Schema( @register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) def midea_binary_sensor(var, config): - cg.add(var.set_code(config[CONF_CODE])) + cg.add(var.set_data(config[CONF_CODE])) @register_trigger("midea", MideaTrigger, MideaData) diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 6925686b34..94fb6f3d94 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -67,20 +67,7 @@ class MideaProtocol : public RemoteProtocol { void dump(const MideaData &data) override; }; -class MideaBinarySensor : public RemoteReceiverBinarySensorBase { - public: - bool matches(RemoteReceiveData src) override { - auto data = MideaProtocol().decode(src); - return data.has_value() && data.value() == this->data_; - } - void set_code(const std::vector &code) { this->data_ = code; } - - protected: - MideaData data_; -}; - -using MideaTrigger = RemoteReceiverTrigger; -using MideaDumper = RemoteReceiverDumper; +DECLARE_REMOTE_PROTOCOL(Midea) template class MideaAction : public RemoteTransmitterActionBase { TEMPLATABLE_VALUE(std::vector, code) diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index fc465dbd5d..96cbbd1467 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -15,6 +15,8 @@ struct RCSwitchData { class RCSwitchBase { public: + using ProtocolData = RCSwitchData; + RCSwitchBase() = default; RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, uint32_t one_low, bool inverted); @@ -213,7 +215,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase { bool dump(RemoteReceiveData src) override; }; -using RCSwitchTrigger = RemoteReceiverTrigger; +using RCSwitchTrigger = RemoteReceiverTrigger; } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index a456007655..ebbb528a23 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -127,6 +127,14 @@ class RemoteTransmitterBase : public RemoteComponentBase { this->temp_.reset(); return TransmitCall(this); } + template + void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + auto call = this->transmit(); + Protocol().encode(call.get_data(), data); + call.set_send_times(send_times); + call.set_send_wait(send_wait); + call.perform(); + } protected: void send_(uint32_t send_times, uint32_t send_wait); @@ -184,12 +192,13 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial template class RemoteProtocol { public: - virtual void encode(RemoteTransmitData *dst, const T &data) = 0; - virtual optional decode(RemoteReceiveData src) = 0; - virtual void dump(const T &data) = 0; + using ProtocolData = T; + virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0; + virtual optional decode(RemoteReceiveData src) = 0; + virtual void dump(const ProtocolData &data) = 0; }; -template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { +template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { public: RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {} @@ -201,13 +210,14 @@ template class RemoteReceiverBinarySensor : public Remot } public: - void set_data(D data) { data_ = data; } + void set_data(typename T::ProtocolData data) { data_ = data; } protected: - D data_; + typename T::ProtocolData data_; }; -template class RemoteReceiverTrigger : public Trigger, public RemoteReceiverListener { +template +class RemoteReceiverTrigger : public Trigger, public RemoteReceiverListener { protected: bool on_receive(RemoteReceiveData src) override { auto proto = T(); @@ -220,28 +230,36 @@ template class RemoteReceiverTrigger : public Trigger } }; -template class RemoteTransmitterActionBase : public Action { +class RemoteTransmittable { public: - void set_parent(RemoteTransmitterBase *parent) { this->parent_ = parent; } + RemoteTransmittable() {} + RemoteTransmittable(RemoteTransmitterBase *transmitter) : transmitter_(transmitter) {} + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } - TEMPLATABLE_VALUE(uint32_t, send_times); - TEMPLATABLE_VALUE(uint32_t, send_wait); + protected: + template + void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + this->transmitter_->transmit(data, send_times, send_wait); + } + RemoteTransmitterBase *transmitter_; +}; +template class RemoteTransmitterActionBase : public RemoteTransmittable, public Action { + TEMPLATABLE_VALUE(uint32_t, send_times) + TEMPLATABLE_VALUE(uint32_t, send_wait) + + protected: void play(Ts... x) override { - auto call = this->parent_->transmit(); + auto call = this->transmitter_->transmit(); this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); call.set_send_wait(this->send_wait_.value_or(x..., 0)); call.perform(); } - - protected: virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; - - RemoteTransmitterBase *parent_{}; }; -template class RemoteReceiverDumper : public RemoteReceiverDumperBase { +template class RemoteReceiverDumper : public RemoteReceiverDumperBase { public: bool dump(RemoteReceiveData src) override { auto proto = T(); @@ -254,9 +272,9 @@ template class RemoteReceiverDumper : public RemoteRecei }; #define DECLARE_REMOTE_PROTOCOL_(prefix) \ - using prefix##BinarySensor = RemoteReceiverBinarySensor; \ - using prefix##Trigger = RemoteReceiverTrigger; \ - using prefix##Dumper = RemoteReceiverDumper; + using prefix##BinarySensor = RemoteReceiverBinarySensor; \ + using prefix##Trigger = RemoteReceiverTrigger; \ + using prefix##Dumper = RemoteReceiverDumper; #define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix) } // namespace remote_base diff --git a/tests/test1.yaml b/tests/test1.yaml index 32e92440c1..61d28faf73 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -3050,6 +3050,9 @@ remote_receiver: on_coolix: then: delay: !lambda "return x.first + x.second;" + on_rc_switch: + then: + delay: !lambda "return uint32_t(x.code) + x.protocol;" status_led: pin: GPIO2 From 8c28bea5b1df8627885615e17f496e3122b182f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:19:15 -0600 Subject: [PATCH 194/270] Bump zeroconf from 0.123.0 to 0.126.0 (#5748) 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 1d53c2761c..3140d0a8a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.4.0 -zeroconf==0.123.0 +zeroconf==0.126.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 08fc96b890051280cd87ae099a78572c8d5f50b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Nov 2023 18:44:49 -0600 Subject: [PATCH 195/270] dashboard: remove usage of codecs module (#5741) --- esphome/dashboard/dashboard.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 5967c95aba..3de40ff402 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -2,7 +2,6 @@ from __future__ import annotations import base64 import binascii -import codecs import collections import datetime import functools @@ -339,8 +338,8 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def handle_stdin(self, json_message): if not self.is_process_active: return - data = json_message["data"] - data = codecs.encode(data, "utf8", "replace") + text: str = json_message["data"] + data = text.encode("utf-8", "replace") _LOGGER.debug("< stdin: %s", data) self._proc.stdin.write(data) @@ -351,18 +350,18 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): while True: try: if self._use_popen: - data = yield self._queue.get() + data: bytes = yield self._queue.get() if data is None: self._proc_on_exit(self._proc.poll()) break else: - data = yield self._proc.stdout.read_until_regex(reg) + data: bytes = yield self._proc.stdout.read_until_regex(reg) except tornado.iostream.StreamClosedError: break - data = codecs.decode(data, "utf8", "replace") - _LOGGER.debug("> stdout: %s", data) - self.write_message({"event": "line", "data": data}) + text = data.decode("utf-8", "replace") + _LOGGER.debug("> stdout: %s", text) + self.write_message({"event": "line", "data": text}) def _stdout_thread(self): if not self._use_popen: From f198be39d708114f01013d436a32bff93c18ec28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Nov 2023 18:46:51 -0600 Subject: [PATCH 196/270] dashboard: Run get_serial_ports in the executor (#5740) --- esphome/dashboard/dashboard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 3de40ff402..050564d21e 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import base64 import binascii import collections @@ -508,8 +509,8 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class SerialPortRequestHandler(BaseHandler): @authenticated - def get(self): - ports = get_serial_ports() + async def get(self): + ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) data = [] for port in ports: desc = port.description From ae0e481cffb6ead0bb57d4b17527f39dd1b88b87 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 13 Nov 2023 18:47:29 -0600 Subject: [PATCH 197/270] Generate partitions.csv based on flash size (#5697) --- esphome/components/esp32/__init__.py | 66 ++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cda02f80c2..9b83d144f8 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -386,10 +386,21 @@ FRAMEWORK_SCHEMA = cv.typed_schema( ) +FLASH_SIZES = [ + "4MB", + "8MB", + "16MB", + "32MB", +] + +CONF_FLASH_SIZE = "flash_size" CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( + *FLASH_SIZES, upper=True + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -401,6 +412,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") @@ -505,24 +517,46 @@ async def to_code(config): ) -ARDUINO_PARTITIONS_CSV = """\ -nvs, data, nvs, 0x009000, 0x005000, -otadata, data, ota, 0x00e000, 0x002000, -app0, app, ota_0, 0x010000, 0x1C0000, -app1, app, ota_1, 0x1D0000, 0x1C0000, -eeprom, data, 0x99, 0x390000, 0x001000, -spiffs, data, spiffs, 0x391000, 0x00F000 +APP_PARTITION_SIZES = { + "4MB": 0x1C0000, # 1792 KB + "8MB": 0x3C0000, # 3840 KB + "16MB": 0x7C0000, # 7936 KB + "32MB": 0xFC0000, # 16128 KB +} + + +def get_arduino_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + eeprom_partition_size = 0x1000 # 4 KB + spiffs_partition_size = 0xF000 # 60 KB + + app0_partition_start = 0x010000 # 64 KB + app1_partition_start = app0_partition_start + app_partition_size + eeprom_partition_start = app1_partition_start + app_partition_size + spiffs_partition_start = eeprom_partition_start + eeprom_partition_size + + partition_csv = f"""\ +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xE000, 0x2000, +app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, +app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, +eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, +spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} """ + return partition_csv -IDF_PARTITIONS_CSV = """\ -# Name, Type, SubType, Offset, Size, Flags +def get_idf_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + + partition_csv = f"""\ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x1C0000, -app1, app, ota_1, , 0x1C0000, -nvs, data, nvs, , 0x6d000, +app0, app, ota_0, , 0x{app_partition_size:X}, +app1, app, ota_1, , 0x{app_partition_size:X}, +nvs, data, nvs, , 0x6D000, """ + return partition_csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -565,13 +599,17 @@ def copy_files(): if CORE.using_arduino: write_file_if_changed( CORE.relative_build_path("partitions.csv"), - ARDUINO_PARTITIONS_CSV, + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), ) if CORE.using_esp_idf: _write_sdkconfig() write_file_if_changed( CORE.relative_build_path("partitions.csv"), - IDF_PARTITIONS_CSV, + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, From bd568eecf5f0751410d21882285104314db3831f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Nov 2023 18:44:49 -0600 Subject: [PATCH 198/270] dashboard: remove usage of codecs module (#5741) --- esphome/dashboard/dashboard.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 5967c95aba..3de40ff402 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -2,7 +2,6 @@ from __future__ import annotations import base64 import binascii -import codecs import collections import datetime import functools @@ -339,8 +338,8 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def handle_stdin(self, json_message): if not self.is_process_active: return - data = json_message["data"] - data = codecs.encode(data, "utf8", "replace") + text: str = json_message["data"] + data = text.encode("utf-8", "replace") _LOGGER.debug("< stdin: %s", data) self._proc.stdin.write(data) @@ -351,18 +350,18 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): while True: try: if self._use_popen: - data = yield self._queue.get() + data: bytes = yield self._queue.get() if data is None: self._proc_on_exit(self._proc.poll()) break else: - data = yield self._proc.stdout.read_until_regex(reg) + data: bytes = yield self._proc.stdout.read_until_regex(reg) except tornado.iostream.StreamClosedError: break - data = codecs.decode(data, "utf8", "replace") - _LOGGER.debug("> stdout: %s", data) - self.write_message({"event": "line", "data": data}) + text = data.decode("utf-8", "replace") + _LOGGER.debug("> stdout: %s", text) + self.write_message({"event": "line", "data": text}) def _stdout_thread(self): if not self._use_popen: From 2ee089c9d50c24bb201e397d5b012aa5b832df65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Nov 2023 18:46:51 -0600 Subject: [PATCH 199/270] dashboard: Run get_serial_ports in the executor (#5740) --- esphome/dashboard/dashboard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 3de40ff402..050564d21e 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import base64 import binascii import collections @@ -508,8 +509,8 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class SerialPortRequestHandler(BaseHandler): @authenticated - def get(self): - ports = get_serial_ports() + async def get(self): + ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) data = [] for port in ports: desc = port.description From 1ac6cf2ff9d83aafe984f0a511384610a0104d3e Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 13 Nov 2023 18:47:29 -0600 Subject: [PATCH 200/270] Generate partitions.csv based on flash size (#5697) --- esphome/components/esp32/__init__.py | 66 ++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index cda02f80c2..9b83d144f8 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -386,10 +386,21 @@ FRAMEWORK_SCHEMA = cv.typed_schema( ) +FLASH_SIZES = [ + "4MB", + "8MB", + "16MB", + "32MB", +] + +CONF_FLASH_SIZE = "flash_size" CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( + *FLASH_SIZES, upper=True + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -401,6 +412,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") @@ -505,24 +517,46 @@ async def to_code(config): ) -ARDUINO_PARTITIONS_CSV = """\ -nvs, data, nvs, 0x009000, 0x005000, -otadata, data, ota, 0x00e000, 0x002000, -app0, app, ota_0, 0x010000, 0x1C0000, -app1, app, ota_1, 0x1D0000, 0x1C0000, -eeprom, data, 0x99, 0x390000, 0x001000, -spiffs, data, spiffs, 0x391000, 0x00F000 +APP_PARTITION_SIZES = { + "4MB": 0x1C0000, # 1792 KB + "8MB": 0x3C0000, # 3840 KB + "16MB": 0x7C0000, # 7936 KB + "32MB": 0xFC0000, # 16128 KB +} + + +def get_arduino_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + eeprom_partition_size = 0x1000 # 4 KB + spiffs_partition_size = 0xF000 # 60 KB + + app0_partition_start = 0x010000 # 64 KB + app1_partition_start = app0_partition_start + app_partition_size + eeprom_partition_start = app1_partition_start + app_partition_size + spiffs_partition_start = eeprom_partition_start + eeprom_partition_size + + partition_csv = f"""\ +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xE000, 0x2000, +app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, +app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, +eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, +spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} """ + return partition_csv -IDF_PARTITIONS_CSV = """\ -# Name, Type, SubType, Offset, Size, Flags +def get_idf_partition_csv(flash_size): + app_partition_size = APP_PARTITION_SIZES[flash_size] + + partition_csv = f"""\ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x1C0000, -app1, app, ota_1, , 0x1C0000, -nvs, data, nvs, , 0x6d000, +app0, app, ota_0, , 0x{app_partition_size:X}, +app1, app, ota_1, , 0x{app_partition_size:X}, +nvs, data, nvs, , 0x6D000, """ + return partition_csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -565,13 +599,17 @@ def copy_files(): if CORE.using_arduino: write_file_if_changed( CORE.relative_build_path("partitions.csv"), - ARDUINO_PARTITIONS_CSV, + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), ) if CORE.using_esp_idf: _write_sdkconfig() write_file_if_changed( CORE.relative_build_path("partitions.csv"), - IDF_PARTITIONS_CSV, + get_idf_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, From 7100d073f84a2ab0a682761808cbd7f49592764b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:32:41 +1300 Subject: [PATCH 201/270] Bump version to 2023.11.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ddba48921e..dde5f23f6f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b5" +__version__ = "2023.11.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 2754ddec1b0c6951ec7123263251dc333737f1c4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:51:45 +1300 Subject: [PATCH 202/270] Allow setup to continue past mqtt if network/wifi is disabled (#5754) --- esphome/components/mqtt/mqtt_client.cpp | 2 +- esphome/components/network/util.cpp | 8 ++++++++ esphome/components/network/util.h | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 6f63935e6e..923762aea4 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -147,7 +147,7 @@ void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); } } -bool MQTTClientComponent::can_proceed() { return this->is_connected(); } +bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); } void MQTTClientComponent::start_dnslookup_() { for (auto &subscription : this->subscriptions_) { diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 941102d6c1..109c2947ce 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -29,6 +29,14 @@ bool is_connected() { return false; } +bool is_disabled() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_disabled(); +#endif + return false; +} + network::IPAddress get_ip_address() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index f248d5cbf4..0322f19215 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -8,6 +8,8 @@ namespace network { /// Return whether the node is connected to the network (through wifi, eth, ...) bool is_connected(); +/// Return whether the network is disabled (only wifi for now) +bool is_disabled(); /// Get the active network hostname std::string get_use_address(); IPAddress get_ip_address(); From aecc6655dbd0a306c0fd811bdb4bf2ec2035988e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:57:25 +1300 Subject: [PATCH 203/270] Dont dump wifi info when disabled (#5755) --- esphome/components/wifi/wifi_component.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index cd87bb48a7..c1d5138f7b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -389,6 +389,10 @@ void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + if (this->is_disabled()) { + ESP_LOGCONFIG(TAG, " WiFi is disabled!"); + return; + } ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str()); ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], From cdcb25be8ee0f85fcad7db8f9b32010a370f2b56 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Wed, 15 Nov 2023 00:38:36 +0100 Subject: [PATCH 204/270] Make precommit checks happy (#5751) --- script/bump-version.py | 2 +- tests/unit_tests/test_cpp_generator.py | 2 +- tests/unit_tests/test_cpp_helpers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/bump-version.py b/script/bump-version.py index 1f034344f9..3e1e473c4b 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -45,7 +45,7 @@ def sub(path, pattern, repl, expected_count=1): content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) if expected_count is not None: assert count == expected_count, f"Pattern {pattern} replacement failed!" - with open(path, "wt") as fh: + with open(path, "w") as fh: fh.write(content) diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 331c500c04..6f4b5a40bc 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -1,4 +1,4 @@ -from typing import Iterator +from collections.abc import Iterator import math diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index ad234250ce..497b3966fb 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -1,5 +1,5 @@ import pytest -from mock import Mock +from unittest.mock import Mock from esphome import cpp_helpers as ch from esphome import const From e0c7a02fbc0bdf794d8ef04db771753b38ebd5a0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:51:45 +1300 Subject: [PATCH 205/270] Allow setup to continue past mqtt if network/wifi is disabled (#5754) --- esphome/components/mqtt/mqtt_client.cpp | 2 +- esphome/components/network/util.cpp | 8 ++++++++ esphome/components/network/util.h | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 6f63935e6e..923762aea4 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -147,7 +147,7 @@ void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); } } -bool MQTTClientComponent::can_proceed() { return this->is_connected(); } +bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); } void MQTTClientComponent::start_dnslookup_() { for (auto &subscription : this->subscriptions_) { diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 941102d6c1..109c2947ce 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -29,6 +29,14 @@ bool is_connected() { return false; } +bool is_disabled() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_disabled(); +#endif + return false; +} + network::IPAddress get_ip_address() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index f248d5cbf4..0322f19215 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -8,6 +8,8 @@ namespace network { /// Return whether the node is connected to the network (through wifi, eth, ...) bool is_connected(); +/// Return whether the network is disabled (only wifi for now) +bool is_disabled(); /// Get the active network hostname std::string get_use_address(); IPAddress get_ip_address(); From f1e862218775539a381a3647f727e5f8211c07d6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:57:25 +1300 Subject: [PATCH 206/270] Dont dump wifi info when disabled (#5755) --- esphome/components/wifi/wifi_component.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index cd87bb48a7..c1d5138f7b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -389,6 +389,10 @@ void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); + if (this->is_disabled()) { + ESP_LOGCONFIG(TAG, " WiFi is disabled!"); + return; + } ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str()); ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str()); ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], From 4e8bdc2155fb8a40b548041f368df6b064674719 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:45:03 +1300 Subject: [PATCH 207/270] Bump version to 2023.11.0b7 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index dde5f23f6f..9889529e7b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b6" +__version__ = "2023.11.0b7" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 0c188728882518ebd20f51693db98b8c8c652052 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:13:39 +1300 Subject: [PATCH 208/270] Bump version to 2023.11.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9889529e7b..f937ecf068 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0b7" +__version__ = "2023.11.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 214b419db25a6b71b121b75ea0dc1ceab9bdee78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Nov 2023 20:21:44 -0600 Subject: [PATCH 209/270] dashboard: Use mdns cache when available if device connection is OTA (#5724) * Use mdns or freshen cache when device connection is OTA Since we already have a service browser running, we likely already know the IP of the deivce we want to connect to so we can replace OTA with the address to avoid the esphome app having to look it up again * isort * Fix zeroconf name resolution refactoring error HostResolver should get the type as the first arg instead of the name * no i/o * tornado support native coros * lint * use new tornado start methods * use new tornado start methods * use new tornado start methods * break * lint * lint * typing, missing awaits * io in executor * missed one * fix: missing if * stale comment * rename run_command to build_device_command since it does not actually run anything --- esphome/dashboard/async_adapter.py | 56 +++++++ esphome/dashboard/dashboard.py | 231 +++++++++++++++++++---------- esphome/zeroconf.py | 60 ++++++-- 3 files changed, 255 insertions(+), 92 deletions(-) create mode 100644 esphome/dashboard/async_adapter.py diff --git a/esphome/dashboard/async_adapter.py b/esphome/dashboard/async_adapter.py new file mode 100644 index 0000000000..d6f4f6e1ff --- /dev/null +++ b/esphome/dashboard/async_adapter.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import asyncio +import threading + + +class ThreadedAsyncEvent: + """This is a shim to allow the asyncio event to be used in a threaded context. + + When more of the code is moved to asyncio, this can be removed. + """ + + def __init__(self) -> None: + """Initialize the ThreadedAsyncEvent.""" + self.event = threading.Event() + self.async_event: asyncio.Event | None = None + self.loop: asyncio.AbstractEventLoop | None = None + + def async_setup( + self, loop: asyncio.AbstractEventLoop, async_event: asyncio.Event + ) -> None: + """Set the asyncio.Event instance.""" + self.loop = loop + self.async_event = async_event + + def async_set(self) -> None: + """Set the asyncio.Event instance.""" + self.async_event.set() + self.event.set() + + def set(self) -> None: + """Set the event.""" + self.loop.call_soon_threadsafe(self.async_event.set) + self.event.set() + + def wait(self) -> None: + """Wait for the event.""" + self.event.wait() + + async def async_wait(self) -> None: + """Wait the event async.""" + await self.async_event.wait() + + def clear(self) -> None: + """Clear the event.""" + self.loop.call_soon_threadsafe(self.async_event.clear) + self.event.clear() + + def async_clear(self) -> None: + """Clear the event async.""" + self.async_event.clear() + self.event.clear() + + def is_set(self) -> bool: + """Return if the event is set.""" + return self.event.is_set() diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 050564d21e..d7d11d8693 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -18,6 +18,7 @@ import shutil import subprocess import threading from pathlib import Path +from typing import Any import tornado import tornado.concurrent @@ -46,11 +47,12 @@ from esphome.storage_json import ( from esphome.util import get_serial_ports, shlex_quote from esphome.zeroconf import ( ESPHOME_SERVICE_TYPE, + AsyncEsphomeZeroconf, DashboardBrowser, DashboardImportDiscovery, DashboardStatus, - EsphomeZeroconf, ) +from .async_adapter import ThreadedAsyncEvent from .util import friendly_name_slugify, password_hash @@ -288,7 +290,10 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): self._use_popen = os.name == "nt" @authenticated - def on_message(self, message): + async def on_message( # pylint: disable=invalid-overridden-method + self, message: str + ) -> None: + # Since tornado 4.5, on_message is allowed to be a coroutine # Messages are always JSON, 500 when not json_message = json.loads(message) type_ = json_message["type"] @@ -298,14 +303,14 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): _LOGGER.warning("Requested unknown message type %s", type_) return - handlers[type_](self, json_message) + await handlers[type_](self, json_message) @websocket_method("spawn") - def handle_spawn(self, json_message): + async def handle_spawn(self, json_message: dict[str, Any]) -> None: if self._proc is not None: # spawn can only be called once return - command = self.build_command(json_message) + command = await self.build_command(json_message) _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) if self._use_popen: @@ -336,7 +341,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): return self._proc is not None and self._proc.returncode is None @websocket_method("stdin") - def handle_stdin(self, json_message): + async def handle_stdin(self, json_message: dict[str, Any]) -> None: if not self.is_process_active: return text: str = json_message["data"] @@ -345,7 +350,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): self._proc.stdin.write(data) @tornado.gen.coroutine - def _redirect_stdout(self): + def _redirect_stdout(self) -> None: reg = b"[\n\r]" while True: @@ -364,7 +369,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): _LOGGER.debug("> stdout: %s", text) self.write_message({"event": "line", "data": text}) - def _stdout_thread(self): + def _stdout_thread(self) -> None: if not self._use_popen: return while True: @@ -377,13 +382,13 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): self._proc.wait(1.0) self._queue.put_nowait(None) - def _proc_on_exit(self, returncode): + def _proc_on_exit(self, returncode: int) -> None: if not self._is_closed: # Check if the proc was not forcibly closed _LOGGER.info("Process exited with return code %s", returncode) self.write_message({"event": "exit", "code": returncode}) - def on_close(self): + def on_close(self) -> None: # Check if proc exists (if 'start' has been run) if self.is_process_active: _LOGGER.debug("Terminating process") @@ -394,32 +399,54 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # Shutdown proc on WS close self._is_closed = True - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: raise NotImplementedError -class EsphomeLogsHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) +DASHBOARD_COMMAND = ["esphome", "--dashboard"] + + +class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): + """Base class for commands that require a port.""" + + async def build_device_command( + self, args: list[str], json_message: dict[str, Any] + ) -> list[str]: + """Build the command to run.""" + configuration = json_message["configuration"] + config_file = settings.rel_path(configuration) + port = json_message["port"] + if ( + port == "OTA" + and (mdns := MDNS_CONTAINER.get_mdns()) + and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) + and (address := await mdns.async_resolve_host(host_name)) + ): + port = address + return [ - "esphome", - "--dashboard", - "logs", + *DASHBOARD_COMMAND, + *args, config_file, "--device", - json_message["port"], + port, ] +class EsphomeLogsHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["logs"], json_message) + + class EsphomeRenameHandler(EsphomeCommandWebSocket): old_name: str - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) self.old_name = json_message["configuration"] return [ - "esphome", - "--dashboard", + *DASHBOARD_COMMAND, "rename", config_file, json_message["newName"], @@ -435,36 +462,22 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): PING_RESULT.pop(self.old_name, None) -class EsphomeUploadHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return [ - "esphome", - "--dashboard", - "upload", - config_file, - "--device", - json_message["port"], - ] +class EsphomeUploadHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["upload"], json_message) -class EsphomeRunHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - config_file = settings.rel_path(json_message["configuration"]) - return [ - "esphome", - "--dashboard", - "run", - config_file, - "--device", - json_message["port"], - ] +class EsphomeRunHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["run"], json_message) class EsphomeCompileHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) - command = ["esphome", "--dashboard", "compile"] + command = [*DASHBOARD_COMMAND, "compile"] if json_message.get("only_generate", False): command.append("--only-generate") command.append(config_file) @@ -472,39 +485,39 @@ class EsphomeCompileHandler(EsphomeCommandWebSocket): class EsphomeValidateHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) - command = ["esphome", "--dashboard", "config", config_file] + command = [*DASHBOARD_COMMAND, "config", config_file] if not settings.streamer_mode: command.append("--show-secrets") return command class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "clean-mqtt", config_file] + return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] class EsphomeCleanHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "clean", config_file] + return [*DASHBOARD_COMMAND, "clean", config_file] class EsphomeVscodeHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", "dummy"] + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"] class EsphomeAceEditorHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir] + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir] class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): - def build_command(self, json_message): - return ["esphome", "--dashboard", "update-all", settings.config_dir] + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] class SerialPortRequestHandler(BaseHandler): @@ -838,8 +851,9 @@ class DashboardEntry: class ListDevicesHandler(BaseHandler): @authenticated - def get(self): - entries = _list_dashboard_entries() + async def get(self): + loop = asyncio.get_running_loop() + entries = await loop.run_in_executor(None, _list_dashboard_entries) self.set_header("content-type", "application/json") configured = {entry.name for entry in entries} self.write( @@ -963,24 +977,39 @@ class BoardsRequestHandler(BaseHandler): self.write(json.dumps(output)) -class MDNSStatusThread(threading.Thread): - def __init__(self): - """Initialize the MDNSStatusThread.""" +class MDNSStatus: + """Class that updates the mdns status.""" + + def __init__(self) -> None: + """Initialize the MDNSStatus class.""" super().__init__() + self.aiozc: AsyncEsphomeZeroconf | None = None # This is the current mdns state for each host (True, False, None) self.host_mdns_state: dict[str, bool | None] = {} # This is the hostnames to filenames mapping self.host_name_to_filename: dict[str, str] = {} + self.filename_to_host_name: dict[str, str] = {} # This is a set of host names to track (i.e no_mdns = false) self.host_name_with_mdns_enabled: set[set] = set() - self._refresh_hosts() + self._loop = asyncio.get_running_loop() - def _refresh_hosts(self): + def filename_to_host_name_thread_safe(self, filename: str) -> str | None: + """Resolve a filename to an address in a thread-safe manner.""" + return self.filename_to_host_name.get(filename) + + async def async_resolve_host(self, host_name: str) -> str | None: + """Resolve a host name to an address in a thread-safe manner.""" + if aiozc := self.aiozc: + return await aiozc.async_resolve_host(host_name) + return None + + async def async_refresh_hosts(self): """Refresh the hosts to track.""" - entries = _list_dashboard_entries() + entries = await self._loop.run_in_executor(None, _list_dashboard_entries) host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename + filename_to_host_name = self.filename_to_host_name for entry in entries: name = entry.name @@ -1003,11 +1032,13 @@ class MDNSStatusThread(threading.Thread): # so when we get an mdns update we can map it back # to the filename host_name_to_filename[name] = filename + filename_to_host_name[filename] = name - def run(self): + async def async_run(self) -> None: global IMPORT_RESULT - zc = EsphomeZeroconf() + aiozc = AsyncEsphomeZeroconf() + self.aiozc = aiozc host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename host_name_with_mdns_enabled = self.host_name_with_mdns_enabled @@ -1020,21 +1051,23 @@ class MDNSStatusThread(threading.Thread): filename = host_name_to_filename[name] PING_RESULT[filename] = result - self._refresh_hosts() stat = DashboardStatus(on_update) imports = DashboardImportDiscovery() browser = DashboardBrowser( - zc, ESPHOME_SERVICE_TYPE, [stat.browser_callback, imports.browser_callback] + aiozc.zeroconf, + ESPHOME_SERVICE_TYPE, + [stat.browser_callback, imports.browser_callback], ) while not STOP_EVENT.is_set(): - self._refresh_hosts() + await self.async_refresh_hosts() IMPORT_RESULT = imports.import_state - PING_REQUEST.wait() - PING_REQUEST.clear() + await PING_REQUEST.async_wait() + PING_REQUEST.async_clear() - browser.cancel() - zc.close() + await browser.async_cancel() + await aiozc.async_close() + self.aiozc = None class PingStatusThread(threading.Thread): @@ -1211,11 +1244,26 @@ class UndoDeleteRequestHandler(BaseHandler): shutil.move(os.path.join(trash_path, configuration), config_file) +class MDNSContainer: + def __init__(self) -> None: + """Initialize the MDNSContainer.""" + self._mdns: MDNSStatus | None = None + + def set_mdns(self, mdns: MDNSStatus) -> None: + """Set the MDNSStatus instance.""" + self._mdns = mdns + + def get_mdns(self) -> MDNSStatus | None: + """Return the MDNSStatus instance.""" + return self._mdns + + PING_RESULT: dict = {} IMPORT_RESULT = {} STOP_EVENT = threading.Event() -PING_REQUEST = threading.Event() +PING_REQUEST = ThreadedAsyncEvent() MQTT_PING_REQUEST = threading.Event() +MDNS_CONTAINER = MDNSContainer() class LoginHandler(BaseHandler): @@ -1478,6 +1526,16 @@ def start_web_server(args): storage.save(path) settings.cookie_secret = storage.cookie_secret + try: + asyncio.run(async_start_web_server(args)) + except KeyboardInterrupt: + pass + + +async def async_start_web_server(args): + loop = asyncio.get_event_loop() + PING_REQUEST.async_setup(loop, asyncio.Event()) + app = make_app(args.verbose) if args.socket is not None: _LOGGER.info( @@ -1502,25 +1560,36 @@ def start_web_server(args): webbrowser.open(f"http://{args.address}:{args.port}") + mdns_task: asyncio.Task | None = None + ping_status_thread: PingStatusThread | None = None if settings.status_use_ping: - status_thread = PingStatusThread() + ping_status_thread = PingStatusThread() + ping_status_thread.start() else: - status_thread = MDNSStatusThread() - status_thread.start() + mdns_status = MDNSStatus() + await mdns_status.async_refresh_hosts() + MDNS_CONTAINER.set_mdns(mdns_status) + mdns_task = asyncio.create_task(mdns_status.async_run()) if settings.status_use_mqtt: status_thread_mqtt = MqttStatusThread() status_thread_mqtt.start() + shutdown_event = asyncio.Event() try: - tornado.ioloop.IOLoop.current().start() - except KeyboardInterrupt: + await shutdown_event.wait() + finally: _LOGGER.info("Shutting down...") STOP_EVENT.set() PING_REQUEST.set() - status_thread.join() + if ping_status_thread: + ping_status_thread.join() + MDNS_CONTAINER.set_mdns(None) + if mdns_task: + mdns_task.cancel() if settings.status_use_mqtt: status_thread_mqtt.join() MQTT_PING_REQUEST.set() if args.socket is not None: os.remove(args.socket) + await asyncio.sleep(0) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index f4cb7f080b..956e348e07 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -1,22 +1,21 @@ from __future__ import annotations +import asyncio import logging from dataclasses import dataclass from typing import Callable -from zeroconf import ( - IPVersion, - ServiceBrowser, - ServiceInfo, - ServiceStateChange, - Zeroconf, -) +from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf +from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf from esphome.storage_json import StorageJSON, ext_storage_path _LOGGER = logging.getLogger(__name__) +_BACKGROUND_TASKS: set[asyncio.Task] = set() + + class HostResolver(ServiceInfo): """Resolve a host name to an IP address.""" @@ -65,7 +64,7 @@ class DiscoveredImport: network: str -class DashboardBrowser(ServiceBrowser): +class DashboardBrowser(AsyncServiceBrowser): """A class to browse for ESPHome nodes.""" @@ -94,7 +93,28 @@ class DashboardImportDiscovery: # Ignore updates for devices that are not in the import state return - info = zeroconf.get_service_info(service_type, name) + info = AsyncServiceInfo( + service_type, + name, + ) + if info.load_from_cache(zeroconf): + self._process_service_info(name, info) + return + task = asyncio.create_task( + self._async_process_service_info(zeroconf, info, service_type, name) + ) + _BACKGROUND_TASKS.add(task) + task.add_done_callback(_BACKGROUND_TASKS.discard) + + async def _async_process_service_info( + self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str + ) -> None: + """Process a service info.""" + if await info.async_request(zeroconf): + self._process_service_info(name, info) + + def _process_service_info(self, name: str, info: ServiceInfo) -> None: + """Process a service info.""" _LOGGER.debug("-> resolved info: %s", info) if info is None: return @@ -146,14 +166,32 @@ class DashboardImportDiscovery: ) +def _make_host_resolver(host: str) -> HostResolver: + """Create a new HostResolver for the given host name.""" + name = host.partition(".")[0] + info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + return info + + class EsphomeZeroconf(Zeroconf): def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: """Resolve a host name to an IP address.""" - name = host.partition(".")[0] - info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + info = _make_host_resolver(host) if ( info.load_from_cache(self) or (timeout and info.request(self, timeout * 1000)) ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): return str(addresses[0]) return None + + +class AsyncEsphomeZeroconf(AsyncZeroconf): + async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + """Resolve a host name to an IP address.""" + info = _make_host_resolver(host) + if ( + info.load_from_cache(self.zeroconf) + or (timeout and await info.async_request(self.zeroconf, timeout * 1000)) + ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): + return str(addresses[0]) + return None From 642db6d92bd2cd3526dfc30b52f91cd26dcdb262 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Nov 2023 22:14:37 -0600 Subject: [PATCH 210/270] Speed up OTAs (#5720) --- esphome/espota2.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/esphome/espota2.py b/esphome/espota2.py index 98d6d3a0d9..dbf48a989a 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -1,10 +1,13 @@ +from __future__ import annotations + +import gzip import hashlib +import io import logging import random import socket import sys import time -import gzip from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address @@ -40,6 +43,10 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] FEATURE_SUPPORTS_COMPRESSION = 0x01 + +UPLOAD_BLOCK_SIZE = 8192 +UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 + _LOGGER = logging.getLogger(__name__) @@ -184,7 +191,9 @@ def send_check(sock, data, msg): raise OTAError(f"Error sending {msg}: {err}") from err -def perform_ota(sock, password, file_handle, filename): +def perform_ota( + sock: socket.socket, password: str, file_handle: io.IOBase, filename: str +) -> None: file_contents = file_handle.read() file_size = len(file_contents) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) @@ -254,14 +263,16 @@ def perform_ota(sock, password, file_handle, filename): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0) # Limit send buffer (usually around 100kB) in order to have progress bar # show the actual progress - sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE) # Set higher timeout during upload - sock.settimeout(20.0) + sock.settimeout(30.0) + start_time = time.perf_counter() offset = 0 progress = ProgressBar() while True: - chunk = upload_contents[offset : offset + 1024] + chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE] if not chunk: break offset += len(chunk) @@ -277,8 +288,9 @@ def perform_ota(sock, password, file_handle, filename): # Enable nodelay for last checks sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + duration = time.perf_counter() - start_time - _LOGGER.info("Waiting for result...") + _LOGGER.info("Upload took %.2f seconds, waiting for result...", duration) receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK) receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK) From 20ea8bf06e4414a038df087c210a47e94cefab23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Nov 2023 22:55:33 -0600 Subject: [PATCH 211/270] dashboard: convert ping thread to use asyncio (#5749) --- esphome/dashboard/async_adapter.py | 29 +------- esphome/dashboard/dashboard.py | 112 +++++++++++++---------------- esphome/dashboard/util.py | 20 ++++++ 3 files changed, 73 insertions(+), 88 deletions(-) diff --git a/esphome/dashboard/async_adapter.py b/esphome/dashboard/async_adapter.py index d6f4f6e1ff..44d2f42ce0 100644 --- a/esphome/dashboard/async_adapter.py +++ b/esphome/dashboard/async_adapter.py @@ -1,18 +1,13 @@ from __future__ import annotations import asyncio -import threading -class ThreadedAsyncEvent: - """This is a shim to allow the asyncio event to be used in a threaded context. - - When more of the code is moved to asyncio, this can be removed. - """ +class AsyncEvent: + """This is a shim around asyncio.Event.""" def __init__(self) -> None: """Initialize the ThreadedAsyncEvent.""" - self.event = threading.Event() self.async_event: asyncio.Event | None = None self.loop: asyncio.AbstractEventLoop | None = None @@ -26,31 +21,11 @@ class ThreadedAsyncEvent: def async_set(self) -> None: """Set the asyncio.Event instance.""" self.async_event.set() - self.event.set() - - def set(self) -> None: - """Set the event.""" - self.loop.call_soon_threadsafe(self.async_event.set) - self.event.set() - - def wait(self) -> None: - """Wait for the event.""" - self.event.wait() async def async_wait(self) -> None: """Wait the event async.""" await self.async_event.wait() - def clear(self) -> None: - """Clear the event.""" - self.loop.call_soon_threadsafe(self.async_event.clear) - self.event.clear() - def async_clear(self) -> None: """Clear the event async.""" self.async_event.clear() - self.event.clear() - - def is_set(self) -> bool: - """Return if the event is set.""" - return self.event.is_set() diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index d7d11d8693..950386d969 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import base64 import binascii -import collections import datetime import functools import gzip @@ -11,14 +10,13 @@ import hashlib import hmac import json import logging -import multiprocessing import os import secrets import shutil import subprocess import threading from pathlib import Path -from typing import Any +from typing import Any, cast import tornado import tornado.concurrent @@ -52,9 +50,9 @@ from esphome.zeroconf import ( DashboardImportDiscovery, DashboardStatus, ) -from .async_adapter import ThreadedAsyncEvent -from .util import friendly_name_slugify, password_hash +from .async_adapter import AsyncEvent +from .util import chunked, friendly_name_slugify, password_hash _LOGGER = logging.getLogger(__name__) @@ -603,7 +601,7 @@ class ImportRequestHandler(BaseHandler): encryption, ) # Make sure the device gets marked online right away - PING_REQUEST.set() + PING_REQUEST.async_set() except FileExistsError: self.set_status(500) self.write("File already exists") @@ -905,15 +903,6 @@ class MainRequestHandler(BaseHandler): ) -def _ping_func(filename, address): - if os.name == "nt": - command = ["ping", "-n", "1", address] - else: - command = ["ping", "-c", "1", address] - rc, _, _ = run_system_command(*command) - return filename, rc == 0 - - class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated def get(self): @@ -1070,47 +1059,48 @@ class MDNSStatus: self.aiozc = None -class PingStatusThread(threading.Thread): - def run(self): - with multiprocessing.Pool(processes=8) as pool: - while not STOP_EVENT.wait(2): - # Only do pings if somebody has the dashboard open +async def _async_ping_host(host: str) -> bool: + """Ping a host.""" + ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"] + process = await asyncio.create_subprocess_exec( + *ping_command, + host, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await process.wait() + return process.returncode == 0 - def callback(ret): - PING_RESULT[ret[0]] = ret[1] - entries = _list_dashboard_entries() - queue = collections.deque() - for entry in entries: - if entry.address is None: - PING_RESULT[entry.filename] = None - continue +class PingStatus: + def __init__(self) -> None: + """Initialize the PingStatus class.""" + super().__init__() + self._loop = asyncio.get_running_loop() - result = pool.apply_async( - _ping_func, (entry.filename, entry.address), callback=callback - ) - queue.append(result) - - while queue: - item = queue[0] - if item.ready(): - queue.popleft() - continue - - try: - item.get(0.1) - except OSError: - # ping not installed - pass - except multiprocessing.TimeoutError: - pass - - if STOP_EVENT.is_set(): - pool.terminate() - return - - PING_REQUEST.wait() - PING_REQUEST.clear() + async def async_run(self) -> None: + """Run the ping status.""" + while not STOP_EVENT.is_set(): + # Only ping if the dashboard is open + await PING_REQUEST.async_wait() + PING_REQUEST.async_clear() + entries = await self._loop.run_in_executor(None, _list_dashboard_entries) + to_ping: list[DashboardEntry] = [ + entry for entry in entries if entry.address is not None + ] + for ping_group in chunked(to_ping, 16): + ping_group = cast(list[DashboardEntry], ping_group) + results = await asyncio.gather( + *(_async_ping_host(entry.address) for entry in ping_group), + return_exceptions=True, + ) + for entry, result in zip(ping_group, results): + if isinstance(result, Exception): + result = False + elif isinstance(result, BaseException): + raise result + PING_RESULT[entry.filename] = result class MqttStatusThread(threading.Thread): @@ -1171,7 +1161,7 @@ class MqttStatusThread(threading.Thread): class PingRequestHandler(BaseHandler): @authenticated def get(self): - PING_REQUEST.set() + PING_REQUEST.async_set() if settings.status_use_mqtt: MQTT_PING_REQUEST.set() self.set_header("content-type", "application/json") @@ -1261,7 +1251,7 @@ class MDNSContainer: PING_RESULT: dict = {} IMPORT_RESULT = {} STOP_EVENT = threading.Event() -PING_REQUEST = ThreadedAsyncEvent() +PING_REQUEST = AsyncEvent() MQTT_PING_REQUEST = threading.Event() MDNS_CONTAINER = MDNSContainer() @@ -1561,10 +1551,10 @@ async def async_start_web_server(args): webbrowser.open(f"http://{args.address}:{args.port}") mdns_task: asyncio.Task | None = None - ping_status_thread: PingStatusThread | None = None + ping_status_task: asyncio.Task | None = None if settings.status_use_ping: - ping_status_thread = PingStatusThread() - ping_status_thread.start() + ping_status = PingStatus() + ping_status_task = asyncio.create_task(ping_status.async_run()) else: mdns_status = MDNSStatus() await mdns_status.async_refresh_hosts() @@ -1581,9 +1571,9 @@ async def async_start_web_server(args): finally: _LOGGER.info("Shutting down...") STOP_EVENT.set() - PING_REQUEST.set() - if ping_status_thread: - ping_status_thread.join() + PING_REQUEST.async_set() + if ping_status_task: + ping_status_task.cancel() MDNS_CONTAINER.set_mdns(None) if mdns_task: mdns_task.cancel() diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py index a2ad530b74..7b6572b989 100644 --- a/esphome/dashboard/util.py +++ b/esphome/dashboard/util.py @@ -1,5 +1,9 @@ import hashlib import unicodedata +from collections.abc import Iterable +from functools import partial +from itertools import islice +from typing import Any from esphome.const import ALLOWED_NAME_CHARS @@ -30,3 +34,19 @@ def friendly_name_slugify(value): .strip("-") ) return "".join(c for c in value if c in ALLOWED_NAME_CHARS) + + +def take(take_num: int, iterable: Iterable) -> list[Any]: + """Return first n items of the iterable as a list. + + From itertools recipes + """ + return list(islice(iterable, take_num)) + + +def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: + """Break *iterable* into lists of length *n*. + + From more-itertools + """ + return iter(partial(take, chunked_num, iter(iterable)), []) From 86b4fdc139d2fa0193c10db3ba4f6e9290074190 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 12:00:28 -0600 Subject: [PATCH 212/270] dashboard: Break apart dashboard into separate modules (#5764) * Break apart dashboard into seperate modules * reduce code change * late imports * late imports * preen * remove accidental changes * save the file --- esphome/dashboard/async_adapter.py | 31 -- esphome/dashboard/core.py | 95 +++++ esphome/dashboard/dashboard.py | 553 ++------------------------- esphome/dashboard/entries.py | 116 ++++++ esphome/dashboard/settings.py | 146 +++++++ esphome/dashboard/status/__init__.py | 0 esphome/dashboard/status/mdns.py | 109 ++++++ esphome/dashboard/status/mqtt.py | 67 ++++ esphome/dashboard/status/ping.py | 57 +++ 9 files changed, 624 insertions(+), 550 deletions(-) delete mode 100644 esphome/dashboard/async_adapter.py create mode 100644 esphome/dashboard/core.py create mode 100644 esphome/dashboard/entries.py create mode 100644 esphome/dashboard/settings.py create mode 100644 esphome/dashboard/status/__init__.py create mode 100644 esphome/dashboard/status/mdns.py create mode 100644 esphome/dashboard/status/mqtt.py create mode 100644 esphome/dashboard/status/ping.py diff --git a/esphome/dashboard/async_adapter.py b/esphome/dashboard/async_adapter.py deleted file mode 100644 index 44d2f42ce0..0000000000 --- a/esphome/dashboard/async_adapter.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -import asyncio - - -class AsyncEvent: - """This is a shim around asyncio.Event.""" - - def __init__(self) -> None: - """Initialize the ThreadedAsyncEvent.""" - self.async_event: asyncio.Event | None = None - self.loop: asyncio.AbstractEventLoop | None = None - - def async_setup( - self, loop: asyncio.AbstractEventLoop, async_event: asyncio.Event - ) -> None: - """Set the asyncio.Event instance.""" - self.loop = loop - self.async_event = async_event - - def async_set(self) -> None: - """Set the asyncio.Event instance.""" - self.async_event.set() - - async def async_wait(self) -> None: - """Wait the event async.""" - await self.async_event.wait() - - def async_clear(self) -> None: - """Clear the event async.""" - self.async_event.clear() diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py new file mode 100644 index 0000000000..4cc2938bb1 --- /dev/null +++ b/esphome/dashboard/core.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import asyncio +import logging +import threading +from typing import TYPE_CHECKING + +from ..zeroconf import DiscoveredImport +from .entries import DashboardEntry +from .settings import DashboardSettings + +if TYPE_CHECKING: + from .status.mdns import MDNSStatus + +_LOGGER = logging.getLogger(__name__) + + +def list_dashboard_entries() -> list[DashboardEntry]: + """List all dashboard entries.""" + return DASHBOARD.settings.entries() + + +class ESPHomeDashboard: + """Class that represents the dashboard.""" + + __slots__ = ( + "loop", + "ping_result", + "import_result", + "stop_event", + "ping_request", + "mqtt_ping_request", + "mdns_status", + "settings", + ) + + def __init__(self) -> None: + """Initialize the ESPHomeDashboard.""" + self.loop: asyncio.AbstractEventLoop | None = None + self.ping_result: dict[str, bool | None] = {} + self.import_result: dict[str, DiscoveredImport] = {} + self.stop_event = threading.Event() + self.ping_request: asyncio.Event | None = None + self.mqtt_ping_request = threading.Event() + self.mdns_status: MDNSStatus | None = None + self.settings: DashboardSettings = DashboardSettings() + + async def async_setup(self) -> None: + """Setup the dashboard.""" + self.loop = asyncio.get_running_loop() + self.ping_request = asyncio.Event() + + async def async_run(self) -> None: + """Run the dashboard.""" + settings = self.settings + mdns_task: asyncio.Task | None = None + ping_status_task: asyncio.Task | None = None + + if settings.status_use_ping: + from .status.ping import PingStatus + + ping_status = PingStatus() + ping_status_task = asyncio.create_task(ping_status.async_run()) + else: + from .status.mdns import MDNSStatus + + mdns_status = MDNSStatus() + await mdns_status.async_refresh_hosts() + self.mdns_status = mdns_status + mdns_task = asyncio.create_task(mdns_status.async_run()) + + if settings.status_use_mqtt: + from .status.mqtt import MqttStatusThread + + status_thread_mqtt = MqttStatusThread() + status_thread_mqtt.start() + + shutdown_event = asyncio.Event() + try: + await shutdown_event.wait() + finally: + _LOGGER.info("Shutting down...") + self.stop_event.set() + self.ping_request.set() + if ping_status_task: + ping_status_task.cancel() + if mdns_task: + mdns_task.cancel() + if settings.status_use_mqtt: + status_thread_mqtt.join() + self.mqtt_ping_request.set() + await asyncio.sleep(0) + + +DASHBOARD = ESPHomeDashboard() diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 950386d969..4a417fe190 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -2,12 +2,10 @@ from __future__ import annotations import asyncio import base64 -import binascii import datetime import functools import gzip import hashlib -import hmac import json import logging import os @@ -16,7 +14,7 @@ import shutil import subprocess import threading from pathlib import Path -from typing import Any, cast +from typing import Any import tornado import tornado.concurrent @@ -32,8 +30,7 @@ import tornado.websocket import yaml from tornado.log import access_log -from esphome import const, platformio_api, util, yaml_util -from esphome.core import CORE +from esphome import const, platformio_api, yaml_util from esphome.helpers import get_bool_env, mkdir_p, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, @@ -43,158 +40,22 @@ from esphome.storage_json import ( trash_storage_path, ) from esphome.util import get_serial_ports, shlex_quote -from esphome.zeroconf import ( - ESPHOME_SERVICE_TYPE, - AsyncEsphomeZeroconf, - DashboardBrowser, - DashboardImportDiscovery, - DashboardStatus, -) -from .async_adapter import AsyncEvent -from .util import chunked, friendly_name_slugify, password_hash +from .core import DASHBOARD, list_dashboard_entries +from .entries import DashboardEntry +from .util import friendly_name_slugify _LOGGER = logging.getLogger(__name__) ENV_DEV = "ESPHOME_DASHBOARD_DEV" -class DashboardSettings: - def __init__(self): - self.config_dir = "" - self.password_hash = "" - self.username = "" - self.using_password = False - self.on_ha_addon = False - self.cookie_secret = None - self.absolute_config_dir = None - self._entry_cache: dict[ - str, tuple[tuple[int, int, float, int], DashboardEntry] - ] = {} - - def parse_args(self, args): - self.on_ha_addon = args.ha_addon - password = args.password or os.getenv("PASSWORD", "") - if not self.on_ha_addon: - self.username = args.username or os.getenv("USERNAME", "") - self.using_password = bool(password) - if self.using_password: - self.password_hash = password_hash(password) - self.config_dir = args.configuration - self.absolute_config_dir = Path(self.config_dir).resolve() - CORE.config_path = os.path.join(self.config_dir, ".") - - @property - def relative_url(self): - return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/") - - @property - def status_use_ping(self): - return get_bool_env("ESPHOME_DASHBOARD_USE_PING") - - @property - def status_use_mqtt(self): - return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") - - @property - def using_ha_addon_auth(self): - if not self.on_ha_addon: - return False - return not get_bool_env("DISABLE_HA_AUTHENTICATION") - - @property - def using_auth(self): - return self.using_password or self.using_ha_addon_auth - - @property - def streamer_mode(self): - return get_bool_env("ESPHOME_STREAMER_MODE") - - def check_password(self, username, password): - if not self.using_auth: - return True - if username != self.username: - return False - - # Compare password in constant running time (to prevent timing attacks) - return hmac.compare_digest(self.password_hash, password_hash(password)) - - def rel_path(self, *args): - joined_path = os.path.join(self.config_dir, *args) - # Raises ValueError if not relative to ESPHome config folder - Path(joined_path).resolve().relative_to(self.absolute_config_dir) - return joined_path - - def list_yaml_files(self) -> list[str]: - return util.list_yaml_files([self.config_dir]) - - def entries(self) -> list[DashboardEntry]: - """Fetch all dashboard entries, thread-safe.""" - path_to_cache_key: dict[str, tuple[int, int, float, int]] = {} - # - # The cache key is (inode, device, mtime, size) - # which allows us to avoid locking since it ensures - # every iteration of this call will always return the newest - # items from disk at the cost of a stat() call on each - # file which is much faster than reading the file - # for the cache hit case which is the common case. - # - # Because there is no lock the cache may - # get built more than once but that's fine as its still - # thread-safe and results in orders of magnitude less - # reads from disk than if we did not cache at all and - # does not have a lock contention issue. - # - for file in self.list_yaml_files(): - try: - # Prefer the json storage path if it exists - stat = os.stat(ext_storage_path(os.path.basename(file))) - except OSError: - try: - # Fallback to the yaml file if the storage - # file does not exist or could not be generated - stat = os.stat(file) - except OSError: - # File was deleted, ignore - continue - path_to_cache_key[file] = ( - stat.st_ino, - stat.st_dev, - stat.st_mtime, - stat.st_size, - ) - - entry_cache = self._entry_cache - - # Remove entries that no longer exist - removed: list[str] = [] - for file in entry_cache: - if file not in path_to_cache_key: - removed.append(file) - - for file in removed: - entry_cache.pop(file) - - dashboard_entries: list[DashboardEntry] = [] - for file, cache_key in path_to_cache_key.items(): - if cached_entry := entry_cache.get(file): - entry_key, dashboard_entry = cached_entry - if entry_key == cache_key: - dashboard_entries.append(dashboard_entry) - continue - - dashboard_entry = DashboardEntry(file) - dashboard_entries.append(dashboard_entry) - entry_cache[file] = (cache_key, dashboard_entry) - - return dashboard_entries - - -settings = DashboardSettings() - cookie_authenticated_yes = b"yes" +settings = DASHBOARD.settings + + def template_args(): version = const.__version__ if "b" in version: @@ -411,12 +272,13 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): self, args: list[str], json_message: dict[str, Any] ) -> list[str]: """Build the command to run.""" + dashboard = DASHBOARD configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] if ( port == "OTA" - and (mdns := MDNS_CONTAINER.get_mdns()) + and (mdns := dashboard.mdns_status) and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) and (address := await mdns.async_resolve_host(host_name)) ): @@ -457,7 +319,7 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): return # Remove the old ping result from the cache - PING_RESULT.pop(self.old_name, None) + DASHBOARD.ping_result.pop(self.old_name, None) class EsphomeUploadHandler(EsphomePortCommandWebSocket): @@ -574,6 +436,7 @@ class ImportRequestHandler(BaseHandler): def post(self): from esphome.components.dashboard_import import import_config + dashboard = DASHBOARD args = json.loads(self.request.body.decode()) try: name = args["name"] @@ -581,7 +444,12 @@ class ImportRequestHandler(BaseHandler): encryption = args.get("encryption", False) imported_device = next( - (res for res in IMPORT_RESULT.values() if res.device_name == name), None + ( + res + for res in dashboard.import_result.values() + if res.device_name == name + ), + None, ) if imported_device is not None: @@ -601,7 +469,7 @@ class ImportRequestHandler(BaseHandler): encryption, ) # Make sure the device gets marked online right away - PING_REQUEST.async_set() + dashboard.ping_request.set() except FileExistsError: self.set_status(500) self.write("File already exists") @@ -733,127 +601,15 @@ class EsphomeVersionHandler(BaseHandler): self.finish() -def _list_dashboard_entries() -> list[DashboardEntry]: - return settings.entries() - - -class DashboardEntry: - """Represents a single dashboard entry. - - This class is thread-safe and read-only. - """ - - __slots__ = ("path", "_storage", "_loaded_storage") - - def __init__(self, path: str) -> None: - """Initialize the DashboardEntry.""" - self.path = path - self._storage = None - self._loaded_storage = False - - def __repr__(self): - """Return the representation of this entry.""" - return ( - f"DashboardEntry({self.path} " - f"address={self.address} " - f"web_port={self.web_port} " - f"name={self.name} " - f"no_mdns={self.no_mdns})" - ) - - @property - def filename(self): - """Return the filename of this entry.""" - return os.path.basename(self.path) - - @property - def storage(self) -> StorageJSON | None: - """Return the StorageJSON object for this entry.""" - if not self._loaded_storage: - self._storage = StorageJSON.load(ext_storage_path(self.filename)) - self._loaded_storage = True - return self._storage - - @property - def address(self): - """Return the address of this entry.""" - if self.storage is None: - return None - return self.storage.address - - @property - def no_mdns(self): - """Return the no_mdns of this entry.""" - if self.storage is None: - return None - return self.storage.no_mdns - - @property - def web_port(self): - """Return the web port of this entry.""" - if self.storage is None: - return None - return self.storage.web_port - - @property - def name(self): - """Return the name of this entry.""" - if self.storage is None: - return self.filename.replace(".yml", "").replace(".yaml", "") - return self.storage.name - - @property - def friendly_name(self): - """Return the friendly name of this entry.""" - if self.storage is None: - return self.name - return self.storage.friendly_name - - @property - def comment(self): - """Return the comment of this entry.""" - if self.storage is None: - return None - return self.storage.comment - - @property - def target_platform(self): - """Return the target platform of this entry.""" - if self.storage is None: - return None - return self.storage.target_platform - - @property - def update_available(self): - """Return if an update is available for this entry.""" - if self.storage is None: - return True - return self.update_old != self.update_new - - @property - def update_old(self): - if self.storage is None: - return "" - return self.storage.esphome_version or "" - - @property - def update_new(self): - return const.__version__ - - @property - def loaded_integrations(self): - if self.storage is None: - return [] - return self.storage.loaded_integrations - - class ListDevicesHandler(BaseHandler): @authenticated async def get(self): loop = asyncio.get_running_loop() - entries = await loop.run_in_executor(None, _list_dashboard_entries) + entries = await loop.run_in_executor(None, list_dashboard_entries) self.set_header("content-type", "application/json") configured = {entry.name for entry in entries} + dashboard = DASHBOARD + self.write( json.dumps( { @@ -882,7 +638,7 @@ class ListDevicesHandler(BaseHandler): "project_version": res.project_version, "network": res.network, } - for res in IMPORT_RESULT.values() + for res in dashboard.import_result.values() if res.device_name not in configured ], } @@ -906,7 +662,7 @@ class MainRequestHandler(BaseHandler): class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated def get(self): - entries = _list_dashboard_entries() + entries = list_dashboard_entries() self.set_header("content-type", "application/json") sd = [] for entry in entries: @@ -966,206 +722,15 @@ class BoardsRequestHandler(BaseHandler): self.write(json.dumps(output)) -class MDNSStatus: - """Class that updates the mdns status.""" - - def __init__(self) -> None: - """Initialize the MDNSStatus class.""" - super().__init__() - self.aiozc: AsyncEsphomeZeroconf | None = None - # This is the current mdns state for each host (True, False, None) - self.host_mdns_state: dict[str, bool | None] = {} - # This is the hostnames to filenames mapping - self.host_name_to_filename: dict[str, str] = {} - self.filename_to_host_name: dict[str, str] = {} - # This is a set of host names to track (i.e no_mdns = false) - self.host_name_with_mdns_enabled: set[set] = set() - self._loop = asyncio.get_running_loop() - - def filename_to_host_name_thread_safe(self, filename: str) -> str | None: - """Resolve a filename to an address in a thread-safe manner.""" - return self.filename_to_host_name.get(filename) - - async def async_resolve_host(self, host_name: str) -> str | None: - """Resolve a host name to an address in a thread-safe manner.""" - if aiozc := self.aiozc: - return await aiozc.async_resolve_host(host_name) - return None - - async def async_refresh_hosts(self): - """Refresh the hosts to track.""" - entries = await self._loop.run_in_executor(None, _list_dashboard_entries) - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled - host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename - filename_to_host_name = self.filename_to_host_name - - for entry in entries: - name = entry.name - # If no_mdns is set, remove it from the set - if entry.no_mdns: - host_name_with_mdns_enabled.discard(name) - continue - - # We are tracking this host - host_name_with_mdns_enabled.add(name) - filename = entry.filename - - # If we just adopted/imported this host, we likely - # already have a state for it, so we should make sure - # to set it so the dashboard shows it as online - if name in host_mdns_state: - PING_RESULT[filename] = host_mdns_state[name] - - # Make sure the mapping is up to date - # so when we get an mdns update we can map it back - # to the filename - host_name_to_filename[name] = filename - filename_to_host_name[filename] = name - - async def async_run(self) -> None: - global IMPORT_RESULT - - aiozc = AsyncEsphomeZeroconf() - self.aiozc = aiozc - host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled - - def on_update(dat: dict[str, bool | None]) -> None: - """Update the global PING_RESULT dict.""" - for name, result in dat.items(): - host_mdns_state[name] = result - if name in host_name_with_mdns_enabled: - filename = host_name_to_filename[name] - PING_RESULT[filename] = result - - stat = DashboardStatus(on_update) - imports = DashboardImportDiscovery() - browser = DashboardBrowser( - aiozc.zeroconf, - ESPHOME_SERVICE_TYPE, - [stat.browser_callback, imports.browser_callback], - ) - - while not STOP_EVENT.is_set(): - await self.async_refresh_hosts() - IMPORT_RESULT = imports.import_state - await PING_REQUEST.async_wait() - PING_REQUEST.async_clear() - - await browser.async_cancel() - await aiozc.async_close() - self.aiozc = None - - -async def _async_ping_host(host: str) -> bool: - """Ping a host.""" - ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"] - process = await asyncio.create_subprocess_exec( - *ping_command, - host, - stdin=asyncio.subprocess.DEVNULL, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL, - ) - await process.wait() - return process.returncode == 0 - - -class PingStatus: - def __init__(self) -> None: - """Initialize the PingStatus class.""" - super().__init__() - self._loop = asyncio.get_running_loop() - - async def async_run(self) -> None: - """Run the ping status.""" - while not STOP_EVENT.is_set(): - # Only ping if the dashboard is open - await PING_REQUEST.async_wait() - PING_REQUEST.async_clear() - entries = await self._loop.run_in_executor(None, _list_dashboard_entries) - to_ping: list[DashboardEntry] = [ - entry for entry in entries if entry.address is not None - ] - for ping_group in chunked(to_ping, 16): - ping_group = cast(list[DashboardEntry], ping_group) - results = await asyncio.gather( - *(_async_ping_host(entry.address) for entry in ping_group), - return_exceptions=True, - ) - for entry, result in zip(ping_group, results): - if isinstance(result, Exception): - result = False - elif isinstance(result, BaseException): - raise result - PING_RESULT[entry.filename] = result - - -class MqttStatusThread(threading.Thread): - def run(self): - from esphome import mqtt - - entries = _list_dashboard_entries() - - config = mqtt.config_from_env() - topic = "esphome/discover/#" - - def on_message(client, userdata, msg): - nonlocal entries - - payload = msg.payload.decode(errors="backslashreplace") - if len(payload) > 0: - data = json.loads(payload) - if "name" not in data: - return - for entry in entries: - if entry.name == data["name"]: - PING_RESULT[entry.filename] = True - return - - def on_connect(client, userdata, flags, return_code): - client.publish("esphome/discover", None, retain=False) - - mqttid = str(binascii.hexlify(os.urandom(6)).decode()) - - client = mqtt.prepare( - config, - [topic], - on_message, - on_connect, - None, - None, - f"esphome-dashboard-{mqttid}", - ) - client.loop_start() - - while not STOP_EVENT.wait(2): - # update entries - entries = _list_dashboard_entries() - - # will be set to true on on_message - for entry in entries: - if entry.no_mdns: - PING_RESULT[entry.filename] = False - - client.publish("esphome/discover", None, retain=False) - MQTT_PING_REQUEST.wait() - MQTT_PING_REQUEST.clear() - - client.disconnect() - client.loop_stop() - - class PingRequestHandler(BaseHandler): @authenticated def get(self): - PING_REQUEST.async_set() + dashboard = DASHBOARD + dashboard.ping_request.set() if settings.status_use_mqtt: - MQTT_PING_REQUEST.set() + dashboard.mqtt_ping_request.set() self.set_header("content-type", "application/json") - self.write(json.dumps(PING_RESULT)) + self.write(json.dumps(dashboard.ping_result)) class InfoRequestHandler(BaseHandler): @@ -1222,7 +787,7 @@ class DeleteRequestHandler(BaseHandler): shutil.rmtree(build_folder, os.path.join(trash_path, name)) # Remove the old ping result from the cache - PING_RESULT.pop(configuration, None) + DASHBOARD.ping_result.pop(configuration, None) class UndoDeleteRequestHandler(BaseHandler): @@ -1234,28 +799,6 @@ class UndoDeleteRequestHandler(BaseHandler): shutil.move(os.path.join(trash_path, configuration), config_file) -class MDNSContainer: - def __init__(self) -> None: - """Initialize the MDNSContainer.""" - self._mdns: MDNSStatus | None = None - - def set_mdns(self, mdns: MDNSStatus) -> None: - """Set the MDNSStatus instance.""" - self._mdns = mdns - - def get_mdns(self) -> MDNSStatus | None: - """Return the MDNSStatus instance.""" - return self._mdns - - -PING_RESULT: dict = {} -IMPORT_RESULT = {} -STOP_EVENT = threading.Event() -PING_REQUEST = AsyncEvent() -MQTT_PING_REQUEST = threading.Event() -MDNS_CONTAINER = MDNSContainer() - - class LoginHandler(BaseHandler): def get(self): if is_authenticated(self): @@ -1517,14 +1060,14 @@ def start_web_server(args): settings.cookie_secret = storage.cookie_secret try: - asyncio.run(async_start_web_server(args)) + asyncio.run(async_start(args)) except KeyboardInterrupt: pass -async def async_start_web_server(args): - loop = asyncio.get_event_loop() - PING_REQUEST.async_setup(loop, asyncio.Event()) +async def async_start(args) -> None: + dashboard = DASHBOARD + await dashboard.async_setup() app = make_app(args.verbose) if args.socket is not None: @@ -1550,36 +1093,8 @@ async def async_start_web_server(args): webbrowser.open(f"http://{args.address}:{args.port}") - mdns_task: asyncio.Task | None = None - ping_status_task: asyncio.Task | None = None - if settings.status_use_ping: - ping_status = PingStatus() - ping_status_task = asyncio.create_task(ping_status.async_run()) - else: - mdns_status = MDNSStatus() - await mdns_status.async_refresh_hosts() - MDNS_CONTAINER.set_mdns(mdns_status) - mdns_task = asyncio.create_task(mdns_status.async_run()) - - if settings.status_use_mqtt: - status_thread_mqtt = MqttStatusThread() - status_thread_mqtt.start() - - shutdown_event = asyncio.Event() try: - await shutdown_event.wait() + await dashboard.async_run() finally: - _LOGGER.info("Shutting down...") - STOP_EVENT.set() - PING_REQUEST.async_set() - if ping_status_task: - ping_status_task.cancel() - MDNS_CONTAINER.set_mdns(None) - if mdns_task: - mdns_task.cancel() - if settings.status_use_mqtt: - status_thread_mqtt.join() - MQTT_PING_REQUEST.set() if args.socket is not None: os.remove(args.socket) - await asyncio.sleep(0) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py new file mode 100644 index 0000000000..582073d655 --- /dev/null +++ b/esphome/dashboard/entries.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import os + +from esphome import const +from esphome.storage_json import StorageJSON, ext_storage_path + + +class DashboardEntry: + """Represents a single dashboard entry. + + This class is thread-safe and read-only. + """ + + __slots__ = ("path", "_storage", "_loaded_storage") + + def __init__(self, path: str) -> None: + """Initialize the DashboardEntry.""" + self.path = path + self._storage = None + self._loaded_storage = False + + def __repr__(self): + """Return the representation of this entry.""" + return ( + f"DashboardEntry({self.path} " + f"address={self.address} " + f"web_port={self.web_port} " + f"name={self.name} " + f"no_mdns={self.no_mdns})" + ) + + @property + def filename(self): + """Return the filename of this entry.""" + return os.path.basename(self.path) + + @property + def storage(self) -> StorageJSON | None: + """Return the StorageJSON object for this entry.""" + if not self._loaded_storage: + self._storage = StorageJSON.load(ext_storage_path(self.filename)) + self._loaded_storage = True + return self._storage + + @property + def address(self): + """Return the address of this entry.""" + if self.storage is None: + return None + return self.storage.address + + @property + def no_mdns(self): + """Return the no_mdns of this entry.""" + if self.storage is None: + return None + return self.storage.no_mdns + + @property + def web_port(self): + """Return the web port of this entry.""" + if self.storage is None: + return None + return self.storage.web_port + + @property + def name(self): + """Return the name of this entry.""" + if self.storage is None: + return self.filename.replace(".yml", "").replace(".yaml", "") + return self.storage.name + + @property + def friendly_name(self): + """Return the friendly name of this entry.""" + if self.storage is None: + return self.name + return self.storage.friendly_name + + @property + def comment(self): + """Return the comment of this entry.""" + if self.storage is None: + return None + return self.storage.comment + + @property + def target_platform(self): + """Return the target platform of this entry.""" + if self.storage is None: + return None + return self.storage.target_platform + + @property + def update_available(self): + """Return if an update is available for this entry.""" + if self.storage is None: + return True + return self.update_old != self.update_new + + @property + def update_old(self): + if self.storage is None: + return "" + return self.storage.esphome_version or "" + + @property + def update_new(self): + return const.__version__ + + @property + def loaded_integrations(self): + if self.storage is None: + return [] + return self.storage.loaded_integrations diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py new file mode 100644 index 0000000000..3409938e0a --- /dev/null +++ b/esphome/dashboard/settings.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import hmac +import os +from pathlib import Path + +from esphome import util +from esphome.core import CORE +from esphome.helpers import get_bool_env +from esphome.storage_json import ext_storage_path + +from .entries import DashboardEntry +from .util import password_hash + + +class DashboardSettings: + """Settings for the dashboard.""" + + def __init__(self): + self.config_dir = "" + self.password_hash = "" + self.username = "" + self.using_password = False + self.on_ha_addon = False + self.cookie_secret = None + self.absolute_config_dir = None + self._entry_cache: dict[ + str, tuple[tuple[int, int, float, int], DashboardEntry] + ] = {} + + def parse_args(self, args): + self.on_ha_addon = args.ha_addon + password = args.password or os.getenv("PASSWORD", "") + if not self.on_ha_addon: + self.username = args.username or os.getenv("USERNAME", "") + self.using_password = bool(password) + if self.using_password: + self.password_hash = password_hash(password) + self.config_dir = args.configuration + self.absolute_config_dir = Path(self.config_dir).resolve() + CORE.config_path = os.path.join(self.config_dir, ".") + + @property + def relative_url(self): + return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/") + + @property + def status_use_ping(self): + return get_bool_env("ESPHOME_DASHBOARD_USE_PING") + + @property + def status_use_mqtt(self): + return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") + + @property + def using_ha_addon_auth(self): + if not self.on_ha_addon: + return False + return not get_bool_env("DISABLE_HA_AUTHENTICATION") + + @property + def using_auth(self): + return self.using_password or self.using_ha_addon_auth + + @property + def streamer_mode(self): + return get_bool_env("ESPHOME_STREAMER_MODE") + + def check_password(self, username, password): + if not self.using_auth: + return True + if username != self.username: + return False + + # Compare password in constant running time (to prevent timing attacks) + return hmac.compare_digest(self.password_hash, password_hash(password)) + + def rel_path(self, *args): + joined_path = os.path.join(self.config_dir, *args) + # Raises ValueError if not relative to ESPHome config folder + Path(joined_path).resolve().relative_to(self.absolute_config_dir) + return joined_path + + def list_yaml_files(self) -> list[str]: + return util.list_yaml_files([self.config_dir]) + + def entries(self) -> list[DashboardEntry]: + """Fetch all dashboard entries, thread-safe.""" + path_to_cache_key: dict[str, tuple[int, int, float, int]] = {} + # + # The cache key is (inode, device, mtime, size) + # which allows us to avoid locking since it ensures + # every iteration of this call will always return the newest + # items from disk at the cost of a stat() call on each + # file which is much faster than reading the file + # for the cache hit case which is the common case. + # + # Because there is no lock the cache may + # get built more than once but that's fine as its still + # thread-safe and results in orders of magnitude less + # reads from disk than if we did not cache at all and + # does not have a lock contention issue. + # + for file in self.list_yaml_files(): + try: + # Prefer the json storage path if it exists + stat = os.stat(ext_storage_path(os.path.basename(file))) + except OSError: + try: + # Fallback to the yaml file if the storage + # file does not exist or could not be generated + stat = os.stat(file) + except OSError: + # File was deleted, ignore + continue + path_to_cache_key[file] = ( + stat.st_ino, + stat.st_dev, + stat.st_mtime, + stat.st_size, + ) + + entry_cache = self._entry_cache + + # Remove entries that no longer exist + removed: list[str] = [] + for file in entry_cache: + if file not in path_to_cache_key: + removed.append(file) + + for file in removed: + entry_cache.pop(file) + + dashboard_entries: list[DashboardEntry] = [] + for file, cache_key in path_to_cache_key.items(): + if cached_entry := entry_cache.get(file): + entry_key, dashboard_entry = cached_entry + if entry_key == cache_key: + dashboard_entries.append(dashboard_entry) + continue + + dashboard_entry = DashboardEntry(file) + dashboard_entries.append(dashboard_entry) + entry_cache[file] = (cache_key, dashboard_entry) + + return dashboard_entries diff --git a/esphome/dashboard/status/__init__.py b/esphome/dashboard/status/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py new file mode 100644 index 0000000000..454bba9cb5 --- /dev/null +++ b/esphome/dashboard/status/mdns.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import asyncio + +from esphome.zeroconf import ( + ESPHOME_SERVICE_TYPE, + AsyncEsphomeZeroconf, + DashboardBrowser, + DashboardImportDiscovery, + DashboardStatus, +) + +from ..core import DASHBOARD, list_dashboard_entries + + +class MDNSStatus: + """Class that updates the mdns status.""" + + def __init__(self) -> None: + """Initialize the MDNSStatus class.""" + super().__init__() + self.aiozc: AsyncEsphomeZeroconf | None = None + # This is the current mdns state for each host (True, False, None) + self.host_mdns_state: dict[str, bool | None] = {} + # This is the hostnames to filenames mapping + self.host_name_to_filename: dict[str, str] = {} + self.filename_to_host_name: dict[str, str] = {} + # This is a set of host names to track (i.e no_mdns = false) + self.host_name_with_mdns_enabled: set[set] = set() + self._loop = asyncio.get_running_loop() + + def filename_to_host_name_thread_safe(self, filename: str) -> str | None: + """Resolve a filename to an address in a thread-safe manner.""" + return self.filename_to_host_name.get(filename) + + async def async_resolve_host(self, host_name: str) -> str | None: + """Resolve a host name to an address in a thread-safe manner.""" + if aiozc := self.aiozc: + return await aiozc.async_resolve_host(host_name) + return None + + async def async_refresh_hosts(self): + """Refresh the hosts to track.""" + entries = await self._loop.run_in_executor(None, list_dashboard_entries) + host_name_with_mdns_enabled = self.host_name_with_mdns_enabled + host_mdns_state = self.host_mdns_state + host_name_to_filename = self.host_name_to_filename + filename_to_host_name = self.filename_to_host_name + ping_result = DASHBOARD.ping_result + + for entry in entries: + name = entry.name + # If no_mdns is set, remove it from the set + if entry.no_mdns: + host_name_with_mdns_enabled.discard(name) + continue + + # We are tracking this host + host_name_with_mdns_enabled.add(name) + filename = entry.filename + + # If we just adopted/imported this host, we likely + # already have a state for it, so we should make sure + # to set it so the dashboard shows it as online + if name in host_mdns_state: + ping_result[filename] = host_mdns_state[name] + + # Make sure the mapping is up to date + # so when we get an mdns update we can map it back + # to the filename + host_name_to_filename[name] = filename + filename_to_host_name[filename] = name + + async def async_run(self) -> None: + dashboard = DASHBOARD + + aiozc = AsyncEsphomeZeroconf() + self.aiozc = aiozc + host_mdns_state = self.host_mdns_state + host_name_to_filename = self.host_name_to_filename + host_name_with_mdns_enabled = self.host_name_with_mdns_enabled + ping_result = dashboard.ping_result + + def on_update(dat: dict[str, bool | None]) -> None: + """Update the global PING_RESULT dict.""" + for name, result in dat.items(): + host_mdns_state[name] = result + if name in host_name_with_mdns_enabled: + filename = host_name_to_filename[name] + ping_result[filename] = result + + stat = DashboardStatus(on_update) + imports = DashboardImportDiscovery() + dashboard.import_result = imports.import_state + + browser = DashboardBrowser( + aiozc.zeroconf, + ESPHOME_SERVICE_TYPE, + [stat.browser_callback, imports.browser_callback], + ) + + while not dashboard.stop_event.is_set(): + await self.async_refresh_hosts() + await dashboard.ping_request.wait() + dashboard.ping_request.clear() + + await browser.async_cancel() + await aiozc.async_close() + self.aiozc = None diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py new file mode 100644 index 0000000000..109b60e133 --- /dev/null +++ b/esphome/dashboard/status/mqtt.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import binascii +import json +import os +import threading + +from esphome import mqtt + +from ..core import DASHBOARD, list_dashboard_entries + + +class MqttStatusThread(threading.Thread): + """Status thread to get the status of the devices via MQTT.""" + + def run(self) -> None: + """Run the status thread.""" + dashboard = DASHBOARD + entries = list_dashboard_entries() + + config = mqtt.config_from_env() + topic = "esphome/discover/#" + + def on_message(client, userdata, msg): + nonlocal entries + + payload = msg.payload.decode(errors="backslashreplace") + if len(payload) > 0: + data = json.loads(payload) + if "name" not in data: + return + for entry in entries: + if entry.name == data["name"]: + dashboard.ping_result[entry.filename] = True + return + + def on_connect(client, userdata, flags, return_code): + client.publish("esphome/discover", None, retain=False) + + mqttid = str(binascii.hexlify(os.urandom(6)).decode()) + + client = mqtt.prepare( + config, + [topic], + on_message, + on_connect, + None, + None, + f"esphome-dashboard-{mqttid}", + ) + client.loop_start() + + while not dashboard.stop_event.wait(2): + # update entries + entries = list_dashboard_entries() + + # will be set to true on on_message + for entry in entries: + if entry.no_mdns: + dashboard.ping_result[entry.filename] = False + + client.publish("esphome/discover", None, retain=False) + dashboard.mqtt_ping_request.wait() + dashboard.mqtt_ping_request.clear() + + client.disconnect() + client.loop_stop() diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py new file mode 100644 index 0000000000..17c1254c9d --- /dev/null +++ b/esphome/dashboard/status/ping.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import asyncio +import os +from typing import cast + +from ..core import DASHBOARD +from ..entries import DashboardEntry +from ..core import list_dashboard_entries +from ..util import chunked + + +async def _async_ping_host(host: str) -> bool: + """Ping a host.""" + ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"] + process = await asyncio.create_subprocess_exec( + *ping_command, + host, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + close_fds=False, + ) + await process.wait() + return process.returncode == 0 + + +class PingStatus: + def __init__(self) -> None: + """Initialize the PingStatus class.""" + super().__init__() + self._loop = asyncio.get_running_loop() + + async def async_run(self) -> None: + """Run the ping status.""" + dashboard = DASHBOARD + + while not dashboard.stop_event.is_set(): + # Only ping if the dashboard is open + await dashboard.ping_request.wait() + dashboard.ping_result.clear() + entries = await self._loop.run_in_executor(None, list_dashboard_entries) + to_ping: list[DashboardEntry] = [ + entry for entry in entries if entry.address is not None + ] + for ping_group in chunked(to_ping, 16): + ping_group = cast(list[DashboardEntry], ping_group) + results = await asyncio.gather( + *(_async_ping_host(entry.address) for entry in ping_group), + return_exceptions=True, + ) + for entry, result in zip(ping_group, results): + if isinstance(result, Exception): + result = False + elif isinstance(result, BaseException): + raise result + dashboard.ping_result[entry.filename] = result From 4ce627b4ee67ee4633c6ca7bd240bfa521d43cbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:28:04 -0600 Subject: [PATCH 213/270] Bump aioesphomeapi from 18.4.0 to 18.4.1 (#5767) 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 3140d0a8a3..abe632bc6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.0 +aioesphomeapi==18.4.1 zeroconf==0.126.0 # esp-idf requires this, but doesn't bundle it by default From c795dbde26691f8ef45b343d131de9db68420d8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 15:34:09 -0600 Subject: [PATCH 214/270] dashboard: split dashboard web server code into its own module (#5770) --- esphome/__main__.py | 2 +- esphome/dashboard/dashboard.py | 1078 +------------------------------ esphome/dashboard/settings.py | 10 +- esphome/dashboard/web_server.py | 1069 ++++++++++++++++++++++++++++++ 4 files changed, 1091 insertions(+), 1068 deletions(-) create mode 100644 esphome/dashboard/web_server.py diff --git a/esphome/__main__.py b/esphome/__main__.py index a253fc78a0..e5456cf8e5 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -514,7 +514,7 @@ def command_clean(args, config): def command_dashboard(args): from esphome.dashboard import dashboard - return dashboard.start_web_server(args) + return dashboard.start_dashboard(args) def command_update_all(args): diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 4a417fe190..789b14653c 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,1054 +1,21 @@ from __future__ import annotations import asyncio -import base64 -import datetime -import functools -import gzip -import hashlib -import json -import logging import os -import secrets -import shutil -import subprocess -import threading -from pathlib import Path -from typing import Any +import socket -import tornado -import tornado.concurrent -import tornado.gen -import tornado.httpserver -import tornado.ioloop -import tornado.iostream -import tornado.netutil -import tornado.process -import tornado.queues -import tornado.web -import tornado.websocket -import yaml -from tornado.log import access_log +from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path -from esphome import const, platformio_api, yaml_util -from esphome.helpers import get_bool_env, mkdir_p, run_system_command -from esphome.storage_json import ( - EsphomeStorageJSON, - StorageJSON, - esphome_storage_path, - ext_storage_path, - trash_storage_path, -) -from esphome.util import get_serial_ports, shlex_quote - -from .core import DASHBOARD, list_dashboard_entries -from .entries import DashboardEntry -from .util import friendly_name_slugify - -_LOGGER = logging.getLogger(__name__) +from .core import DASHBOARD +from .web_server import make_app, start_web_server ENV_DEV = "ESPHOME_DASHBOARD_DEV" - -cookie_authenticated_yes = b"yes" - - settings = DASHBOARD.settings -def template_args(): - version = const.__version__ - if "b" in version: - docs_link = "https://beta.esphome.io/" - elif "dev" in version: - docs_link = "https://next.esphome.io/" - else: - docs_link = "https://www.esphome.io/" - - return { - "version": version, - "docs_link": docs_link, - "get_static_file_url": get_static_file_url, - "relative_url": settings.relative_url, - "streamer_mode": settings.streamer_mode, - "config_dir": settings.config_dir, - } - - -def authenticated(func): - @functools.wraps(func) - def decorator(self, *args, **kwargs): - if not is_authenticated(self): - self.redirect("./login") - return None - return func(self, *args, **kwargs) - - return decorator - - -def is_authenticated(request_handler): - if settings.on_ha_addon: - # Handle ingress - disable auth on ingress port - # X-HA-Ingress is automatically stripped on the non-ingress server in nginx - header = request_handler.request.headers.get("X-HA-Ingress", "NO") - if str(header) == "YES": - return True - if settings.using_auth: - return ( - request_handler.get_secure_cookie("authenticated") - == cookie_authenticated_yes - ) - return True - - -def bind_config(func): - def decorator(self, *args, **kwargs): - configuration = self.get_argument("configuration") - kwargs = kwargs.copy() - kwargs["configuration"] = configuration - return func(self, *args, **kwargs) - - return decorator - - -# pylint: disable=abstract-method -class BaseHandler(tornado.web.RequestHandler): - pass - - -def websocket_class(cls): - # pylint: disable=protected-access - if not hasattr(cls, "_message_handlers"): - cls._message_handlers = {} - - for _, method in cls.__dict__.items(): - if hasattr(method, "_message_handler"): - cls._message_handlers[method._message_handler] = method - - return cls - - -def websocket_method(name): - def wrap(fn): - # pylint: disable=protected-access - fn._message_handler = name - return fn - - return wrap - - -@websocket_class -class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): - def __init__(self, application, request, **kwargs): - super().__init__(application, request, **kwargs) - self._proc = None - self._queue = None - self._is_closed = False - # Windows doesn't support non-blocking pipes, - # use Popen() with a reading thread instead - self._use_popen = os.name == "nt" - - @authenticated - async def on_message( # pylint: disable=invalid-overridden-method - self, message: str - ) -> None: - # Since tornado 4.5, on_message is allowed to be a coroutine - # Messages are always JSON, 500 when not - json_message = json.loads(message) - type_ = json_message["type"] - # pylint: disable=no-member - handlers = type(self)._message_handlers - if type_ not in handlers: - _LOGGER.warning("Requested unknown message type %s", type_) - return - - await handlers[type_](self, json_message) - - @websocket_method("spawn") - async def handle_spawn(self, json_message: dict[str, Any]) -> None: - if self._proc is not None: - # spawn can only be called once - return - command = await self.build_command(json_message) - _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) - - if self._use_popen: - self._queue = tornado.queues.Queue() - # pylint: disable=consider-using-with - self._proc = subprocess.Popen( - command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - stdout_thread = threading.Thread(target=self._stdout_thread) - stdout_thread.daemon = True - stdout_thread.start() - else: - self._proc = tornado.process.Subprocess( - command, - stdout=tornado.process.Subprocess.STREAM, - stderr=subprocess.STDOUT, - stdin=tornado.process.Subprocess.STREAM, - ) - self._proc.set_exit_callback(self._proc_on_exit) - - tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) - - @property - def is_process_active(self): - return self._proc is not None and self._proc.returncode is None - - @websocket_method("stdin") - async def handle_stdin(self, json_message: dict[str, Any]) -> None: - if not self.is_process_active: - return - text: str = json_message["data"] - data = text.encode("utf-8", "replace") - _LOGGER.debug("< stdin: %s", data) - self._proc.stdin.write(data) - - @tornado.gen.coroutine - def _redirect_stdout(self) -> None: - reg = b"[\n\r]" - - while True: - try: - if self._use_popen: - data: bytes = yield self._queue.get() - if data is None: - self._proc_on_exit(self._proc.poll()) - break - else: - data: bytes = yield self._proc.stdout.read_until_regex(reg) - except tornado.iostream.StreamClosedError: - break - - text = data.decode("utf-8", "replace") - _LOGGER.debug("> stdout: %s", text) - self.write_message({"event": "line", "data": text}) - - def _stdout_thread(self) -> None: - if not self._use_popen: - return - while True: - data = self._proc.stdout.readline() - if data: - data = data.replace(b"\r", b"") - self._queue.put_nowait(data) - if self._proc.poll() is not None: - break - self._proc.wait(1.0) - self._queue.put_nowait(None) - - def _proc_on_exit(self, returncode: int) -> None: - if not self._is_closed: - # Check if the proc was not forcibly closed - _LOGGER.info("Process exited with return code %s", returncode) - self.write_message({"event": "exit", "code": returncode}) - - def on_close(self) -> None: - # Check if proc exists (if 'start' has been run) - if self.is_process_active: - _LOGGER.debug("Terminating process") - if self._use_popen: - self._proc.terminate() - else: - self._proc.proc.terminate() - # Shutdown proc on WS close - self._is_closed = True - - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - raise NotImplementedError - - -DASHBOARD_COMMAND = ["esphome", "--dashboard"] - - -class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): - """Base class for commands that require a port.""" - - async def build_device_command( - self, args: list[str], json_message: dict[str, Any] - ) -> list[str]: - """Build the command to run.""" - dashboard = DASHBOARD - configuration = json_message["configuration"] - config_file = settings.rel_path(configuration) - port = json_message["port"] - if ( - port == "OTA" - and (mdns := dashboard.mdns_status) - and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) - and (address := await mdns.async_resolve_host(host_name)) - ): - port = address - - return [ - *DASHBOARD_COMMAND, - *args, - config_file, - "--device", - port, - ] - - -class EsphomeLogsHandler(EsphomePortCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - """Build the command to run.""" - return await self.build_device_command(["logs"], json_message) - - -class EsphomeRenameHandler(EsphomeCommandWebSocket): - old_name: str - - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - self.old_name = json_message["configuration"] - return [ - *DASHBOARD_COMMAND, - "rename", - config_file, - json_message["newName"], - ] - - def _proc_on_exit(self, returncode): - super()._proc_on_exit(returncode) - - if returncode != 0: - return - - # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(self.old_name, None) - - -class EsphomeUploadHandler(EsphomePortCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - """Build the command to run.""" - return await self.build_device_command(["upload"], json_message) - - -class EsphomeRunHandler(EsphomePortCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - """Build the command to run.""" - return await self.build_device_command(["run"], json_message) - - -class EsphomeCompileHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - command = [*DASHBOARD_COMMAND, "compile"] - if json_message.get("only_generate", False): - command.append("--only-generate") - command.append(config_file) - return command - - -class EsphomeValidateHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - command = [*DASHBOARD_COMMAND, "config", config_file] - if not settings.streamer_mode: - command.append("--show-secrets") - return command - - -class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] - - -class EsphomeCleanHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - config_file = settings.rel_path(json_message["configuration"]) - return [*DASHBOARD_COMMAND, "clean", config_file] - - -class EsphomeVscodeHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"] - - -class EsphomeAceEditorHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir] - - -class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): - async def build_command(self, json_message: dict[str, Any]) -> list[str]: - return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] - - -class SerialPortRequestHandler(BaseHandler): - @authenticated - async def get(self): - ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) - data = [] - for port in ports: - desc = port.description - if port.path == "/dev/ttyAMA0": - desc = "UART pins on GPIO header" - split_desc = desc.split(" - ") - if len(split_desc) == 2 and split_desc[0] == split_desc[1]: - # Some serial ports repeat their values - desc = split_desc[0] - data.append({"port": port.path, "desc": desc}) - data.append({"port": "OTA", "desc": "Over-The-Air"}) - data.sort(key=lambda x: x["port"], reverse=True) - self.set_header("content-type", "application/json") - self.write(json.dumps(data)) - - -class WizardRequestHandler(BaseHandler): - @authenticated - def post(self): - from esphome import wizard - - kwargs = { - k: v - for k, v in json.loads(self.request.body.decode()).items() - if k in ("name", "platform", "board", "ssid", "psk", "password") - } - if not kwargs["name"]: - self.set_status(422) - self.set_header("content-type", "application/json") - self.write(json.dumps({"error": "Name is required"})) - return - - kwargs["friendly_name"] = kwargs["name"] - kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) - - kwargs["ota_password"] = secrets.token_hex(16) - noise_psk = secrets.token_bytes(32) - kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() - filename = f"{kwargs['name']}.yaml" - destination = settings.rel_path(filename) - wizard.wizard_write(path=destination, **kwargs) - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": filename})) - self.finish() - - -class ImportRequestHandler(BaseHandler): - @authenticated - def post(self): - from esphome.components.dashboard_import import import_config - - dashboard = DASHBOARD - args = json.loads(self.request.body.decode()) - try: - name = args["name"] - friendly_name = args.get("friendly_name") - encryption = args.get("encryption", False) - - imported_device = next( - ( - res - for res in dashboard.import_result.values() - if res.device_name == name - ), - None, - ) - - if imported_device is not None: - network = imported_device.network - if friendly_name is None: - friendly_name = imported_device.friendly_name - else: - network = const.CONF_WIFI - - import_config( - settings.rel_path(f"{name}.yaml"), - name, - friendly_name, - args["project_name"], - args["package_import_url"], - network, - encryption, - ) - # Make sure the device gets marked online right away - dashboard.ping_request.set() - except FileExistsError: - self.set_status(500) - self.write("File already exists") - return - except ValueError: - self.set_status(422) - self.write("Invalid package url") - return - - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": f"{name}.yaml"})) - self.finish() - - -class DownloadListRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - storage_path = ext_storage_path(configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return - - from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS - from esphome.components.esp32 import get_download_types as esp32_types - from esphome.components.esp8266 import get_download_types as esp8266_types - from esphome.components.libretiny import get_download_types as libretiny_types - from esphome.components.rp2040 import get_download_types as rp2040_types - - downloads = [] - platform = storage_json.target_platform.lower() - if platform == const.PLATFORM_RP2040: - downloads = rp2040_types(storage_json) - elif platform == const.PLATFORM_ESP8266: - downloads = esp8266_types(storage_json) - elif platform.upper() in ESP32_VARIANTS: - downloads = esp32_types(storage_json) - elif platform == const.PLATFORM_BK72XX: - downloads = libretiny_types(storage_json) - elif platform == const.PLATFORM_RTL87XX: - downloads = libretiny_types(storage_json) - else: - self.send_error(418) - return - - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps(downloads)) - self.finish() - return - - -class DownloadBinaryRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - compressed = self.get_argument("compressed", "0") == "1" - - storage_path = ext_storage_path(configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error(404) - return - - # fallback to type=, but prioritize file= - file_name = self.get_argument("type", None) - file_name = self.get_argument("file", file_name) - if file_name is None: - self.send_error(400) - return - file_name = file_name.replace("..", "").lstrip("/") - # get requested download name, or build it based on filename - download_name = self.get_argument( - "download", - f"{storage_json.name}-{file_name}", - ) - path = os.path.dirname(storage_json.firmware_bin_path) - path = os.path.join(path, file_name) - - if not Path(path).is_file(): - args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(404 if rc == 2 else 500) - return - - idedata = platformio_api.IDEData(json.loads(stdout)) - - found = False - for image in idedata.extra_flash_images: - if image.path.endswith(file_name): - path = image.path - download_name = file_name - found = True - break - - if not found: - self.send_error(404) - return - - download_name = download_name + ".gz" if compressed else download_name - - self.set_header("Content-Type", "application/octet-stream") - self.set_header( - "Content-Disposition", f'attachment; filename="{download_name}"' - ) - self.set_header("Cache-Control", "no-cache") - if not Path(path).is_file(): - self.send_error(404) - return - - with open(path, "rb") as f: - data = f.read() - if compressed: - data = gzip.compress(data, 9) - self.write(data) - - self.finish() - - -class EsphomeVersionHandler(BaseHandler): - @authenticated - def get(self): - self.set_header("Content-Type", "application/json") - self.write(json.dumps({"version": const.__version__})) - self.finish() - - -class ListDevicesHandler(BaseHandler): - @authenticated - async def get(self): - loop = asyncio.get_running_loop() - entries = await loop.run_in_executor(None, list_dashboard_entries) - self.set_header("content-type", "application/json") - configured = {entry.name for entry in entries} - dashboard = DASHBOARD - - self.write( - json.dumps( - { - "configured": [ - { - "name": entry.name, - "friendly_name": entry.friendly_name, - "configuration": entry.filename, - "loaded_integrations": entry.loaded_integrations, - "deployed_version": entry.update_old, - "current_version": entry.update_new, - "path": entry.path, - "comment": entry.comment, - "address": entry.address, - "web_port": entry.web_port, - "target_platform": entry.target_platform, - } - for entry in entries - ], - "importable": [ - { - "name": res.device_name, - "friendly_name": res.friendly_name, - "package_import_url": res.package_import_url, - "project_name": res.project_name, - "project_version": res.project_version, - "network": res.network, - } - for res in dashboard.import_result.values() - if res.device_name not in configured - ], - } - ) - ) - - -class MainRequestHandler(BaseHandler): - @authenticated - def get(self): - begin = bool(self.get_argument("begin", False)) - - self.render( - "index.template.html", - begin=begin, - **template_args(), - login_enabled=settings.using_password, - ) - - -class PrometheusServiceDiscoveryHandler(BaseHandler): - @authenticated - def get(self): - entries = list_dashboard_entries() - self.set_header("content-type", "application/json") - sd = [] - for entry in entries: - if entry.web_port is None: - continue - labels = { - "__meta_name": entry.name, - "__meta_esp_platform": entry.target_platform, - "__meta_esphome_version": entry.storage.esphome_version, - } - for integration in entry.storage.loaded_integrations: - labels[f"__meta_integration_{integration}"] = "true" - sd.append( - { - "targets": [ - f"{entry.address}:{entry.web_port}", - ], - "labels": labels, - } - ) - self.write(json.dumps(sd)) - - -class BoardsRequestHandler(BaseHandler): - @authenticated - def get(self, platform: str): - from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS - from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS - from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS - from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS - from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS - - platform_to_boards = { - const.PLATFORM_ESP32: ESP32_BOARDS, - const.PLATFORM_ESP8266: ESP8266_BOARDS, - const.PLATFORM_RP2040: RP2040_BOARDS, - const.PLATFORM_BK72XX: BK72XX_BOARDS, - const.PLATFORM_RTL87XX: RTL87XX_BOARDS, - } - # filter all ESP32 variants by requested platform - if platform.startswith("esp32"): - boards = { - k: v - for k, v in platform_to_boards[const.PLATFORM_ESP32].items() - if v[const.KEY_VARIANT] == platform.upper() - } - else: - boards = platform_to_boards[platform] - - # map to a {board_name: board_title} dict - platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} - # sort by board title - boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) - output = [{"items": dict(boards_items)}] - - self.set_header("content-type", "application/json") - self.write(json.dumps(output)) - - -class PingRequestHandler(BaseHandler): - @authenticated - def get(self): - dashboard = DASHBOARD - dashboard.ping_request.set() - if settings.status_use_mqtt: - dashboard.mqtt_ping_request.set() - self.set_header("content-type", "application/json") - self.write(json.dumps(dashboard.ping_result)) - - -class InfoRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - yaml_path = settings.rel_path(configuration) - all_yaml_files = settings.list_yaml_files() - - if yaml_path not in all_yaml_files: - self.set_status(404) - return - - self.set_header("content-type", "application/json") - self.write(DashboardEntry(yaml_path).storage.to_json()) - - -class EditRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - filename = settings.rel_path(configuration) - content = "" - if os.path.isfile(filename): - with open(file=filename, encoding="utf-8") as f: - content = f.read() - self.write(content) - - @authenticated - @bind_config - def post(self, configuration=None): - with open(file=settings.rel_path(configuration), mode="wb") as f: - f.write(self.request.body) - self.set_status(200) - - -class DeleteRequestHandler(BaseHandler): - @authenticated - @bind_config - def post(self, configuration=None): - config_file = settings.rel_path(configuration) - storage_path = ext_storage_path(configuration) - - trash_path = trash_storage_path() - mkdir_p(trash_path) - shutil.move(config_file, os.path.join(trash_path, configuration)) - - storage_json = StorageJSON.load(storage_path) - if storage_json is not None: - # Delete build folder (if exists) - name = storage_json.name - build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(trash_path, name)) - - # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(configuration, None) - - -class UndoDeleteRequestHandler(BaseHandler): - @authenticated - @bind_config - def post(self, configuration=None): - config_file = settings.rel_path(configuration) - trash_path = trash_storage_path() - shutil.move(os.path.join(trash_path, configuration), config_file) - - -class LoginHandler(BaseHandler): - def get(self): - if is_authenticated(self): - self.redirect("./") - else: - self.render_login_page() - - def render_login_page(self, error=None): - self.render( - "login.template.html", - error=error, - ha_addon=settings.using_ha_addon_auth, - has_username=bool(settings.username), - **template_args(), - ) - - def post_ha_addon_login(self): - import requests - - headers = { - "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), - } - - data = { - "username": self.get_argument("username", ""), - "password": self.get_argument("password", ""), - } - try: - req = requests.post( - "http://supervisor/auth", headers=headers, json=data, timeout=30 - ) - if req.status_code == 200: - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.warning("Error during Hass.io auth request: %s", err) - self.set_status(500) - self.render_login_page(error="Internal server error") - return - self.set_status(401) - self.render_login_page(error="Invalid username or password") - - def post_native_login(self): - username = self.get_argument("username", "") - password = self.get_argument("password", "") - if settings.check_password(username, password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("./") - return - error_str = ( - "Invalid username or password" if settings.username else "Invalid password" - ) - self.set_status(401) - self.render_login_page(error=error_str) - - def post(self): - if settings.using_ha_addon_auth: - self.post_ha_addon_login() - else: - self.post_native_login() - - -class LogoutHandler(BaseHandler): - @authenticated - def get(self): - self.clear_cookie("authenticated") - self.redirect("./login") - - -class SecretKeysRequestHandler(BaseHandler): - @authenticated - def get(self): - filename = None - - for secret_filename in const.SECRETS_FILES: - relative_filename = settings.rel_path(secret_filename) - if os.path.isfile(relative_filename): - filename = relative_filename - break - - if filename is None: - self.send_error(404) - return - - secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False)) - - self.set_header("content-type", "application/json") - self.write(json.dumps(secret_keys)) - - -class SafeLoaderIgnoreUnknown(yaml.SafeLoader): - def ignore_unknown(self, node): - return f"{node.tag} {node.value}" - - def construct_yaml_binary(self, node) -> str: - return super().construct_yaml_binary(node).decode("ascii") - - -SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) -SafeLoaderIgnoreUnknown.add_constructor( - "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary -) - - -class JsonConfigRequestHandler(BaseHandler): - @authenticated - @bind_config - def get(self, configuration=None): - filename = settings.rel_path(configuration) - if not os.path.isfile(filename): - self.send_error(404) - return - - args = ["esphome", "config", filename, "--show-secrets"] - - rc, stdout, _ = run_system_command(*args) - - if rc != 0: - self.send_error(422) - return - - data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) - self.set_header("content-type", "application/json") - self.write(json.dumps(data)) - self.finish() - - -def get_base_frontend_path(): - if ENV_DEV not in os.environ: - import esphome_dashboard - - return esphome_dashboard.where() - - static_path = os.environ[ENV_DEV] - if not static_path.endswith("/"): - static_path += "/" - - # This path can be relative, so resolve against the root or else templates don't work - return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) - - -def get_static_path(*args): - return os.path.join(get_base_frontend_path(), "static", *args) - - -@functools.cache -def get_static_file_url(name): - base = f"./static/{name}" - - if ENV_DEV in os.environ: - return base - - # Module imports can't deduplicate if stuff added to url - if name == "js/esphome/index.js": - import esphome_dashboard - - return base.replace("index.js", esphome_dashboard.entrypoint()) - - path = get_static_path(name) - with open(path, "rb") as f_handle: - hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] - return f"{base}?hash={hash_}" - - -def make_app(debug=get_bool_env(ENV_DEV)): - def log_function(handler): - if handler.get_status() < 400: - log_method = access_log.info - - if isinstance(handler, SerialPortRequestHandler) and not debug: - return - if isinstance(handler, PingRequestHandler) and not debug: - return - elif handler.get_status() < 500: - log_method = access_log.warning - else: - log_method = access_log.error - - request_time = 1000.0 * handler.request.request_time() - # pylint: disable=protected-access - log_method( - "%d %s %.2fms", - handler.get_status(), - handler._request_summary(), - request_time, - ) - - class StaticFileHandler(tornado.web.StaticFileHandler): - def get_cache_time( - self, path: str, modified: datetime.datetime | None, mime_type: str - ) -> int: - """Override to customize cache control behavior.""" - if debug: - return 0 - # Assets that are hashed have ?hash= in the URL, all javascript - # filenames hashed so we can cache them for a long time - if "hash" in self.request.arguments or "/javascript" in mime_type: - return self.CACHE_MAX_AGE - return super().get_cache_time(path, modified, mime_type) - - app_settings = { - "debug": debug, - "cookie_secret": settings.cookie_secret, - "log_function": log_function, - "websocket_ping_interval": 30.0, - "template_path": get_base_frontend_path(), - } - rel = settings.relative_url - app = tornado.web.Application( - [ - (f"{rel}", MainRequestHandler), - (f"{rel}login", LoginHandler), - (f"{rel}logout", LogoutHandler), - (f"{rel}logs", EsphomeLogsHandler), - (f"{rel}upload", EsphomeUploadHandler), - (f"{rel}run", EsphomeRunHandler), - (f"{rel}compile", EsphomeCompileHandler), - (f"{rel}validate", EsphomeValidateHandler), - (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), - (f"{rel}clean", EsphomeCleanHandler), - (f"{rel}vscode", EsphomeVscodeHandler), - (f"{rel}ace", EsphomeAceEditorHandler), - (f"{rel}update-all", EsphomeUpdateAllHandler), - (f"{rel}info", InfoRequestHandler), - (f"{rel}edit", EditRequestHandler), - (f"{rel}downloads", DownloadListRequestHandler), - (f"{rel}download.bin", DownloadBinaryRequestHandler), - (f"{rel}serial-ports", SerialPortRequestHandler), - (f"{rel}ping", PingRequestHandler), - (f"{rel}delete", DeleteRequestHandler), - (f"{rel}undo-delete", UndoDeleteRequestHandler), - (f"{rel}wizard", WizardRequestHandler), - (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), - (f"{rel}devices", ListDevicesHandler), - (f"{rel}import", ImportRequestHandler), - (f"{rel}secret_keys", SecretKeysRequestHandler), - (f"{rel}json-config", JsonConfigRequestHandler), - (f"{rel}rename", EsphomeRenameHandler), - (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), - (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), - (f"{rel}version", EsphomeVersionHandler), - ], - **app_settings, - ) - - return app - - -def start_web_server(args): +def start_dashboard(args) -> None: + """Start the dashboard.""" settings.parse_args(args) if settings.using_auth: @@ -1066,35 +33,22 @@ def start_web_server(args): async def async_start(args) -> None: + """Start the dashboard.""" dashboard = DASHBOARD await dashboard.async_setup() + sock: socket.socket | None = args.socket + address: str | None = args.address + port: int | None = args.port - app = make_app(args.verbose) - if args.socket is not None: - _LOGGER.info( - "Starting dashboard web server on unix socket %s and configuration dir %s...", - args.socket, - settings.config_dir, - ) - server = tornado.httpserver.HTTPServer(app) - socket = tornado.netutil.bind_unix_socket(args.socket, mode=0o666) - server.add_socket(socket) - else: - _LOGGER.info( - "Starting dashboard web server on http://%s:%s and configuration dir %s...", - args.address, - args.port, - settings.config_dir, - ) - app.listen(args.port, args.address) + start_web_server(make_app(args.verbose), sock, address, port, settings.config_dir) - if args.open_ui: - import webbrowser + if args.open_ui: + import webbrowser - webbrowser.open(f"http://{args.address}:{args.port}") + webbrowser.open(f"http://{args.address}:{args.port}") try: await dashboard.async_run() finally: - if args.socket is not None: - os.remove(args.socket) + if sock: + os.remove(sock) diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 3409938e0a..1ddb6f652d 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -29,15 +29,15 @@ class DashboardSettings: ] = {} def parse_args(self, args): - self.on_ha_addon = args.ha_addon - password = args.password or os.getenv("PASSWORD", "") + self.on_ha_addon: bool = args.ha_addon + password: str = args.password or os.getenv("PASSWORD", "") if not self.on_ha_addon: - self.username = args.username or os.getenv("USERNAME", "") + self.username: str = args.username or os.getenv("USERNAME", "") self.using_password = bool(password) if self.using_password: self.password_hash = password_hash(password) - self.config_dir = args.configuration - self.absolute_config_dir = Path(self.config_dir).resolve() + self.config_dir: str = args.configuration + self.absolute_config_dir: Path = Path(self.config_dir).resolve() CORE.config_path = os.path.join(self.config_dir, ".") @property diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py new file mode 100644 index 0000000000..086a28cbb2 --- /dev/null +++ b/esphome/dashboard/web_server.py @@ -0,0 +1,1069 @@ +from __future__ import annotations + +import asyncio +import base64 +import datetime +import functools +import gzip +import hashlib +import json +import logging +import os +import secrets +import shutil +import subprocess +import threading +from pathlib import Path +from typing import Any + +import tornado +import tornado.concurrent +import tornado.gen +import tornado.httpserver +import tornado.ioloop +import tornado.iostream +import tornado.netutil +import tornado.process +import tornado.queues +import tornado.web +import tornado.websocket +import yaml +from tornado.log import access_log + +from esphome import const, platformio_api, yaml_util +from esphome.helpers import get_bool_env, mkdir_p, run_system_command +from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path +from esphome.util import get_serial_ports, shlex_quote + +from .core import DASHBOARD, list_dashboard_entries +from .entries import DashboardEntry +from .util import friendly_name_slugify + +_LOGGER = logging.getLogger(__name__) + +ENV_DEV = "ESPHOME_DASHBOARD_DEV" + + +cookie_authenticated_yes = b"yes" + + +settings = DASHBOARD.settings + + +def template_args(): + version = const.__version__ + if "b" in version: + docs_link = "https://beta.esphome.io/" + elif "dev" in version: + docs_link = "https://next.esphome.io/" + else: + docs_link = "https://www.esphome.io/" + + return { + "version": version, + "docs_link": docs_link, + "get_static_file_url": get_static_file_url, + "relative_url": settings.relative_url, + "streamer_mode": settings.streamer_mode, + "config_dir": settings.config_dir, + } + + +def authenticated(func): + @functools.wraps(func) + def decorator(self, *args, **kwargs): + if not is_authenticated(self): + self.redirect("./login") + return None + return func(self, *args, **kwargs) + + return decorator + + +def is_authenticated(request_handler): + if settings.on_ha_addon: + # Handle ingress - disable auth on ingress port + # X-HA-Ingress is automatically stripped on the non-ingress server in nginx + header = request_handler.request.headers.get("X-HA-Ingress", "NO") + if str(header) == "YES": + return True + if settings.using_auth: + return ( + request_handler.get_secure_cookie("authenticated") + == cookie_authenticated_yes + ) + return True + + +def bind_config(func): + def decorator(self, *args, **kwargs): + configuration = self.get_argument("configuration") + kwargs = kwargs.copy() + kwargs["configuration"] = configuration + return func(self, *args, **kwargs) + + return decorator + + +# pylint: disable=abstract-method +class BaseHandler(tornado.web.RequestHandler): + pass + + +def websocket_class(cls): + # pylint: disable=protected-access + if not hasattr(cls, "_message_handlers"): + cls._message_handlers = {} + + for _, method in cls.__dict__.items(): + if hasattr(method, "_message_handler"): + cls._message_handlers[method._message_handler] = method + + return cls + + +def websocket_method(name): + def wrap(fn): + # pylint: disable=protected-access + fn._message_handler = name + return fn + + return wrap + + +@websocket_class +class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): + def __init__(self, application, request, **kwargs): + super().__init__(application, request, **kwargs) + self._proc = None + self._queue = None + self._is_closed = False + # Windows doesn't support non-blocking pipes, + # use Popen() with a reading thread instead + self._use_popen = os.name == "nt" + + @authenticated + async def on_message( # pylint: disable=invalid-overridden-method + self, message: str + ) -> None: + # Since tornado 4.5, on_message is allowed to be a coroutine + # Messages are always JSON, 500 when not + json_message = json.loads(message) + type_ = json_message["type"] + # pylint: disable=no-member + handlers = type(self)._message_handlers + if type_ not in handlers: + _LOGGER.warning("Requested unknown message type %s", type_) + return + + await handlers[type_](self, json_message) + + @websocket_method("spawn") + async def handle_spawn(self, json_message: dict[str, Any]) -> None: + if self._proc is not None: + # spawn can only be called once + return + command = await self.build_command(json_message) + _LOGGER.info("Running command '%s'", " ".join(shlex_quote(x) for x in command)) + + if self._use_popen: + self._queue = tornado.queues.Queue() + # pylint: disable=consider-using-with + self._proc = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + stdout_thread = threading.Thread(target=self._stdout_thread) + stdout_thread.daemon = True + stdout_thread.start() + else: + self._proc = tornado.process.Subprocess( + command, + stdout=tornado.process.Subprocess.STREAM, + stderr=subprocess.STDOUT, + stdin=tornado.process.Subprocess.STREAM, + close_fds=False, + ) + self._proc.set_exit_callback(self._proc_on_exit) + + tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) + + @property + def is_process_active(self): + return self._proc is not None and self._proc.returncode is None + + @websocket_method("stdin") + async def handle_stdin(self, json_message: dict[str, Any]) -> None: + if not self.is_process_active: + return + text: str = json_message["data"] + data = text.encode("utf-8", "replace") + _LOGGER.debug("< stdin: %s", data) + self._proc.stdin.write(data) + + @tornado.gen.coroutine + def _redirect_stdout(self) -> None: + reg = b"[\n\r]" + + while True: + try: + if self._use_popen: + data: bytes = yield self._queue.get() + if data is None: + self._proc_on_exit(self._proc.poll()) + break + else: + data: bytes = yield self._proc.stdout.read_until_regex(reg) + except tornado.iostream.StreamClosedError: + break + + text = data.decode("utf-8", "replace") + _LOGGER.debug("> stdout: %s", text) + self.write_message({"event": "line", "data": text}) + + def _stdout_thread(self) -> None: + if not self._use_popen: + return + while True: + data = self._proc.stdout.readline() + if data: + data = data.replace(b"\r", b"") + self._queue.put_nowait(data) + if self._proc.poll() is not None: + break + self._proc.wait(1.0) + self._queue.put_nowait(None) + + def _proc_on_exit(self, returncode: int) -> None: + if not self._is_closed: + # Check if the proc was not forcibly closed + _LOGGER.info("Process exited with return code %s", returncode) + self.write_message({"event": "exit", "code": returncode}) + + def on_close(self) -> None: + # Check if proc exists (if 'start' has been run) + if self.is_process_active: + _LOGGER.debug("Terminating process") + if self._use_popen: + self._proc.terminate() + else: + self._proc.proc.terminate() + # Shutdown proc on WS close + self._is_closed = True + + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + raise NotImplementedError + + +DASHBOARD_COMMAND = ["esphome", "--dashboard"] + + +class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): + """Base class for commands that require a port.""" + + async def build_device_command( + self, args: list[str], json_message: dict[str, Any] + ) -> list[str]: + """Build the command to run.""" + dashboard = DASHBOARD + configuration = json_message["configuration"] + config_file = settings.rel_path(configuration) + port = json_message["port"] + if ( + port == "OTA" + and (mdns := dashboard.mdns_status) + and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) + and (address := await mdns.async_resolve_host(host_name)) + ): + port = address + + return [ + *DASHBOARD_COMMAND, + *args, + config_file, + "--device", + port, + ] + + +class EsphomeLogsHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["logs"], json_message) + + +class EsphomeRenameHandler(EsphomeCommandWebSocket): + old_name: str + + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + self.old_name = json_message["configuration"] + return [ + *DASHBOARD_COMMAND, + "rename", + config_file, + json_message["newName"], + ] + + def _proc_on_exit(self, returncode): + super()._proc_on_exit(returncode) + + if returncode != 0: + return + + # Remove the old ping result from the cache + DASHBOARD.ping_result.pop(self.old_name, None) + + +class EsphomeUploadHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["upload"], json_message) + + +class EsphomeRunHandler(EsphomePortCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + """Build the command to run.""" + return await self.build_device_command(["run"], json_message) + + +class EsphomeCompileHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + command = [*DASHBOARD_COMMAND, "compile"] + if json_message.get("only_generate", False): + command.append("--only-generate") + command.append(config_file) + return command + + +class EsphomeValidateHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + command = [*DASHBOARD_COMMAND, "config", config_file] + if not settings.streamer_mode: + command.append("--show-secrets") + return command + + +class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] + + +class EsphomeCleanHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + config_file = settings.rel_path(json_message["configuration"]) + return [*DASHBOARD_COMMAND, "clean", config_file] + + +class EsphomeVscodeHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"] + + +class EsphomeAceEditorHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir] + + +class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): + async def build_command(self, json_message: dict[str, Any]) -> list[str]: + return [*DASHBOARD_COMMAND, "update-all", settings.config_dir] + + +class SerialPortRequestHandler(BaseHandler): + @authenticated + async def get(self): + ports = await asyncio.get_running_loop().run_in_executor(None, get_serial_ports) + data = [] + for port in ports: + desc = port.description + if port.path == "/dev/ttyAMA0": + desc = "UART pins on GPIO header" + split_desc = desc.split(" - ") + if len(split_desc) == 2 and split_desc[0] == split_desc[1]: + # Some serial ports repeat their values + desc = split_desc[0] + data.append({"port": port.path, "desc": desc}) + data.append({"port": "OTA", "desc": "Over-The-Air"}) + data.sort(key=lambda x: x["port"], reverse=True) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + + +class WizardRequestHandler(BaseHandler): + @authenticated + def post(self): + from esphome import wizard + + kwargs = { + k: v + for k, v in json.loads(self.request.body.decode()).items() + if k in ("name", "platform", "board", "ssid", "psk", "password") + } + if not kwargs["name"]: + self.set_status(422) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Name is required"})) + return + + kwargs["friendly_name"] = kwargs["name"] + kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) + + kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + filename = f"{kwargs['name']}.yaml" + destination = settings.rel_path(filename) + wizard.wizard_write(path=destination, **kwargs) + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) + self.finish() + + +class ImportRequestHandler(BaseHandler): + @authenticated + def post(self): + from esphome.components.dashboard_import import import_config + + dashboard = DASHBOARD + args = json.loads(self.request.body.decode()) + try: + name = args["name"] + friendly_name = args.get("friendly_name") + encryption = args.get("encryption", False) + + imported_device = next( + ( + res + for res in dashboard.import_result.values() + if res.device_name == name + ), + None, + ) + + if imported_device is not None: + network = imported_device.network + if friendly_name is None: + friendly_name = imported_device.friendly_name + else: + network = const.CONF_WIFI + + import_config( + settings.rel_path(f"{name}.yaml"), + name, + friendly_name, + args["project_name"], + args["package_import_url"], + network, + encryption, + ) + # Make sure the device gets marked online right away + dashboard.ping_request.set() + except FileExistsError: + self.set_status(500) + self.write("File already exists") + return + except ValueError: + self.set_status(422) + self.write("Invalid package url") + return + + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": f"{name}.yaml"})) + self.finish() + + +class DownloadListRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + storage_path = ext_storage_path(configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS + from esphome.components.esp32 import get_download_types as esp32_types + from esphome.components.esp8266 import get_download_types as esp8266_types + from esphome.components.libretiny import get_download_types as libretiny_types + from esphome.components.rp2040 import get_download_types as rp2040_types + + downloads = [] + platform = storage_json.target_platform.lower() + if platform == const.PLATFORM_RP2040: + downloads = rp2040_types(storage_json) + elif platform == const.PLATFORM_ESP8266: + downloads = esp8266_types(storage_json) + elif platform.upper() in ESP32_VARIANTS: + downloads = esp32_types(storage_json) + elif platform == const.PLATFORM_BK72XX: + downloads = libretiny_types(storage_json) + elif platform == const.PLATFORM_RTL87XX: + downloads = libretiny_types(storage_json) + else: + self.send_error(418) + return + + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps(downloads)) + self.finish() + return + + +class DownloadBinaryRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + compressed = self.get_argument("compressed", "0") == "1" + + storage_path = ext_storage_path(configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + # fallback to type=, but prioritize file= + file_name = self.get_argument("type", None) + file_name = self.get_argument("file", file_name) + if file_name is None: + self.send_error(400) + return + file_name = file_name.replace("..", "").lstrip("/") + # get requested download name, or build it based on filename + download_name = self.get_argument( + "download", + f"{storage_json.name}-{file_name}", + ) + path = os.path.dirname(storage_json.firmware_bin_path) + path = os.path.join(path, file_name) + + if not Path(path).is_file(): + args = ["esphome", "idedata", settings.rel_path(configuration)] + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(404 if rc == 2 else 500) + return + + idedata = platformio_api.IDEData(json.loads(stdout)) + + found = False + for image in idedata.extra_flash_images: + if image.path.endswith(file_name): + path = image.path + download_name = file_name + found = True + break + + if not found: + self.send_error(404) + return + + download_name = download_name + ".gz" if compressed else download_name + + self.set_header("Content-Type", "application/octet-stream") + self.set_header( + "Content-Disposition", f'attachment; filename="{download_name}"' + ) + self.set_header("Cache-Control", "no-cache") + if not Path(path).is_file(): + self.send_error(404) + return + + with open(path, "rb") as f: + data = f.read() + if compressed: + data = gzip.compress(data, 9) + self.write(data) + + self.finish() + + +class EsphomeVersionHandler(BaseHandler): + @authenticated + def get(self): + self.set_header("Content-Type", "application/json") + self.write(json.dumps({"version": const.__version__})) + self.finish() + + +class ListDevicesHandler(BaseHandler): + @authenticated + async def get(self): + loop = asyncio.get_running_loop() + entries = await loop.run_in_executor(None, list_dashboard_entries) + self.set_header("content-type", "application/json") + configured = {entry.name for entry in entries} + dashboard = DASHBOARD + + self.write( + json.dumps( + { + "configured": [ + { + "name": entry.name, + "friendly_name": entry.friendly_name, + "configuration": entry.filename, + "loaded_integrations": entry.loaded_integrations, + "deployed_version": entry.update_old, + "current_version": entry.update_new, + "path": entry.path, + "comment": entry.comment, + "address": entry.address, + "web_port": entry.web_port, + "target_platform": entry.target_platform, + } + for entry in entries + ], + "importable": [ + { + "name": res.device_name, + "friendly_name": res.friendly_name, + "package_import_url": res.package_import_url, + "project_name": res.project_name, + "project_version": res.project_version, + "network": res.network, + } + for res in dashboard.import_result.values() + if res.device_name not in configured + ], + } + ) + ) + + +class MainRequestHandler(BaseHandler): + @authenticated + def get(self): + begin = bool(self.get_argument("begin", False)) + + self.render( + "index.template.html", + begin=begin, + **template_args(), + login_enabled=settings.using_password, + ) + + +class PrometheusServiceDiscoveryHandler(BaseHandler): + @authenticated + def get(self): + entries = list_dashboard_entries() + self.set_header("content-type", "application/json") + sd = [] + for entry in entries: + if entry.web_port is None: + continue + labels = { + "__meta_name": entry.name, + "__meta_esp_platform": entry.target_platform, + "__meta_esphome_version": entry.storage.esphome_version, + } + for integration in entry.storage.loaded_integrations: + labels[f"__meta_integration_{integration}"] = "true" + sd.append( + { + "targets": [ + f"{entry.address}:{entry.web_port}", + ], + "labels": labels, + } + ) + self.write(json.dumps(sd)) + + +class BoardsRequestHandler(BaseHandler): + @authenticated + def get(self, platform: str): + from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS + from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS + from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS + from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS + from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS + + platform_to_boards = { + const.PLATFORM_ESP32: ESP32_BOARDS, + const.PLATFORM_ESP8266: ESP8266_BOARDS, + const.PLATFORM_RP2040: RP2040_BOARDS, + const.PLATFORM_BK72XX: BK72XX_BOARDS, + const.PLATFORM_RTL87XX: RTL87XX_BOARDS, + } + # filter all ESP32 variants by requested platform + if platform.startswith("esp32"): + boards = { + k: v + for k, v in platform_to_boards[const.PLATFORM_ESP32].items() + if v[const.KEY_VARIANT] == platform.upper() + } + else: + boards = platform_to_boards[platform] + + # map to a {board_name: board_title} dict + platform_boards = {key: val[const.KEY_NAME] for key, val in boards.items()} + # sort by board title + boards_items = sorted(platform_boards.items(), key=lambda item: item[1]) + output = [{"items": dict(boards_items)}] + + self.set_header("content-type", "application/json") + self.write(json.dumps(output)) + + +class PingRequestHandler(BaseHandler): + @authenticated + def get(self): + dashboard = DASHBOARD + dashboard.ping_request.set() + if settings.status_use_mqtt: + dashboard.mqtt_ping_request.set() + self.set_header("content-type", "application/json") + self.write(json.dumps(dashboard.ping_result)) + + +class InfoRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + yaml_path = settings.rel_path(configuration) + all_yaml_files = settings.list_yaml_files() + + if yaml_path not in all_yaml_files: + self.set_status(404) + return + + self.set_header("content-type", "application/json") + self.write(DashboardEntry(yaml_path).storage.to_json()) + + +class EditRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + filename = settings.rel_path(configuration) + content = "" + if os.path.isfile(filename): + with open(file=filename, encoding="utf-8") as f: + content = f.read() + self.write(content) + + @authenticated + @bind_config + def post(self, configuration=None): + with open(file=settings.rel_path(configuration), mode="wb") as f: + f.write(self.request.body) + self.set_status(200) + + +class DeleteRequestHandler(BaseHandler): + @authenticated + @bind_config + def post(self, configuration=None): + config_file = settings.rel_path(configuration) + storage_path = ext_storage_path(configuration) + + trash_path = trash_storage_path() + mkdir_p(trash_path) + shutil.move(config_file, os.path.join(trash_path, configuration)) + + storage_json = StorageJSON.load(storage_path) + if storage_json is not None: + # Delete build folder (if exists) + name = storage_json.name + build_folder = os.path.join(settings.config_dir, name) + if build_folder is not None: + shutil.rmtree(build_folder, os.path.join(trash_path, name)) + + # Remove the old ping result from the cache + DASHBOARD.ping_result.pop(configuration, None) + + +class UndoDeleteRequestHandler(BaseHandler): + @authenticated + @bind_config + def post(self, configuration=None): + config_file = settings.rel_path(configuration) + trash_path = trash_storage_path() + shutil.move(os.path.join(trash_path, configuration), config_file) + + +class LoginHandler(BaseHandler): + def get(self): + if is_authenticated(self): + self.redirect("./") + else: + self.render_login_page() + + def render_login_page(self, error=None): + self.render( + "login.template.html", + error=error, + ha_addon=settings.using_ha_addon_auth, + has_username=bool(settings.username), + **template_args(), + ) + + def post_ha_addon_login(self): + import requests + + headers = { + "X-Supervisor-Token": os.getenv("SUPERVISOR_TOKEN"), + } + + data = { + "username": self.get_argument("username", ""), + "password": self.get_argument("password", ""), + } + try: + req = requests.post( + "http://supervisor/auth", headers=headers, json=data, timeout=30 + ) + if req.status_code == 200: + self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self.redirect("/") + return + except Exception as err: # pylint: disable=broad-except + _LOGGER.warning("Error during Hass.io auth request: %s", err) + self.set_status(500) + self.render_login_page(error="Internal server error") + return + self.set_status(401) + self.render_login_page(error="Invalid username or password") + + def post_native_login(self): + username = self.get_argument("username", "") + password = self.get_argument("password", "") + if settings.check_password(username, password): + self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self.redirect("./") + return + error_str = ( + "Invalid username or password" if settings.username else "Invalid password" + ) + self.set_status(401) + self.render_login_page(error=error_str) + + def post(self): + if settings.using_ha_addon_auth: + self.post_ha_addon_login() + else: + self.post_native_login() + + +class LogoutHandler(BaseHandler): + @authenticated + def get(self): + self.clear_cookie("authenticated") + self.redirect("./login") + + +class SecretKeysRequestHandler(BaseHandler): + @authenticated + def get(self): + filename = None + + for secret_filename in const.SECRETS_FILES: + relative_filename = settings.rel_path(secret_filename) + if os.path.isfile(relative_filename): + filename = relative_filename + break + + if filename is None: + self.send_error(404) + return + + secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False)) + + self.set_header("content-type", "application/json") + self.write(json.dumps(secret_keys)) + + +class SafeLoaderIgnoreUnknown(yaml.SafeLoader): + def ignore_unknown(self, node): + return f"{node.tag} {node.value}" + + def construct_yaml_binary(self, node) -> str: + return super().construct_yaml_binary(node).decode("ascii") + + +SafeLoaderIgnoreUnknown.add_constructor(None, SafeLoaderIgnoreUnknown.ignore_unknown) +SafeLoaderIgnoreUnknown.add_constructor( + "tag:yaml.org,2002:binary", SafeLoaderIgnoreUnknown.construct_yaml_binary +) + + +class JsonConfigRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + filename = settings.rel_path(configuration) + if not os.path.isfile(filename): + self.send_error(404) + return + + args = ["esphome", "config", filename, "--show-secrets"] + + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(422) + return + + data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown) + self.set_header("content-type", "application/json") + self.write(json.dumps(data)) + self.finish() + + +def get_base_frontend_path(): + if ENV_DEV not in os.environ: + import esphome_dashboard + + return esphome_dashboard.where() + + static_path = os.environ[ENV_DEV] + if not static_path.endswith("/"): + static_path += "/" + + # This path can be relative, so resolve against the root or else templates don't work + return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) + + +def get_static_path(*args): + return os.path.join(get_base_frontend_path(), "static", *args) + + +@functools.cache +def get_static_file_url(name): + base = f"./static/{name}" + + if ENV_DEV in os.environ: + return base + + # Module imports can't deduplicate if stuff added to url + if name == "js/esphome/index.js": + import esphome_dashboard + + return base.replace("index.js", esphome_dashboard.entrypoint()) + + path = get_static_path(name) + with open(path, "rb") as f_handle: + hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] + return f"{base}?hash={hash_}" + + +def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application: + def log_function(handler): + if handler.get_status() < 400: + log_method = access_log.info + + if isinstance(handler, SerialPortRequestHandler) and not debug: + return + if isinstance(handler, PingRequestHandler) and not debug: + return + elif handler.get_status() < 500: + log_method = access_log.warning + else: + log_method = access_log.error + + request_time = 1000.0 * handler.request.request_time() + # pylint: disable=protected-access + log_method( + "%d %s %.2fms", + handler.get_status(), + handler._request_summary(), + request_time, + ) + + class StaticFileHandler(tornado.web.StaticFileHandler): + def get_cache_time( + self, path: str, modified: datetime.datetime | None, mime_type: str + ) -> int: + """Override to customize cache control behavior.""" + if debug: + return 0 + # Assets that are hashed have ?hash= in the URL, all javascript + # filenames hashed so we can cache them for a long time + if "hash" in self.request.arguments or "/javascript" in mime_type: + return self.CACHE_MAX_AGE + return super().get_cache_time(path, modified, mime_type) + + app_settings = { + "debug": debug, + "cookie_secret": settings.cookie_secret, + "log_function": log_function, + "websocket_ping_interval": 30.0, + "template_path": get_base_frontend_path(), + } + rel = settings.relative_url + return tornado.web.Application( + [ + (f"{rel}", MainRequestHandler), + (f"{rel}login", LoginHandler), + (f"{rel}logout", LogoutHandler), + (f"{rel}logs", EsphomeLogsHandler), + (f"{rel}upload", EsphomeUploadHandler), + (f"{rel}run", EsphomeRunHandler), + (f"{rel}compile", EsphomeCompileHandler), + (f"{rel}validate", EsphomeValidateHandler), + (f"{rel}clean-mqtt", EsphomeCleanMqttHandler), + (f"{rel}clean", EsphomeCleanHandler), + (f"{rel}vscode", EsphomeVscodeHandler), + (f"{rel}ace", EsphomeAceEditorHandler), + (f"{rel}update-all", EsphomeUpdateAllHandler), + (f"{rel}info", InfoRequestHandler), + (f"{rel}edit", EditRequestHandler), + (f"{rel}downloads", DownloadListRequestHandler), + (f"{rel}download.bin", DownloadBinaryRequestHandler), + (f"{rel}serial-ports", SerialPortRequestHandler), + (f"{rel}ping", PingRequestHandler), + (f"{rel}delete", DeleteRequestHandler), + (f"{rel}undo-delete", UndoDeleteRequestHandler), + (f"{rel}wizard", WizardRequestHandler), + (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), + (f"{rel}devices", ListDevicesHandler), + (f"{rel}import", ImportRequestHandler), + (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}json-config", JsonConfigRequestHandler), + (f"{rel}rename", EsphomeRenameHandler), + (f"{rel}prometheus-sd", PrometheusServiceDiscoveryHandler), + (f"{rel}boards/([a-z0-9]+)", BoardsRequestHandler), + (f"{rel}version", EsphomeVersionHandler), + ], + **app_settings, + ) + + +def start_web_server( + app: tornado.web.Application, + socket: str | None, + address: str | None, + port: int | None, + config_dir: str, +) -> None: + """Start the web server listener.""" + if socket is None: + _LOGGER.info( + "Starting dashboard web server on http://%s:%s and configuration dir %s...", + address, + port, + config_dir, + ) + app.listen(port, address) + return + + _LOGGER.info( + "Starting dashboard web server on unix socket %s and configuration dir %s...", + socket, + config_dir, + ) + server = tornado.httpserver.HTTPServer(app) + socket = tornado.netutil.bind_unix_socket(socket, mode=0o666) + server.add_socket(socket) From 4e3170dc95e90702126ccb14d7b54fb252ae5098 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:48:59 +0000 Subject: [PATCH 215/270] Bump zeroconf from 0.126.0 to 0.127.0 (#5768) 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 abe632bc6c..9afe7064c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.4.1 -zeroconf==0.126.0 +zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 3644853d38fd3f95fae038b6f5441444306306ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 18:07:51 -0600 Subject: [PATCH 216/270] dashboard: fix subprocesses blocking the event loop (#5772) * dashboard: fix subprocesses blocking the event loop - break apart the util module - adds a new util to run subprocesses with asyncio * take a list --- esphome/dashboard/settings.py | 2 +- esphome/dashboard/status/ping.py | 15 +++----- esphome/dashboard/util.py | 52 ---------------------------- esphome/dashboard/util/__init__.py | 0 esphome/dashboard/util/itertools.py | 22 ++++++++++++ esphome/dashboard/util/password.py | 11 ++++++ esphome/dashboard/util/subprocess.py | 31 +++++++++++++++++ esphome/dashboard/util/text.py | 25 +++++++++++++ esphome/dashboard/web_server.py | 13 +++---- 9 files changed, 101 insertions(+), 70 deletions(-) delete mode 100644 esphome/dashboard/util.py create mode 100644 esphome/dashboard/util/__init__.py create mode 100644 esphome/dashboard/util/itertools.py create mode 100644 esphome/dashboard/util/password.py create mode 100644 esphome/dashboard/util/subprocess.py create mode 100644 esphome/dashboard/util/text.py diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 1ddb6f652d..888616f6f7 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -10,7 +10,7 @@ from esphome.helpers import get_bool_env from esphome.storage_json import ext_storage_path from .entries import DashboardEntry -from .util import password_hash +from .util.password import password_hash class DashboardSettings: diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 17c1254c9d..678d7844ae 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -7,22 +7,15 @@ from typing import cast from ..core import DASHBOARD from ..entries import DashboardEntry from ..core import list_dashboard_entries -from ..util import chunked +from ..util.itertools import chunked +from ..util.subprocess import async_system_command_status async def _async_ping_host(host: str) -> bool: """Ping a host.""" - ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"] - process = await asyncio.create_subprocess_exec( - *ping_command, - host, - stdin=asyncio.subprocess.DEVNULL, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL, - close_fds=False, + return await async_system_command_status( + ["ping", "-n" if os.name == "nt" else "-c", "1", host] ) - await process.wait() - return process.returncode == 0 class PingStatus: diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py deleted file mode 100644 index 7b6572b989..0000000000 --- a/esphome/dashboard/util.py +++ /dev/null @@ -1,52 +0,0 @@ -import hashlib -import unicodedata -from collections.abc import Iterable -from functools import partial -from itertools import islice -from typing import Any - -from esphome.const import ALLOWED_NAME_CHARS - - -def password_hash(password: str) -> bytes: - """Create a hash of a password to transform it to a fixed-length digest. - - Note this is not meant for secure storage, but for securely comparing passwords. - """ - return hashlib.sha256(password.encode()).digest() - - -def strip_accents(value): - return "".join( - c - for c in unicodedata.normalize("NFD", str(value)) - if unicodedata.category(c) != "Mn" - ) - - -def friendly_name_slugify(value): - value = ( - strip_accents(value) - .lower() - .replace(" ", "-") - .replace("_", "-") - .replace("--", "-") - .strip("-") - ) - return "".join(c for c in value if c in ALLOWED_NAME_CHARS) - - -def take(take_num: int, iterable: Iterable) -> list[Any]: - """Return first n items of the iterable as a list. - - From itertools recipes - """ - return list(islice(iterable, take_num)) - - -def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: - """Break *iterable* into lists of length *n*. - - From more-itertools - """ - return iter(partial(take, chunked_num, iter(iterable)), []) diff --git a/esphome/dashboard/util/__init__.py b/esphome/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/dashboard/util/itertools.py b/esphome/dashboard/util/itertools.py new file mode 100644 index 0000000000..54e95ef802 --- /dev/null +++ b/esphome/dashboard/util/itertools.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from collections.abc import Iterable +from functools import partial +from itertools import islice +from typing import Any + + +def take(take_num: int, iterable: Iterable) -> list[Any]: + """Return first n items of the iterable as a list. + + From itertools recipes + """ + return list(islice(iterable, take_num)) + + +def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: + """Break *iterable* into lists of length *n*. + + From more-itertools + """ + return iter(partial(take, chunked_num, iter(iterable)), []) diff --git a/esphome/dashboard/util/password.py b/esphome/dashboard/util/password.py new file mode 100644 index 0000000000..e7ea28c25d --- /dev/null +++ b/esphome/dashboard/util/password.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +import hashlib + + +def password_hash(password: str) -> bytes: + """Create a hash of a password to transform it to a fixed-length digest. + + Note this is not meant for secure storage, but for securely comparing passwords. + """ + return hashlib.sha256(password.encode()).digest() diff --git a/esphome/dashboard/util/subprocess.py b/esphome/dashboard/util/subprocess.py new file mode 100644 index 0000000000..583dd116e3 --- /dev/null +++ b/esphome/dashboard/util/subprocess.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + + +async def async_system_command_status(command: Iterable[str]) -> bool: + """Run a system command checking only the status.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + close_fds=False, + ) + await process.wait() + return process.returncode == 0 + + +async def async_run_system_command(command: Iterable[str]) -> tuple[bool, bytes, bytes]: + """Run a system command and return a tuple of returncode, stdout, stderr.""" + process = await asyncio.create_subprocess_exec( + *command, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + close_fds=False, + ) + stdout, stderr = await process.communicate() + await process.wait() + return process.returncode, stdout, stderr diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py new file mode 100644 index 0000000000..08d2df6abf --- /dev/null +++ b/esphome/dashboard/util/text.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 086a28cbb2..c0cc00b66c 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -31,13 +31,14 @@ import yaml from tornado.log import access_log from esphome import const, platformio_api, yaml_util -from esphome.helpers import get_bool_env, mkdir_p, run_system_command +from esphome.helpers import get_bool_env, mkdir_p from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path from esphome.util import get_serial_ports, shlex_quote from .core import DASHBOARD, list_dashboard_entries from .entries import DashboardEntry -from .util import friendly_name_slugify +from .util.text import friendly_name_slugify +from .util.subprocess import async_run_system_command _LOGGER = logging.getLogger(__name__) @@ -522,7 +523,7 @@ class DownloadListRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): compressed = self.get_argument("compressed", "0") == "1" storage_path = ext_storage_path(configuration) @@ -548,7 +549,7 @@ class DownloadBinaryRequestHandler(BaseHandler): if not Path(path).is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] - rc, stdout, _ = run_system_command(*args) + rc, stdout, _ = await async_run_system_command(args) if rc != 0: self.send_error(404 if rc == 2 else 500) @@ -902,7 +903,7 @@ SafeLoaderIgnoreUnknown.add_constructor( class JsonConfigRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): filename = settings.rel_path(configuration) if not os.path.isfile(filename): self.send_error(404) @@ -910,7 +911,7 @@ class JsonConfigRequestHandler(BaseHandler): args = ["esphome", "config", filename, "--show-secrets"] - rc, stdout, _ = run_system_command(*args) + rc, stdout, _ = await async_run_system_command(args) if rc != 0: self.send_error(422) From 5f1d8dfa5bcc9d1ecae593e2e05c8a5609bca767 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 18:08:17 -0600 Subject: [PATCH 217/270] dashboard: use fastest available yaml loader in the dashboard (#5771) * dashboard: use fastest available yaml loader in the dashboard * remove unrelated change --- esphome/dashboard/web_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index c0cc00b66c..76faa43015 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -34,6 +34,7 @@ from esphome import const, platformio_api, yaml_util from esphome.helpers import get_bool_env, mkdir_p from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path from esphome.util import get_serial_ports, shlex_quote +from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD, list_dashboard_entries from .entries import DashboardEntry @@ -886,7 +887,7 @@ class SecretKeysRequestHandler(BaseHandler): self.write(json.dumps(secret_keys)) -class SafeLoaderIgnoreUnknown(yaml.SafeLoader): +class SafeLoaderIgnoreUnknown(FastestAvailableSafeLoader): def ignore_unknown(self, node): return f"{node.tag} {node.value}" From 149d814fab0980a4948806e014306d587805eadf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 20:49:56 -0600 Subject: [PATCH 218/270] dashboard: Centralize dashboard entries into DashboardEntries class (#5774) * Centralize dashboard entries into DashboardEntries class * preen * preen * preen * preen * preen --- esphome/dashboard/core.py | 11 +- esphome/dashboard/entries.py | 202 ++++++++++++++++++++++++++----- esphome/dashboard/settings.py | 86 ++----------- esphome/dashboard/status/mdns.py | 7 +- esphome/dashboard/status/mqtt.py | 7 +- esphome/dashboard/status/ping.py | 3 +- esphome/dashboard/web_server.py | 26 ++-- 7 files changed, 209 insertions(+), 133 deletions(-) diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index 4cc2938bb1..f18da92d80 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -6,7 +6,7 @@ import threading from typing import TYPE_CHECKING from ..zeroconf import DiscoveredImport -from .entries import DashboardEntry +from .entries import DashboardEntries from .settings import DashboardSettings if TYPE_CHECKING: @@ -15,15 +15,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -def list_dashboard_entries() -> list[DashboardEntry]: - """List all dashboard entries.""" - return DASHBOARD.settings.entries() - - class ESPHomeDashboard: """Class that represents the dashboard.""" __slots__ = ( + "entries", "loop", "ping_result", "import_result", @@ -36,6 +32,7 @@ class ESPHomeDashboard: def __init__(self) -> None: """Initialize the ESPHomeDashboard.""" + self.entries: DashboardEntries | None = None self.loop: asyncio.AbstractEventLoop | None = None self.ping_result: dict[str, bool | None] = {} self.import_result: dict[str, DiscoveredImport] = {} @@ -49,12 +46,14 @@ class ESPHomeDashboard: """Setup the dashboard.""" self.loop = asyncio.get_running_loop() self.ping_request = asyncio.Event() + self.entries = DashboardEntries(self.settings.config_dir) async def async_run(self) -> None: """Run the dashboard.""" settings = self.settings mdns_task: asyncio.Task | None = None ping_status_task: asyncio.Task | None = None + await self.entries.async_update_entries() if settings.status_use_ping: from .status.ping import PingStatus diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 582073d655..ff539fc620 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -1,10 +1,150 @@ from __future__ import annotations +import asyncio +import logging import os -from esphome import const +from esphome import const, util from esphome.storage_json import StorageJSON, ext_storage_path +_LOGGER = logging.getLogger(__name__) + +DashboardCacheKeyType = tuple[int, int, float, int] + + +class DashboardEntries: + """Represents all dashboard entries.""" + + __slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock") + + def __init__(self, config_dir: str) -> None: + """Initialize the DashboardEntries.""" + self._loop = asyncio.get_running_loop() + self._config_dir = config_dir + # Entries are stored as + # { + # "path/to/file.yaml": DashboardEntry, + # ... + # } + self._entries: dict[str, DashboardEntry] = {} + self._loaded_entries = False + self._update_lock = asyncio.Lock() + + def get(self, path: str) -> DashboardEntry | None: + """Get an entry by path.""" + return self._entries.get(path) + + async def _async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + def all(self) -> list[DashboardEntry]: + """Return all entries.""" + return asyncio.run_coroutine_threadsafe(self._async_all, self._loop).result() + + def async_all(self) -> list[DashboardEntry]: + """Return all entries.""" + return list(self._entries.values()) + + async def async_request_update_entries(self) -> None: + """Request an update of the dashboard entries from disk. + + If an update is already in progress, this will do nothing. + """ + if self._update_lock.locked(): + _LOGGER.debug("Dashboard entries are already being updated") + return + await self.async_update_entries() + + async def async_update_entries(self) -> None: + """Update the dashboard entries from disk.""" + async with self._update_lock: + await self._async_update_entries() + + def _load_entries( + self, entries: dict[DashboardEntry, DashboardCacheKeyType] + ) -> None: + """Load all entries from disk.""" + for entry, cache_key in entries.items(): + _LOGGER.debug( + "Loading dashboard entry %s because cache key changed: %s", + entry.path, + cache_key, + ) + entry.load_from_disk(cache_key) + + async def _async_update_entries(self) -> list[DashboardEntry]: + """Sync the dashboard entries from disk.""" + _LOGGER.debug("Updating dashboard entries") + # At some point it would be nice to use watchdog to avoid polling + + path_to_cache_key = await self._loop.run_in_executor( + None, self._get_path_to_cache_key + ) + added: dict[DashboardEntry, DashboardCacheKeyType] = {} + updated: dict[DashboardEntry, DashboardCacheKeyType] = {} + removed: set[DashboardEntry] = { + entry + for filename, entry in self._entries.items() + if filename not in path_to_cache_key + } + entries = self._entries + for path, cache_key in path_to_cache_key.items(): + if entry := self._entries.get(path): + if entry.cache_key != cache_key: + updated[entry] = cache_key + else: + entry = DashboardEntry(path, cache_key) + added[entry] = cache_key + + if added or updated: + await self._loop.run_in_executor( + None, self._load_entries, {**added, **updated} + ) + + for entry in added: + _LOGGER.debug("Added dashboard entry %s", entry.path) + entries[entry.path] = entry + + if entry in removed: + _LOGGER.debug("Removed dashboard entry %s", entry.path) + entries.pop(entry.path) + + for entry in updated: + _LOGGER.debug("Updated dashboard entry %s", entry.path) + # In the future we can fire events when entries are added/removed/updated + + def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: + """Return a dict of path to cache key.""" + path_to_cache_key: dict[str, DashboardCacheKeyType] = {} + # + # The cache key is (inode, device, mtime, size) + # which allows us to avoid locking since it ensures + # every iteration of this call will always return the newest + # items from disk at the cost of a stat() call on each + # file which is much faster than reading the file + # for the cache hit case which is the common case. + # + for file in util.list_yaml_files([self._config_dir]): + try: + # Prefer the json storage path if it exists + stat = os.stat(ext_storage_path(os.path.basename(file))) + except OSError: + try: + # Fallback to the yaml file if the storage + # file does not exist or could not be generated + stat = os.stat(file) + except OSError: + # File was deleted, ignore + continue + path_to_cache_key[file] = ( + stat.st_ino, + stat.st_dev, + stat.st_mtime, + stat.st_size, + ) + return path_to_cache_key + class DashboardEntry: """Represents a single dashboard entry. @@ -12,13 +152,15 @@ class DashboardEntry: This class is thread-safe and read-only. """ - __slots__ = ("path", "_storage", "_loaded_storage") + __slots__ = ("path", "filename", "_storage_path", "cache_key", "storage") - def __init__(self, path: str) -> None: + def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: """Initialize the DashboardEntry.""" self.path = path - self._storage = None - self._loaded_storage = False + self.filename = os.path.basename(path) + self._storage_path = ext_storage_path(self.filename) + self.cache_key = cache_key + self.storage: StorageJSON | None = None def __repr__(self): """Return the representation of this entry.""" @@ -30,87 +172,91 @@ class DashboardEntry: f"no_mdns={self.no_mdns})" ) - @property - def filename(self): - """Return the filename of this entry.""" - return os.path.basename(self.path) + def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: + """Load this entry from disk.""" + self.storage = StorageJSON.load(self._storage_path) + # + # Currently StorageJSON.load() will return None if the file does not exist + # + # StorageJSON currently does not provide an updated cache key so we use the + # one that is passed in. + # + # The cache key was read from the disk moments ago and may be stale but + # it does not matter since we are polling anyways, and the next call to + # async_update_entries() will load it again in the extremely rare case that + # it changed between the two calls. + # + if cache_key: + self.cache_key = cache_key @property - def storage(self) -> StorageJSON | None: - """Return the StorageJSON object for this entry.""" - if not self._loaded_storage: - self._storage = StorageJSON.load(ext_storage_path(self.filename)) - self._loaded_storage = True - return self._storage - - @property - def address(self): + def address(self) -> str | None: """Return the address of this entry.""" if self.storage is None: return None return self.storage.address @property - def no_mdns(self): + def no_mdns(self) -> bool | None: """Return the no_mdns of this entry.""" if self.storage is None: return None return self.storage.no_mdns @property - def web_port(self): + def web_port(self) -> int | None: """Return the web port of this entry.""" if self.storage is None: return None return self.storage.web_port @property - def name(self): + def name(self) -> str: """Return the name of this entry.""" if self.storage is None: return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name @property - def friendly_name(self): + def friendly_name(self) -> str: """Return the friendly name of this entry.""" if self.storage is None: return self.name return self.storage.friendly_name @property - def comment(self): + def comment(self) -> str | None: """Return the comment of this entry.""" if self.storage is None: return None return self.storage.comment @property - def target_platform(self): + def target_platform(self) -> str | None: """Return the target platform of this entry.""" if self.storage is None: return None return self.storage.target_platform @property - def update_available(self): + def update_available(self) -> bool: """Return if an update is available for this entry.""" if self.storage is None: return True return self.update_old != self.update_new @property - def update_old(self): + def update_old(self) -> str: if self.storage is None: return "" return self.storage.esphome_version or "" @property - def update_new(self): + def update_new(self) -> str: return const.__version__ @property - def loaded_integrations(self): + def loaded_integrations(self) -> list[str]: if self.storage is None: return [] return self.storage.loaded_integrations diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 888616f6f7..76633e1bf2 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -4,29 +4,23 @@ import hmac import os from pathlib import Path -from esphome import util from esphome.core import CORE from esphome.helpers import get_bool_env -from esphome.storage_json import ext_storage_path -from .entries import DashboardEntry from .util.password import password_hash class DashboardSettings: """Settings for the dashboard.""" - def __init__(self): - self.config_dir = "" - self.password_hash = "" - self.username = "" - self.using_password = False - self.on_ha_addon = False - self.cookie_secret = None - self.absolute_config_dir = None - self._entry_cache: dict[ - str, tuple[tuple[int, int, float, int], DashboardEntry] - ] = {} + def __init__(self) -> None: + self.config_dir: str = "" + self.password_hash: str = "" + self.username: str = "" + self.using_password: bool = False + self.on_ha_addon: bool = False + self.cookie_secret: str | None = None + self.absolute_config_dir: Path | None = None def parse_args(self, args): self.on_ha_addon: bool = args.ha_addon @@ -80,67 +74,3 @@ class DashboardSettings: # Raises ValueError if not relative to ESPHome config folder Path(joined_path).resolve().relative_to(self.absolute_config_dir) return joined_path - - def list_yaml_files(self) -> list[str]: - return util.list_yaml_files([self.config_dir]) - - def entries(self) -> list[DashboardEntry]: - """Fetch all dashboard entries, thread-safe.""" - path_to_cache_key: dict[str, tuple[int, int, float, int]] = {} - # - # The cache key is (inode, device, mtime, size) - # which allows us to avoid locking since it ensures - # every iteration of this call will always return the newest - # items from disk at the cost of a stat() call on each - # file which is much faster than reading the file - # for the cache hit case which is the common case. - # - # Because there is no lock the cache may - # get built more than once but that's fine as its still - # thread-safe and results in orders of magnitude less - # reads from disk than if we did not cache at all and - # does not have a lock contention issue. - # - for file in self.list_yaml_files(): - try: - # Prefer the json storage path if it exists - stat = os.stat(ext_storage_path(os.path.basename(file))) - except OSError: - try: - # Fallback to the yaml file if the storage - # file does not exist or could not be generated - stat = os.stat(file) - except OSError: - # File was deleted, ignore - continue - path_to_cache_key[file] = ( - stat.st_ino, - stat.st_dev, - stat.st_mtime, - stat.st_size, - ) - - entry_cache = self._entry_cache - - # Remove entries that no longer exist - removed: list[str] = [] - for file in entry_cache: - if file not in path_to_cache_key: - removed.append(file) - - for file in removed: - entry_cache.pop(file) - - dashboard_entries: list[DashboardEntry] = [] - for file, cache_key in path_to_cache_key.items(): - if cached_entry := entry_cache.get(file): - entry_key, dashboard_entry = cached_entry - if entry_key == cache_key: - dashboard_entries.append(dashboard_entry) - continue - - dashboard_entry = DashboardEntry(file) - dashboard_entries.append(dashboard_entry) - entry_cache[file] = (cache_key, dashboard_entry) - - return dashboard_entries diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 454bba9cb5..51d11390b7 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -10,7 +10,7 @@ from esphome.zeroconf import ( DashboardStatus, ) -from ..core import DASHBOARD, list_dashboard_entries +from ..core import DASHBOARD class MDNSStatus: @@ -41,12 +41,13 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" - entries = await self._loop.run_in_executor(None, list_dashboard_entries) + dashboard = DASHBOARD + entries = dashboard.entries.async_all() host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename filename_to_host_name = self.filename_to_host_name - ping_result = DASHBOARD.ping_result + ping_result = dashboard.ping_result for entry in entries: name = entry.name diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py index 109b60e133..2fd3a332a7 100644 --- a/esphome/dashboard/status/mqtt.py +++ b/esphome/dashboard/status/mqtt.py @@ -7,7 +7,7 @@ import threading from esphome import mqtt -from ..core import DASHBOARD, list_dashboard_entries +from ..core import DASHBOARD class MqttStatusThread(threading.Thread): @@ -16,7 +16,7 @@ class MqttStatusThread(threading.Thread): def run(self) -> None: """Run the status thread.""" dashboard = DASHBOARD - entries = list_dashboard_entries() + entries = dashboard.entries.all() config = mqtt.config_from_env() topic = "esphome/discover/#" @@ -51,8 +51,7 @@ class MqttStatusThread(threading.Thread): client.loop_start() while not dashboard.stop_event.wait(2): - # update entries - entries = list_dashboard_entries() + entries = dashboard.entries.all() # will be set to true on on_message for entry in entries: diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 678d7844ae..35fb2259f0 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -6,7 +6,6 @@ from typing import cast from ..core import DASHBOARD from ..entries import DashboardEntry -from ..core import list_dashboard_entries from ..util.itertools import chunked from ..util.subprocess import async_system_command_status @@ -32,7 +31,7 @@ class PingStatus: # Only ping if the dashboard is open await dashboard.ping_request.wait() dashboard.ping_result.clear() - entries = await self._loop.run_in_executor(None, list_dashboard_entries) + entries = dashboard.entries.async_all() to_ping: list[DashboardEntry] = [ entry for entry in entries if entry.address is not None ] diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 76faa43015..9a5de0a933 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -36,10 +36,9 @@ from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_pa from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader -from .core import DASHBOARD, list_dashboard_entries -from .entries import DashboardEntry -from .util.text import friendly_name_slugify +from .core import DASHBOARD from .util.subprocess import async_run_system_command +from .util.text import friendly_name_slugify _LOGGER = logging.getLogger(__name__) @@ -601,11 +600,11 @@ class EsphomeVersionHandler(BaseHandler): class ListDevicesHandler(BaseHandler): @authenticated async def get(self): - loop = asyncio.get_running_loop() - entries = await loop.run_in_executor(None, list_dashboard_entries) + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() self.set_header("content-type", "application/json") configured = {entry.name for entry in entries} - dashboard = DASHBOARD self.write( json.dumps( @@ -658,8 +657,10 @@ class MainRequestHandler(BaseHandler): class PrometheusServiceDiscoveryHandler(BaseHandler): @authenticated - def get(self): - entries = list_dashboard_entries() + async def get(self): + dashboard = DASHBOARD + await dashboard.entries.async_request_update_entries() + entries = dashboard.entries.async_all() self.set_header("content-type", "application/json") sd = [] for entry in entries: @@ -733,16 +734,17 @@ class PingRequestHandler(BaseHandler): class InfoRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration=None): yaml_path = settings.rel_path(configuration) - all_yaml_files = settings.list_yaml_files() + dashboard = DASHBOARD + entry = dashboard.entries.get(yaml_path) - if yaml_path not in all_yaml_files: + if not entry: self.set_status(404) return self.set_header("content-type", "application/json") - self.write(DashboardEntry(yaml_path).storage.to_json()) + self.write(entry.storage.to_json()) class EditRequestHandler(BaseHandler): From ef945d298c9d171535c16e5b1719bd1cbdf0d152 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 15 Nov 2023 21:29:50 -0600 Subject: [PATCH 219/270] Add more VA triggers (#5762) --- .../components/voice_assistant/__init__.py | 61 ++++++++++++++++--- .../voice_assistant/voice_assistant.cpp | 39 ++++++++---- .../voice_assistant/voice_assistant.h | 16 +++-- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 3270b9f370..5715604605 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] -CONF_SILENCE_DETECTION = "silence_detection" -CONF_ON_LISTENING = "on_listening" -CONF_ON_START = "on_start" -CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" -CONF_ON_STT_END = "on_stt_end" -CONF_ON_TTS_START = "on_tts_start" -CONF_ON_TTS_END = "on_tts_end" CONF_ON_END = "on_end" CONF_ON_ERROR = "on_error" +CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_START = "on_intent_start" +CONF_ON_LISTENING = "on_listening" +CONF_ON_START = "on_start" +CONF_ON_STT_END = "on_stt_end" +CONF_ON_STT_VAD_END = "on_stt_vad_end" +CONF_ON_STT_VAD_START = "on_stt_vad_start" +CONF_ON_TTS_END = "on_tts_end" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +CONF_SILENCE_DETECTION = "silence_detection" CONF_USE_WAKE_WORD = "use_wake_word" CONF_VAD_THRESHOLD = "vad_threshold" -CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" @@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), ) @@ -177,6 +194,34 @@ async def to_code(config): config[CONF_ON_CLIENT_DISCONNECTED], ) + if CONF_ON_INTENT_START in config: + await automation.build_automation( + var.get_intent_start_trigger(), + [], + config[CONF_ON_INTENT_START], + ) + + if CONF_ON_INTENT_END in config: + await automation.build_automation( + var.get_intent_end_trigger(), + [], + config[CONF_ON_INTENT_END], + ) + + if CONF_ON_STT_VAD_START in config: + await automation.build_automation( + var.get_stt_vad_start_trigger(), + [], + config[CONF_ON_STT_VAD_START], + ) + + if CONF_ON_STT_VAD_END in config: + await automation.build_automation( + var.get_stt_vad_end_trigger(), + [], + config[CONF_ON_STT_VAD_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fc5dd6e4e4..7ebbe762b3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,7 +31,7 @@ void VoiceAssistant::setup() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } @@ -69,7 +69,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate speaker buffer."); + ESP_LOGW(TAG, "Could not allocate speaker buffer"); this->mark_failed(); return; } @@ -79,7 +79,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); if (this->input_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate input buffer."); + ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); return; } @@ -89,7 +89,7 @@ void VoiceAssistant::setup() { this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate ring buffer."); + ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } @@ -98,7 +98,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate send buffer."); + ESP_LOGW(TAG, "Could not allocate send buffer"); this->mark_failed(); return; } @@ -221,8 +221,8 @@ void VoiceAssistant::loop() { msg.audio_settings = audio_settings; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -280,7 +280,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_size_ += len; } } else { - ESP_LOGW(TAG, "Receive buffer full."); + ESP_LOGW(TAG, "Receive buffer full"); } if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -290,7 +290,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_index_ -= written; this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { - ESP_LOGW(TAG, "Speaker buffer full."); + ESP_LOGW(TAG, "Speaker buffer full"); } } if (this->wait_for_stream_end_) { @@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_STT_START: - ESP_LOGD(TAG, "STT Started"); + ESP_LOGD(TAG, "STT started"); this->listening_trigger_->trigger(); break; case api::enums::VOICE_ASSISTANT_STT_END: { @@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->stt_end_trigger_->trigger(text); break; } + case api::enums::VOICE_ASSISTANT_INTENT_START: + ESP_LOGD(TAG, "Intent started"); + this->intent_start_trigger_->trigger(); + break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { this->conversation_id_ = std::move(arg.value); } } + this->intent_end_trigger_->trigger(); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); @@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); @@ -634,6 +639,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); break; } + case api::enums::VOICE_ASSISTANT_STT_VAD_START: + ESP_LOGD(TAG, "Starting STT by VAD"); + this->stt_vad_start_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_STT_VAD_END: + ESP_LOGD(TAG, "STT by VAD end"); + this->stt_vad_end_trigger_->trigger(); + break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a265522bca..a985bc4678 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -100,13 +100,17 @@ class VoiceAssistant : public Component { void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } + Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -124,13 +128,17 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); - Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); Trigger<> *client_connected_trigger_ = new Trigger<>(); From 10a9129b7b1cfa05e13dc55452a7dec087e566ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Nov 2023 01:41:49 -0600 Subject: [PATCH 220/270] Pass the name to the log runner when available (#5759) --- esphome/components/api/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 2c43eca70c..701848b1f1 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -18,9 +18,10 @@ from . import CONF_ENCRYPTION _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config, address): +async def async_run_logs(config: dict[str, Any], address: str) -> None: """Run the logs command in the event loop.""" conf = config["api"] + name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None @@ -28,7 +29,6 @@ async def async_run_logs(config, address): noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) aiozc = AsyncZeroconf() - cli = APIClient( address, port, @@ -48,7 +48,7 @@ async def async_run_logs(config, address): text = text.replace("\033", "\\033") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") - stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc) + stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name) try: while True: await asyncio.sleep(60) From 754bd5b7bedca31190edcb0b19638daceae180a9 Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:45:08 +0000 Subject: [PATCH 221/270] Fix MY9231 flicker (#5765) --- esphome/components/my9231/my9231.cpp | 26 +++++++++++++++++++++----- esphome/components/my9231/my9231.h | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace my9231 { @@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() { MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { @@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) { // Send 16 DI pulse. After 14 falling edges, the command data are // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { for (uint8_t i = bits; i > 0; i--) { @@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) { this->pin_di_->digital_write(false); } } +void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { + delayMicroseconds(12); + for (uint8_t i = 0; i < count; i++) { + this->pin_dcki_->digital_write(true); + this->pin_dcki_->digital_write(false); + } +} } // namespace my9231 } // namespace esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; From 4ac49907ca339345f934467ff73f56654b7afc49 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 15 Nov 2023 21:29:50 -0600 Subject: [PATCH 222/270] Add more VA triggers (#5762) --- .../components/voice_assistant/__init__.py | 61 ++++++++++++++++--- .../voice_assistant/voice_assistant.cpp | 39 ++++++++---- .../voice_assistant/voice_assistant.h | 16 +++-- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 3270b9f370..5715604605 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -18,20 +18,25 @@ DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] -CONF_SILENCE_DETECTION = "silence_detection" -CONF_ON_LISTENING = "on_listening" -CONF_ON_START = "on_start" -CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" -CONF_ON_STT_END = "on_stt_end" -CONF_ON_TTS_START = "on_tts_start" -CONF_ON_TTS_END = "on_tts_end" CONF_ON_END = "on_end" CONF_ON_ERROR = "on_error" +CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_START = "on_intent_start" +CONF_ON_LISTENING = "on_listening" +CONF_ON_START = "on_start" +CONF_ON_STT_END = "on_stt_end" +CONF_ON_STT_VAD_END = "on_stt_vad_end" +CONF_ON_STT_VAD_START = "on_stt_vad_start" +CONF_ON_TTS_END = "on_tts_end" +CONF_ON_TTS_START = "on_tts_start" +CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" + +CONF_SILENCE_DETECTION = "silence_detection" CONF_USE_WAKE_WORD = "use_wake_word" CONF_VAD_THRESHOLD = "vad_threshold" -CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_AUTO_GAIN = "auto_gain" +CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level" CONF_VOLUME_MULTIPLIER = "volume_multiplier" @@ -88,6 +93,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), ) @@ -177,6 +194,34 @@ async def to_code(config): config[CONF_ON_CLIENT_DISCONNECTED], ) + if CONF_ON_INTENT_START in config: + await automation.build_automation( + var.get_intent_start_trigger(), + [], + config[CONF_ON_INTENT_START], + ) + + if CONF_ON_INTENT_END in config: + await automation.build_automation( + var.get_intent_end_trigger(), + [], + config[CONF_ON_INTENT_END], + ) + + if CONF_ON_STT_VAD_START in config: + await automation.build_automation( + var.get_stt_vad_start_trigger(), + [], + config[CONF_ON_STT_VAD_START], + ) + + if CONF_ON_STT_VAD_END in config: + await automation.build_automation( + var.get_stt_vad_end_trigger(), + [], + config[CONF_ON_STT_VAD_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fc5dd6e4e4..7ebbe762b3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -31,7 +31,7 @@ void VoiceAssistant::setup() { this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket."); + ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } @@ -69,7 +69,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate speaker buffer."); + ESP_LOGW(TAG, "Could not allocate speaker buffer"); this->mark_failed(); return; } @@ -79,7 +79,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE); if (this->input_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate input buffer."); + ESP_LOGW(TAG, "Could not allocate input buffer"); this->mark_failed(); return; } @@ -89,7 +89,7 @@ void VoiceAssistant::setup() { this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t)); if (this->ring_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate ring buffer."); + ESP_LOGW(TAG, "Could not allocate ring buffer"); this->mark_failed(); return; } @@ -98,7 +98,7 @@ void VoiceAssistant::setup() { ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { - ESP_LOGW(TAG, "Could not allocate send buffer."); + ESP_LOGW(TAG, "Could not allocate send buffer"); this->mark_failed(); return; } @@ -221,8 +221,8 @@ void VoiceAssistant::loop() { msg.audio_settings = audio_settings; if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) { - ESP_LOGW(TAG, "Could not request start."); - this->error_trigger_->trigger("not-connected", "Could not request start."); + ESP_LOGW(TAG, "Could not request start"); + this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -280,7 +280,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_size_ += len; } } else { - ESP_LOGW(TAG, "Receive buffer full."); + ESP_LOGW(TAG, "Receive buffer full"); } if (this->speaker_buffer_size_ > 0) { size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_); @@ -290,7 +290,7 @@ void VoiceAssistant::loop() { this->speaker_buffer_index_ -= written; this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); }); } else { - ESP_LOGW(TAG, "Speaker buffer full."); + ESP_LOGW(TAG, "Speaker buffer full"); } } if (this->wait_for_stream_end_) { @@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_STT_START: - ESP_LOGD(TAG, "STT Started"); + ESP_LOGD(TAG, "STT started"); this->listening_trigger_->trigger(); break; case api::enums::VOICE_ASSISTANT_STT_END: { @@ -525,19 +525,24 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in STT_END event."); + ESP_LOGW(TAG, "No text in STT_END event"); return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->stt_end_trigger_->trigger(text); break; } + case api::enums::VOICE_ASSISTANT_INTENT_START: + ESP_LOGD(TAG, "Intent started"); + this->intent_start_trigger_->trigger(); + break; case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { this->conversation_id_ = std::move(arg.value); } } + this->intent_end_trigger_->trigger(); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -548,7 +553,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (text.empty()) { - ESP_LOGW(TAG, "No text in TTS_START event."); + ESP_LOGW(TAG, "No text in TTS_START event"); return; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); @@ -566,7 +571,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } if (url.empty()) { - ESP_LOGW(TAG, "No url in TTS_END event."); + ESP_LOGW(TAG, "No url in TTS_END event"); return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); @@ -634,6 +639,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); break; } + case api::enums::VOICE_ASSISTANT_STT_VAD_START: + ESP_LOGD(TAG, "Starting STT by VAD"); + this->stt_vad_start_trigger_->trigger(); + break; + case api::enums::VOICE_ASSISTANT_STT_VAD_END: + ESP_LOGD(TAG, "STT by VAD end"); + this->stt_vad_end_trigger_->trigger(); + break; default: ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type); break; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a265522bca..a985bc4678 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -100,13 +100,17 @@ class VoiceAssistant : public Component { void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } + Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } + Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } Trigger *get_error_trigger() const { return this->error_trigger_; } Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -124,13 +128,17 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *intent_end_trigger_ = new Trigger<>(); + Trigger<> *intent_start_trigger_ = new Trigger<>(); Trigger<> *listening_trigger_ = new Trigger<>(); + Trigger<> *end_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); + Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); - Trigger<> *end_trigger_ = new Trigger<>(); + Trigger *tts_start_trigger_ = new Trigger(); Trigger *error_trigger_ = new Trigger(); Trigger<> *client_connected_trigger_ = new Trigger<>(); From 255483de63f9c1fd846cbbdaae98be96df41d6cb Mon Sep 17 00:00:00 2001 From: Mat931 <49403702+Mat931@users.noreply.github.com> Date: Thu, 16 Nov 2023 07:45:08 +0000 Subject: [PATCH 223/270] Fix MY9231 flicker (#5765) --- esphome/components/my9231/my9231.cpp | 26 +++++++++++++++++++++----- esphome/components/my9231/my9231.h | 1 + 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index a97587b7be..c511591856 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -1,5 +1,6 @@ #include "my9231.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace my9231 { @@ -51,7 +52,11 @@ void MY9231OutputComponent::setup() { MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE; ESP_LOGV(TAG, " Command: 0x%02X", command); - this->init_chips_(command); + { + InterruptLock lock; + this->send_dcki_pulses_(32 * this->num_chips_); + this->init_chips_(command); + } ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { @@ -66,11 +71,14 @@ void MY9231OutputComponent::loop() { if (!this->update_) return; - for (auto pwm_amount : this->pwm_amounts_) { - this->write_word_(pwm_amount, this->bit_depth_); + { + InterruptLock lock; + for (auto pwm_amount : this->pwm_amounts_) { + this->write_word_(pwm_amount, this->bit_depth_); + } + // Send 8 DI pulses. After 8 falling edges, the duty data are store. + this->send_di_pulses_(8); } - // Send 8 DI pulses. After 8 falling edges, the duty data are store. - this->send_di_pulses_(8); this->update_ = false; } void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) { @@ -92,6 +100,7 @@ void MY9231OutputComponent::init_chips_(uint8_t command) { // Send 16 DI pulse. After 14 falling edges, the command data are // stored and after 16 falling edges the duty mode is activated. this->send_di_pulses_(16); + delayMicroseconds(12); } void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) { for (uint8_t i = bits; i > 0; i--) { @@ -106,6 +115,13 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) { this->pin_di_->digital_write(false); } } +void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) { + delayMicroseconds(12); + for (uint8_t i = 0; i < count; i++) { + this->pin_dcki_->digital_write(true); + this->pin_dcki_->digital_write(false); + } +} } // namespace my9231 } // namespace esphome diff --git a/esphome/components/my9231/my9231.h b/esphome/components/my9231/my9231.h index a777dcc960..77c1259853 100644 --- a/esphome/components/my9231/my9231.h +++ b/esphome/components/my9231/my9231.h @@ -49,6 +49,7 @@ class MY9231OutputComponent : public Component { void init_chips_(uint8_t command); void write_word_(uint16_t value, uint8_t bits); void send_di_pulses_(uint8_t count); + void send_dcki_pulses_(uint8_t count); GPIOPin *pin_di_; GPIOPin *pin_dcki_; From 445b13dbc6330df2361e06fb9499ae31c6a1153b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:55:28 +1300 Subject: [PATCH 224/270] Bump version to 2023.11.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f937ecf068..7ceaffbe57 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.0" +__version__ = "2023.11.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fefdb80fdc941d583aba45b947bd79ae72ee6fa7 Mon Sep 17 00:00:00 2001 From: Nikita Kuklev Date: Thu, 16 Nov 2023 02:06:03 -0600 Subject: [PATCH 225/270] Add proper support for SH1107 to SSD1306 component (#5166) --- esphome/components/ssd1306_base/__init__.py | 7 +- .../components/ssd1306_base/ssd1306_base.cpp | 103 ++++++++++++------ .../components/ssd1306_base/ssd1306_base.h | 2 + .../components/ssd1306_i2c/ssd1306_i2c.cpp | 14 ++- .../components/ssd1306_spi/ssd1306_spi.cpp | 8 +- 5 files changed, 95 insertions(+), 39 deletions(-) diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index f4abd845c8..55239dfcb8 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -33,6 +33,7 @@ MODELS = { "SH1106_96X16": SSD1306Model.SH1106_MODEL_96_16, "SH1106_64X48": SSD1306Model.SH1106_MODEL_64_48, "SH1107_128X64": SSD1306Model.SH1107_MODEL_128_64, + "SH1107_128X128": SSD1306Model.SH1107_MODEL_128_128, "SSD1305_128X32": SSD1306Model.SSD1305_MODEL_128_32, "SSD1305_128X64": SSD1306Model.SSD1305_MODEL_128_64, } @@ -63,8 +64,10 @@ SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend( cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, cv.Optional(CONF_FLIP_X, default=True): cv.boolean, cv.Optional(CONF_FLIP_Y, default=True): cv.boolean, - cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=-32, max=32), - cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=-32, max=32), + # Offsets determine shifts of memory location to LCD rows/columns, + # and this family of controllers supports up to 128x128 screens + cv.Optional(CONF_OFFSET_X, default=0): cv.int_range(min=0, max=128), + cv.Optional(CONF_OFFSET_Y, default=0): cv.int_range(min=0, max=128), cv.Optional(CONF_INVERT, default=False): cv.boolean, } ).extend(cv.polling_component_schema("1s")) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 3cacd473d1..00b5c2d5a2 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -35,16 +35,31 @@ static const uint8_t SSD1306_COMMAND_INVERSE_DISPLAY = 0xA7; static const uint8_t SSD1305_COMMAND_SET_BRIGHTNESS = 0x82; static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8; +static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC; +static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD; + void SSD1306::setup() { this->init_internal_(this->get_buffer_length_()); + // SH1107 resources + // + // Datasheet v2.3: + // www.displayfuture.com/Display/datasheet/controller/SH1107.pdf + // Adafruit C++ driver: + // github.com/adafruit/Adafruit_SH110x + // Adafruit CircuitPython driver: + // github.com/adafruit/Adafruit_CircuitPython_DisplayIO_SH1107 + // Turn off display during initialization (0xAE) this->command(SSD1306_COMMAND_DISPLAY_OFF); - // Set oscillator frequency to 4'b1000 with no clock division (0xD5) - this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); - // Oscillator frequency <= 4'b1000, no clock division - this->command(0x80); + // If SH1107, use POR defaults (0x50) = divider 1, frequency +0% + if (!this->is_sh1107_()) { + // Set oscillator frequency to 4'b1000 with no clock division (0xD5) + this->command(SSD1306_COMMAND_SET_DISPLAY_CLOCK_DIV); + // Oscillator frequency <= 4'b1000, no clock division + this->command(0x80); + } // Enable low power display mode for SSD1305 (0xD8) if (this->is_ssd1305_()) { @@ -60,11 +75,26 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_DISPLAY_OFFSET_Y); this->command(0x00 + this->offset_y_); - // Set start line at line 0 (0x40) - this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + if (this->is_sh1107_()) { + // Set start line at line 0 (0xDC) + this->command(SH1107_COMMAND_SET_START_LINE); + this->command(0x00); + } else { + // Set start line at line 0 (0x40) + this->command(SSD1306_COMMAND_SET_START_LINE | 0x00); + } - // SSD1305 does not have charge pump - if (!this->is_ssd1305_()) { + if (this->is_ssd1305_()) { + // SSD1305 does not have charge pump + } else if (this->is_sh1107_()) { + // Enable charge pump (0xAD) + this->command(SH1107_COMMAND_CHARGE_PUMP); + if (this->external_vcc_) { + this->command(0x8A); + } else { + this->command(0x8B); + } + } else { // Enable charge pump (0x8D) this->command(SSD1306_COMMAND_CHARGE_PUMP); if (this->external_vcc_) { @@ -76,34 +106,41 @@ void SSD1306::setup() { // Set addressing mode to horizontal (0x20) this->command(SSD1306_COMMAND_MEMORY_MODE); - this->command(0x00); - + if (!this->is_sh1107_()) { + // SH1107 memory mode is a 1 byte command + this->command(0x00); + } // X flip mode (0xA0, 0xA1) this->command(SSD1306_COMMAND_SEGRE_MAP | this->flip_x_); // Y flip mode (0xC0, 0xC8) this->command(SSD1306_COMMAND_COM_SCAN_INC | (this->flip_y_ << 3)); - // Set pin configuration (0xDA) - this->command(SSD1306_COMMAND_SET_COM_PINS); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - this->command(0x02); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SSD1306_MODEL_64_32: - case SH1106_MODEL_64_48: - case SH1107_MODEL_128_64: - case SSD1305_MODEL_128_32: - case SSD1305_MODEL_128_64: - case SSD1306_MODEL_72_40: - this->command(0x12); - break; + if (!this->is_sh1107_()) { + // Set pin configuration (0xDA) + this->command(SSD1306_COMMAND_SET_COM_PINS); + switch (this->model_) { + case SSD1306_MODEL_128_32: + case SH1106_MODEL_128_32: + case SSD1306_MODEL_96_16: + case SH1106_MODEL_96_16: + this->command(0x02); + break; + case SSD1306_MODEL_128_64: + case SH1106_MODEL_128_64: + case SSD1306_MODEL_64_48: + case SSD1306_MODEL_64_32: + case SH1106_MODEL_64_48: + case SSD1305_MODEL_128_32: + case SSD1305_MODEL_128_64: + case SSD1306_MODEL_72_40: + this->command(0x12); + break; + case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: + // Not used, but prevents build warning + break; + } } // Pre-charge period (0xD9) @@ -118,6 +155,7 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: this->command(0x35); break; case SSD1306_MODEL_72_40: @@ -149,7 +187,7 @@ void SSD1306::setup() { this->turn_on(); } void SSD1306::display() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { this->write_display_data(); return; } @@ -183,6 +221,7 @@ bool SSD1306::is_sh1106_() const { return this->model_ == SH1106_MODEL_96_16 || this->model_ == SH1106_MODEL_128_32 || this->model_ == SH1106_MODEL_128_64; } +bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; } bool SSD1306::is_ssd1305_() const { return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; } @@ -224,6 +263,7 @@ void SSD1306::turn_off() { int SSD1306::get_height_internal() { switch (this->model_) { case SH1107_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_128_32: case SSD1306_MODEL_64_32: @@ -254,6 +294,7 @@ int SSD1306::get_width_internal() { case SH1106_MODEL_128_64: case SSD1305_MODEL_128_32: case SSD1305_MODEL_128_64: + case SH1107_MODEL_128_128: return 128; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 4b0e9bb80e..34b76d284d 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -19,6 +19,7 @@ enum SSD1306Model { SH1106_MODEL_96_16, SH1106_MODEL_64_48, SH1107_MODEL_128_64, + SH1107_MODEL_128_128, SSD1305_MODEL_128_32, SSD1305_MODEL_128_64, }; @@ -58,6 +59,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void init_reset_(); bool is_sh1106_() const; + bool is_sh1107_() const; bool is_ssd1305_() const; void draw_absolute_pixel_internal(int x, int y, Color color) override; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 96734eb618..ed7cf102ee 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -38,13 +38,19 @@ void I2CSSD1306::dump_config() { } void I2CSSD1306::command(uint8_t value) { this->write_byte(0x00, value); } void HOT I2CSSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { uint32_t i = 0; for (uint8_t page = 0; page < (uint8_t) this->get_height_internal() / 8; page++) { this->command(0xB0 + page); // row - this->command(0x02); // lower column - this->command(0x10); // higher column - + if (this->is_sh1106_()) { + this->command(0x02); // lower column - 0x02 is historical SH1106 value + } else { + // Other SH1107 drivers use 0x00 + // Column values dont change and it seems they can be set only once, + // but we follow SH1106 implementation and resend them + this->command(0x00); + } + this->command(0x10); // higher column for (uint8_t x = 0; x < (uint8_t) this->get_width_internal() / 16; x++) { uint8_t data[16]; for (uint8_t &j : data) diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 7f025d77cd..0a0debfd65 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -36,10 +36,14 @@ void SPISSD1306::command(uint8_t value) { this->disable(); } void HOT SPISSD1306::write_display_data() { - if (this->is_sh1106_()) { + if (this->is_sh1106_() || this->is_sh1107_()) { for (uint8_t y = 0; y < (uint8_t) this->get_height_internal() / 8; y++) { this->command(0xB0 + y); - this->command(0x02); + if (this->is_sh1106_()) { + this->command(0x02); + } else { + this->command(0x00); + } this->command(0x10); this->dc_pin_->digital_write(true); for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x++) { From 208edf89dc1dd458eb55ca6ca3f976bc231b7d4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:06:16 +1300 Subject: [PATCH 226/270] Split release workflow jobs per system arch (#5723) --- .github/actions/build-image/action.yaml | 97 ++++++++++++++++++ .github/workflows/release.yml | 130 +++++++++++++++++------- docker/generate_tags.py | 54 +++++++--- 3 files changed, 232 insertions(+), 49 deletions(-) create mode 100644 .github/actions/build-image/action.yaml diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml new file mode 100644 index 0000000000..4c98a47ecd --- /dev/null +++ b/.github/actions/build-image/action.yaml @@ -0,0 +1,97 @@ +name: Build Image +inputs: + platform: + description: "Platform to build for" + required: true + example: "linux/amd64" + target: + description: "Target to build" + required: true + example: "docker" + baseimg: + description: "Base image type" + required: true + example: "docker" + suffix: + description: "Suffix to add to tags" + required: true + version: + description: "Version to build" + required: true + example: "2023.12.0" +runs: + using: "composite" + steps: + - name: Generate short tags + id: tags + shell: bash + run: | + output=$(docker/generate_tags.py \ + --tag "${{ inputs.version }}" \ + --suffix "${{ inputs.suffix }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done + + - name: Build and push to ghcr by digest + id: build-ghcr + uses: docker/build-push-action@v5.0.0 + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export ghcr digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/ghcr + digest="${{ steps.build-ghcr.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" + + - name: Upload ghcr digest + uses: actions/upload-artifact@v3.1.3 + with: + name: digests-${{ inputs.target }}-ghcr + path: /tmp/digests/${{ inputs.target }}/ghcr/* + if-no-files-found: error + retention-days: 1 + + - name: Build and push to dockerhub by digest + id: build-dockerhub + uses: docker/build-push-action@v5.0.0 + with: + context: . + file: ./docker/Dockerfile + platforms: ${{ inputs.platform }} + target: ${{ inputs.target }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BASEIMGTYPE=${{ inputs.baseimg }} + BUILD_VERSION=${{ inputs.version }} + outputs: | + type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true + + - name: Export dockerhub digests + shell: bash + run: | + mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub + digest="${{ steps.build-dockerhub.outputs.digest }}" + touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" + + - name: Upload dockerhub digest + uses: actions/upload-artifact@v3.1.3 + with: + name: digests-${{ inputs.target }}-dockerhub + path: /tmp/digests/${{ inputs.target }}/dockerhub/* + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14dbeee7b7..0e23db521a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,30 +63,20 @@ jobs: run: twine upload dist/* deploy-docker: - name: Build and publish ESPHome ${{ matrix.image.title}} + name: Build ESPHome ${{ matrix.platform }} if: github.repository == 'esphome/esphome' permissions: contents: read packages: write runs-on: ubuntu-latest - continue-on-error: ${{ matrix.image.title == 'lint' }} needs: [init] strategy: fail-fast: false matrix: - image: - - title: "ha-addon" - suffix: "hassio" - target: "hassio" - baseimg: "hassio" - - title: "docker" - suffix: "" - target: "docker" - baseimg: "docker" - - title: "lint" - suffix: "lint" - target: "lint" - baseimg: "docker" + platform: + - linux/amd64 + - linux/arm/v7 + - linux/arm64 steps: - uses: actions/checkout@v4.1.1 - name: Set up Python @@ -97,6 +87,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.0.0 - name: Set up QEMU + if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.0.0 - name: Log in to docker hub @@ -111,34 +102,105 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build docker + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: docker + baseimg: docker + suffix: "" + version: ${{ needs.init.outputs.tag }} + + - name: Build ha-addon + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: hassio + baseimg: hassio + suffix: "hassio" + version: ${{ needs.init.outputs.tag }} + + - name: Build lint + uses: ./.github/actions/build-image + with: + platform: ${{ matrix.platform }} + target: lint + baseimg: docker + suffix: lint + version: ${{ needs.init.outputs.tag }} + + deploy-manifest: + name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} + runs-on: ubuntu-latest + needs: + - init + - deploy-docker + if: github.repository == 'esphome/esphome' + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + image: + - title: "ha-addon" + target: "hassio" + suffix: "hassio" + - title: "docker" + target: "docker" + suffix: "" + - title: "lint" + target: "lint" + suffix: "lint" + registry: + - ghcr + - dockerhub + steps: + - uses: actions/checkout@v4.1.1 + - name: Download digests + uses: actions/download-artifact@v3.0.2 + with: + name: digests-${{ matrix.image.target }}-${{ matrix.registry }} + path: /tmp/digests + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Log in to docker hub + if: matrix.registry == 'dockerhub' + uses: docker/login-action@v3.0.0 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + if: matrix.registry == 'ghcr' + uses: docker/login-action@v3.0.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Generate short tags id: tags run: | - docker/generate_tags.py \ + output=$(docker/generate_tags.py \ --tag "${{ needs.init.outputs.tag }}" \ - --suffix "${{ matrix.image.suffix }}" + --suffix "${{ matrix.image.suffix }}" \ + --registry "${{ matrix.registry }}") + echo $output + for l in $output; do + echo $l >> $GITHUB_OUTPUT + done - - name: Build and push - uses: docker/build-push-action@v5.0.0 - with: - context: . - file: ./docker/Dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64 - target: ${{ matrix.image.target }} - push: true - # yamllint disable rule:line-length - cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} - cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max - # yamllint enable rule:line-length - tags: ${{ steps.tags.outputs.tags }} - build-args: | - BASEIMGTYPE=${{ matrix.image.baseimg }} - BUILD_VERSION=${{ needs.init.outputs.tag }} + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ + $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) deploy-ha-addon-repo: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: [deploy-manifest] steps: - name: Trigger Workflow uses: actions/github-script@v6.4.1 diff --git a/docker/generate_tags.py b/docker/generate_tags.py index 71d0735526..3fc787d485 100755 --- a/docker/generate_tags.py +++ b/docker/generate_tags.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 import re -import os import argparse -import json CHANNEL_DEV = "dev" CHANNEL_BETA = "beta" CHANNEL_RELEASE = "release" +GHCR = "ghcr" +DOCKERHUB = "dockerhub" + parser = argparse.ArgumentParser() parser.add_argument( "--tag", @@ -21,21 +22,31 @@ parser.add_argument( required=True, help="The suffix of the tag.", ) +parser.add_argument( + "--registry", + type=str, + choices=[GHCR, DOCKERHUB], + required=False, + action="append", + help="The registry to build tags for.", +) def main(): args = parser.parse_args() # detect channel from tag - match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) + match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag) major_minor_version = None - if match is None: + if match is None: # eg 2023.12.0-dev20231109-testbranch + channel = None # Ran with custom tag for a branch etc + elif match.group(3) is not None: # eg 2023.12.0-dev20231109 channel = CHANNEL_DEV - elif match.group(2) is None: + elif match.group(2) is not None: # eg 2023.12.0b1 + channel = CHANNEL_BETA + else: # eg 2023.12.0 major_minor_version = match.group(1) channel = CHANNEL_RELEASE - else: - channel = CHANNEL_BETA tags_to_push = [args.tag] if channel == CHANNEL_DEV: @@ -53,15 +64,28 @@ def main(): suffix = f"-{args.suffix}" if args.suffix else "" - with open(os.environ["GITHUB_OUTPUT"], "w") as f: - print(f"channel={channel}", file=f) - print(f"image=esphome/esphome{suffix}", file=f) - full_tags = [] + image_name = f"esphome/esphome{suffix}" - for tag in tags_to_push: - full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] - full_tags += [f"esphome/esphome{suffix}:{tag}"] - print(f"tags={','.join(full_tags)}", file=f) + print(f"channel={channel}") + + if args.registry is None: + args.registry = [GHCR, DOCKERHUB] + elif len(args.registry) == 1: + if GHCR in args.registry: + print(f"image=ghcr.io/{image_name}") + if DOCKERHUB in args.registry: + print(f"image=docker.io/{image_name}") + + print(f"image_name={image_name}") + + full_tags = [] + + for tag in tags_to_push: + if GHCR in args.registry: + full_tags += [f"ghcr.io/{image_name}:{tag}"] + if DOCKERHUB in args.registry: + full_tags += [f"docker.io/{image_name}:{tag}"] + print(f"tags={','.join(full_tags)}") if __name__ == "__main__": From 5464368c081fbc369d7db67548ca8e7faf053931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:35:42 -0600 Subject: [PATCH 227/270] Bump aioesphomeapi from 18.4.1 to 18.5.2 (#5780) 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 9afe7064c2..3ac0a1f937 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.1 +aioesphomeapi==18.5.2 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From 32e3f2623973f45bd37b6b0dc956adcd82a45e77 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 17 Nov 2023 01:16:03 -0800 Subject: [PATCH 228/270] fix 32-bit arm (#5781) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7ca633a982..a892e1df38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,7 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ fi RUN \ From 6f8d7c6acde30483e9c9508d72697a9f22400c7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 17:48:53 -0600 Subject: [PATCH 229/270] Bump aioesphomeapi to 18.5.3 (#5785) - Avoids creating a zeroconf instance when we do not need one supports https://github.com/esphome/esphome/pull/5783 changelog: https://github.com/esphome/aioesphomeapi/compare/v18.5.2...v18.5.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3ac0a1f937..1866d33ab2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.2 +aioesphomeapi==18.5.3 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From 288af1f4d2f6bf57d1701ef483b502691758a73a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 17:50:40 -0600 Subject: [PATCH 230/270] Refactor log api client to let aioesphomeapi manage zeroconf (#5783) aioesphomeapi is now smart enough to avoid creating a zeroconf instance until its needed after https://github.com/esphome/aioesphomeapi/pull/643 This avoids the needs to have a background zeroconf instance running that is processing incoming records but will never do anything --- esphome/components/api/client.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 701848b1f1..dd013c8c34 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -8,7 +8,6 @@ from typing import Any from aioesphomeapi import APIClient from aioesphomeapi.api_pb2 import SubscribeLogsResponse from aioesphomeapi.log_runner import async_run -from zeroconf.asyncio import AsyncZeroconf from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE @@ -28,14 +27,12 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) - aiozc = AsyncZeroconf() cli = APIClient( address, port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, - zeroconf_instance=aiozc.zeroconf, ) dashboard = CORE.dashboard @@ -48,12 +45,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: text = text.replace("\033", "\\033") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") - stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name) + stop = await async_run(cli, on_log, name=name) try: - while True: - await asyncio.sleep(60) + await asyncio.Event().wait() finally: - await aiozc.async_close() await stop() From 3c243e663f18723570cf313d502da85fec2a063e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Nov 2023 18:33:10 -0600 Subject: [PATCH 231/270] dashboard: Add support for firing events (#5775) * dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * tweaks * fixes * remove typing_extensions * rename for asyncio * rename for asyncio * rename for asyncio * preen * lint * lint * move dict converter * lint --- esphome/dashboard/const.py | 8 ++ esphome/dashboard/core.py | 49 ++++++++++- esphome/dashboard/entries.py | 141 +++++++++++++++++++++++++++---- esphome/dashboard/enum.py | 19 +++++ esphome/dashboard/status/mdns.py | 48 ++++++----- esphome/dashboard/status/mqtt.py | 17 ++-- esphome/dashboard/status/ping.py | 10 +-- esphome/dashboard/web_server.py | 37 ++++---- 8 files changed, 251 insertions(+), 78 deletions(-) create mode 100644 esphome/dashboard/const.py create mode 100644 esphome/dashboard/enum.py diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py new file mode 100644 index 0000000000..ed2b81d3e8 --- /dev/null +++ b/esphome/dashboard/const.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +EVENT_ENTRY_ADDED = "entry_added" +EVENT_ENTRY_REMOVED = "entry_removed" +EVENT_ENTRY_UPDATED = "entry_updated" +EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" + +SENTINEL = object() diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index f18da92d80..ffec9784e8 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -3,7 +3,9 @@ from __future__ import annotations import asyncio import logging import threading -from typing import TYPE_CHECKING +from dataclasses import dataclass +from functools import partial +from typing import TYPE_CHECKING, Any, Callable from ..zeroconf import DiscoveredImport from .entries import DashboardEntries @@ -12,16 +14,55 @@ from .settings import DashboardSettings if TYPE_CHECKING: from .status.mdns import MDNSStatus + _LOGGER = logging.getLogger(__name__) +@dataclass +class Event: + """Dashboard Event.""" + + event_type: str + data: dict[str, Any] + + +class EventBus: + """Dashboard event bus.""" + + def __init__(self) -> None: + """Initialize the Dashboard event bus.""" + self._listeners: dict[str, set[Callable[[Event], None]]] = {} + + def async_add_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> Callable[[], None]: + """Add a listener to the event bus.""" + self._listeners.setdefault(event_type, set()).add(listener) + return partial(self._async_remove_listener, event_type, listener) + + def _async_remove_listener( + self, event_type: str, listener: Callable[[Event], None] + ) -> None: + """Remove a listener from the event bus.""" + self._listeners[event_type].discard(listener) + + def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None: + """Fire an event.""" + event = Event(event_type, event_data) + + _LOGGER.debug("Firing event: %s", event) + + for listener in self._listeners.get(event_type, set()): + listener(event) + + class ESPHomeDashboard: """Class that represents the dashboard.""" __slots__ = ( + "bus", "entries", "loop", - "ping_result", "import_result", "stop_event", "ping_request", @@ -32,9 +73,9 @@ class ESPHomeDashboard: def __init__(self) -> None: """Initialize the ESPHomeDashboard.""" + self.bus = EventBus() self.entries: DashboardEntries | None = None self.loop: asyncio.AbstractEventLoop | None = None - self.ping_result: dict[str, bool | None] = {} self.import_result: dict[str, DiscoveredImport] = {} self.stop_event = threading.Event() self.ping_request: asyncio.Event | None = None @@ -46,7 +87,7 @@ class ESPHomeDashboard: """Setup the dashboard.""" self.loop = asyncio.get_running_loop() self.ping_request = asyncio.Event() - self.entries = DashboardEntries(self.settings.config_dir) + self.entries = DashboardEntries(self) async def async_run(self) -> None: """Run the dashboard.""" diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index ff539fc620..42b3a2e743 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -3,24 +3,78 @@ from __future__ import annotations import asyncio import logging import os +from typing import TYPE_CHECKING, Any from esphome import const, util from esphome.storage_json import StorageJSON, ext_storage_path +from .const import ( + EVENT_ENTRY_ADDED, + EVENT_ENTRY_REMOVED, + EVENT_ENTRY_STATE_CHANGED, + EVENT_ENTRY_UPDATED, +) +from .enum import StrEnum + +if TYPE_CHECKING: + from .core import ESPHomeDashboard + _LOGGER = logging.getLogger(__name__) + DashboardCacheKeyType = tuple[int, int, float, int] +# Currently EntryState is a simple +# online/offline/unknown enum, but in the future +# it may be expanded to include more states + + +class EntryState(StrEnum): + ONLINE = "online" + OFFLINE = "offline" + UNKNOWN = "unknown" + + +_BOOL_TO_ENTRY_STATE = { + True: EntryState.ONLINE, + False: EntryState.OFFLINE, + None: EntryState.UNKNOWN, +} +_ENTRY_STATE_TO_BOOL = { + EntryState.ONLINE: True, + EntryState.OFFLINE: False, + EntryState.UNKNOWN: None, +} + + +def bool_to_entry_state(value: bool) -> EntryState: + """Convert a bool to an entry state.""" + return _BOOL_TO_ENTRY_STATE[value] + + +def entry_state_to_bool(value: EntryState) -> bool | None: + """Convert an entry state to a bool.""" + return _ENTRY_STATE_TO_BOOL[value] + class DashboardEntries: """Represents all dashboard entries.""" - __slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock") + __slots__ = ( + "_dashboard", + "_loop", + "_config_dir", + "_entries", + "_entry_states", + "_loaded_entries", + "_update_lock", + ) - def __init__(self, config_dir: str) -> None: + def __init__(self, dashboard: ESPHomeDashboard) -> None: """Initialize the DashboardEntries.""" + self._dashboard = dashboard self._loop = asyncio.get_running_loop() - self._config_dir = config_dir + self._config_dir = dashboard.settings.config_dir # Entries are stored as # { # "path/to/file.yaml": DashboardEntry, @@ -46,6 +100,25 @@ class DashboardEntries: """Return all entries.""" return list(self._entries.values()) + def set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + asyncio.run_coroutine_threadsafe( + self._async_set_state(entry, state), self._loop + ).result() + + async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + self.async_set_state(entry, state) + + def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None: + """Set the state for an entry.""" + if entry.state == state: + return + entry.state = state + self._dashboard.bus.async_fire( + EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state} + ) + async def async_request_update_entries(self) -> None: """Request an update of the dashboard entries from disk. @@ -81,16 +154,17 @@ class DashboardEntries: path_to_cache_key = await self._loop.run_in_executor( None, self._get_path_to_cache_key ) + entries = self._entries added: dict[DashboardEntry, DashboardCacheKeyType] = {} updated: dict[DashboardEntry, DashboardCacheKeyType] = {} removed: set[DashboardEntry] = { entry - for filename, entry in self._entries.items() + for filename, entry in entries.items() if filename not in path_to_cache_key } - entries = self._entries + for path, cache_key in path_to_cache_key.items(): - if entry := self._entries.get(path): + if entry := entries.get(path): if entry.cache_key != cache_key: updated[entry] = cache_key else: @@ -102,17 +176,17 @@ class DashboardEntries: None, self._load_entries, {**added, **updated} ) + bus = self._dashboard.bus for entry in added: - _LOGGER.debug("Added dashboard entry %s", entry.path) entries[entry.path] = entry + bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) - if entry in removed: - _LOGGER.debug("Removed dashboard entry %s", entry.path) - entries.pop(entry.path) + for entry in removed: + del entries[entry.path] + bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) for entry in updated: - _LOGGER.debug("Updated dashboard entry %s", entry.path) - # In the future we can fire events when entries are added/removed/updated + bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: """Return a dict of path to cache key.""" @@ -152,29 +226,64 @@ class DashboardEntry: This class is thread-safe and read-only. """ - __slots__ = ("path", "filename", "_storage_path", "cache_key", "storage") + __slots__ = ( + "path", + "filename", + "_storage_path", + "cache_key", + "storage", + "state", + "_to_dict", + ) def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: """Initialize the DashboardEntry.""" self.path = path - self.filename = os.path.basename(path) + self.filename: str = os.path.basename(path) self._storage_path = ext_storage_path(self.filename) self.cache_key = cache_key self.storage: StorageJSON | None = None + self.state = EntryState.UNKNOWN + self._to_dict: dict[str, Any] | None = None def __repr__(self): """Return the representation of this entry.""" return ( - f"DashboardEntry({self.path} " + f"DashboardEntry(path={self.path} " f"address={self.address} " f"web_port={self.web_port} " f"name={self.name} " - f"no_mdns={self.no_mdns})" + f"no_mdns={self.no_mdns} " + f"state={self.state} " + ")" ) + def to_dict(self) -> dict[str, Any]: + """Return a dict representation of this entry. + + The dict includes the loaded configuration but not + the current state of the entry. + """ + if self._to_dict is None: + self._to_dict = { + "name": self.name, + "friendly_name": self.friendly_name, + "configuration": self.filename, + "loaded_integrations": self.loaded_integrations, + "deployed_version": self.update_old, + "current_version": self.update_new, + "path": self.path, + "comment": self.comment, + "address": self.address, + "web_port": self.web_port, + "target_platform": self.target_platform, + } + return self._to_dict + def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: """Load this entry from disk.""" self.storage = StorageJSON.load(self._storage_path) + self._to_dict = None # # Currently StorageJSON.load() will return None if the file does not exist # diff --git a/esphome/dashboard/enum.py b/esphome/dashboard/enum.py new file mode 100644 index 0000000000..6aff21620e --- /dev/null +++ b/esphome/dashboard/enum.py @@ -0,0 +1,19 @@ +"""Enum backports from standard lib.""" +from __future__ import annotations + +from enum import Enum +from typing import Any + + +class StrEnum(str, Enum): + """Partial backport of Python 3.11's StrEnum for our basic use cases.""" + + def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: + """Create a new StrEnum instance.""" + if not isinstance(value, str): + raise TypeError(f"{value!r} is not a string") + return super().__new__(cls, value, *args, **kwargs) + + def __str__(self) -> str: + """Return self.value.""" + return str(self.value) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 51d11390b7..cbe3b3309e 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -10,7 +10,9 @@ from esphome.zeroconf import ( DashboardStatus, ) +from ..const import SENTINEL from ..core import DASHBOARD +from ..entries import bool_to_entry_state class MDNSStatus: @@ -22,16 +24,16 @@ class MDNSStatus: self.aiozc: AsyncEsphomeZeroconf | None = None # This is the current mdns state for each host (True, False, None) self.host_mdns_state: dict[str, bool | None] = {} - # This is the hostnames to filenames mapping - self.host_name_to_filename: dict[str, str] = {} - self.filename_to_host_name: dict[str, str] = {} + # This is the hostnames to path mapping + self.host_name_to_path: dict[str, str] = {} + self.path_to_host_name: dict[str, str] = {} # This is a set of host names to track (i.e no_mdns = false) self.host_name_with_mdns_enabled: set[set] = set() self._loop = asyncio.get_running_loop() - def filename_to_host_name_thread_safe(self, filename: str) -> str | None: - """Resolve a filename to an address in a thread-safe manner.""" - return self.filename_to_host_name.get(filename) + def get_path_to_host_name(self, path: str) -> str | None: + """Resolve a path to an address in a thread-safe manner.""" + return self.path_to_host_name.get(path) async def async_resolve_host(self, host_name: str) -> str | None: """Resolve a host name to an address in a thread-safe manner.""" @@ -42,14 +44,14 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" dashboard = DASHBOARD - entries = dashboard.entries.async_all() + current_entries = dashboard.entries.async_all() host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename - filename_to_host_name = self.filename_to_host_name - ping_result = dashboard.ping_result + host_name_to_path = self.host_name_to_path + path_to_host_name = self.path_to_host_name + entries = dashboard.entries - for entry in entries: + for entry in current_entries: name = entry.name # If no_mdns is set, remove it from the set if entry.no_mdns: @@ -58,37 +60,37 @@ class MDNSStatus: # We are tracking this host host_name_with_mdns_enabled.add(name) - filename = entry.filename + path = entry.path # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if name in host_mdns_state: - ping_result[filename] = host_mdns_state[name] + if (online := host_mdns_state.get(name, SENTINEL)) != SENTINEL: + entries.async_set_state(entry, bool_to_entry_state(online)) # Make sure the mapping is up to date # so when we get an mdns update we can map it back # to the filename - host_name_to_filename[name] = filename - filename_to_host_name[filename] = name + host_name_to_path[name] = path + path_to_host_name[path] = name async def async_run(self) -> None: dashboard = DASHBOARD - + entries = dashboard.entries aiozc = AsyncEsphomeZeroconf() self.aiozc = aiozc host_mdns_state = self.host_mdns_state - host_name_to_filename = self.host_name_to_filename + host_name_to_path = self.host_name_to_path host_name_with_mdns_enabled = self.host_name_with_mdns_enabled - ping_result = dashboard.ping_result def on_update(dat: dict[str, bool | None]) -> None: - """Update the global PING_RESULT dict.""" + """Update the entry state.""" for name, result in dat.items(): host_mdns_state[name] = result - if name in host_name_with_mdns_enabled: - filename = host_name_to_filename[name] - ping_result[filename] = result + if name not in host_name_with_mdns_enabled: + continue + if entry := entries.get(host_name_to_path[name]): + entries.async_set_state(entry, bool_to_entry_state(result)) stat = DashboardStatus(on_update) imports = DashboardImportDiscovery() diff --git a/esphome/dashboard/status/mqtt.py b/esphome/dashboard/status/mqtt.py index 2fd3a332a7..8c35dd2535 100644 --- a/esphome/dashboard/status/mqtt.py +++ b/esphome/dashboard/status/mqtt.py @@ -8,6 +8,7 @@ import threading from esphome import mqtt from ..core import DASHBOARD +from ..entries import EntryState class MqttStatusThread(threading.Thread): @@ -16,22 +17,23 @@ class MqttStatusThread(threading.Thread): def run(self) -> None: """Run the status thread.""" dashboard = DASHBOARD - entries = dashboard.entries.all() + entries = dashboard.entries + current_entries = entries.all() config = mqtt.config_from_env() topic = "esphome/discover/#" def on_message(client, userdata, msg): - nonlocal entries + nonlocal current_entries payload = msg.payload.decode(errors="backslashreplace") if len(payload) > 0: data = json.loads(payload) if "name" not in data: return - for entry in entries: + for entry in current_entries: if entry.name == data["name"]: - dashboard.ping_result[entry.filename] = True + entries.set_state(entry, EntryState.ONLINE) return def on_connect(client, userdata, flags, return_code): @@ -51,12 +53,11 @@ class MqttStatusThread(threading.Thread): client.loop_start() while not dashboard.stop_event.wait(2): - entries = dashboard.entries.all() - + current_entries = entries.all() # will be set to true on on_message - for entry in entries: + for entry in current_entries: if entry.no_mdns: - dashboard.ping_result[entry.filename] = False + entries.set_state(entry, EntryState.OFFLINE) client.publish("esphome/discover", None, retain=False) dashboard.mqtt_ping_request.wait() diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 35fb2259f0..d8281d9de1 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -5,7 +5,7 @@ import os from typing import cast from ..core import DASHBOARD -from ..entries import DashboardEntry +from ..entries import DashboardEntry, bool_to_entry_state from ..util.itertools import chunked from ..util.subprocess import async_system_command_status @@ -26,14 +26,14 @@ class PingStatus: async def async_run(self) -> None: """Run the ping status.""" dashboard = DASHBOARD + entries = dashboard.entries while not dashboard.stop_event.is_set(): # Only ping if the dashboard is open await dashboard.ping_request.wait() - dashboard.ping_result.clear() - entries = dashboard.entries.async_all() + current_entries = dashboard.entries.async_all() to_ping: list[DashboardEntry] = [ - entry for entry in entries if entry.address is not None + entry for entry in current_entries if entry.address is not None ] for ping_group in chunked(to_ping, 16): ping_group = cast(list[DashboardEntry], ping_group) @@ -46,4 +46,4 @@ class PingStatus: result = False elif isinstance(result, BaseException): raise result - dashboard.ping_result[entry.filename] = result + entries.async_set_state(entry, bool_to_entry_state(result)) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9a5de0a933..9972808948 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -37,6 +37,7 @@ from esphome.util import get_serial_ports, shlex_quote from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD +from .entries import EntryState, entry_state_to_bool from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify @@ -275,7 +276,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): if ( port == "OTA" and (mdns := dashboard.mdns_status) - and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) + and (host_name := mdns.get_path_to_host_name(config_file)) and (address := await mdns.async_resolve_host(host_name)) ): port = address @@ -315,7 +316,9 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket): return # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(self.old_name, None) + entries = DASHBOARD.entries + if entry := entries.get(self.old_name): + entries.async_set_state(entry, EntryState.UNKNOWN) class EsphomeUploadHandler(EsphomePortCommandWebSocket): @@ -609,22 +612,7 @@ class ListDevicesHandler(BaseHandler): self.write( json.dumps( { - "configured": [ - { - "name": entry.name, - "friendly_name": entry.friendly_name, - "configuration": entry.filename, - "loaded_integrations": entry.loaded_integrations, - "deployed_version": entry.update_old, - "current_version": entry.update_new, - "path": entry.path, - "comment": entry.comment, - "address": entry.address, - "web_port": entry.web_port, - "target_platform": entry.target_platform, - } - for entry in entries - ], + "configured": [entry.to_dict() for entry in entries], "importable": [ { "name": res.device_name, @@ -728,7 +716,15 @@ class PingRequestHandler(BaseHandler): if settings.status_use_mqtt: dashboard.mqtt_ping_request.set() self.set_header("content-type", "application/json") - self.write(json.dumps(dashboard.ping_result)) + + self.write( + json.dumps( + { + entry.filename: entry_state_to_bool(entry.state) + for entry in dashboard.entries.async_all() + } + ) + ) class InfoRequestHandler(BaseHandler): @@ -785,9 +781,6 @@ class DeleteRequestHandler(BaseHandler): if build_folder is not None: shutil.rmtree(build_folder, os.path.join(trash_path, name)) - # Remove the old ping result from the cache - DASHBOARD.ping_result.pop(configuration, None) - class UndoDeleteRequestHandler(BaseHandler): @authenticated From 8fbb4e27d1e09e3e7336baf5f2ef5113ecbdb734 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 18 Nov 2023 02:00:59 -0600 Subject: [PATCH 232/270] Add 2MB option for partitions.csv generation and restore use of user-defined partitions (#5779) --- esphome/components/esp32/__init__.py | 58 ++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9b83d144f8..fd5e9377dd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3,23 +3,26 @@ from typing import Union, Optional from pathlib import Path import logging import os +import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_ESPHOME, CONF_FRAMEWORK, + CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, + CONF_PATH, + CONF_PLATFORMIO_OPTIONS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_TYPE, + CONF_URL, CONF_VARIANT, CONF_VERSION, - CONF_ADVANCED, - CONF_REFRESH, - CONF_PATH, - CONF_URL, - CONF_REF, - CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, @@ -327,6 +330,32 @@ def _detect_variant(value): return value +def final_validate(config): + if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]: + return config + + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if ( + CONF_PARTITIONS in config + and pio_partitions_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + + if ( + pio_flash_size_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + + return config + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( @@ -387,6 +416,7 @@ FRAMEWORK_SCHEMA = cv.typed_schema( FLASH_SIZES = [ + "2MB", "4MB", "8MB", "16MB", @@ -394,6 +424,7 @@ FLASH_SIZES = [ ] CONF_FLASH_SIZE = "flash_size" +CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -401,6 +432,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), + cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -410,6 +442,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) @@ -462,7 +497,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -507,7 +545,10 @@ async def to_code(config): [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_define( "USE_ARDUINO_VERSION_CODE", @@ -518,6 +559,7 @@ async def to_code(config): APP_PARTITION_SIZES = { + "2MB": 0x0C0000, # 768 KB "4MB": 0x1C0000, # 1792 KB "8MB": 0x3C0000, # 3840 KB "16MB": 0x7C0000, # 7936 KB From c36944326368ca3d323eeb41ba3146d052c30629 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:28:04 -0600 Subject: [PATCH 233/270] Bump aioesphomeapi from 18.4.0 to 18.4.1 (#5767) 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 1d53c2761c..b1ca8e5058 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.0 +aioesphomeapi==18.4.1 zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default From e5e3b253bca349ee354ee76565b11ea571bbad4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:35:42 -0600 Subject: [PATCH 234/270] Bump aioesphomeapi from 18.4.1 to 18.5.2 (#5780) 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 b1ca8e5058..7a3bb421ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.4.1 +aioesphomeapi==18.5.2 zeroconf==0.123.0 # esp-idf requires this, but doesn't bundle it by default From 625ce2b8ebb88367174a1a73b684fa690261471d Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 17 Nov 2023 01:16:03 -0800 Subject: [PATCH 235/270] fix 32-bit arm (#5781) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7ca633a982..a892e1df38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,7 @@ ENV \ # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian RUN \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ + ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \ fi RUN \ From 22eef036c7a74072534e00b7071775fc8b20d1a8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 18 Nov 2023 02:00:59 -0600 Subject: [PATCH 236/270] Add 2MB option for partitions.csv generation and restore use of user-defined partitions (#5779) --- esphome/components/esp32/__init__.py | 58 ++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 9b83d144f8..fd5e9377dd 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3,23 +3,26 @@ from typing import Union, Optional from pathlib import Path import logging import os +import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.const import ( + CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_ESPHOME, CONF_FRAMEWORK, + CONF_IGNORE_EFUSE_MAC_CRC, CONF_NAME, + CONF_PATH, + CONF_PLATFORMIO_OPTIONS, + CONF_REF, + CONF_REFRESH, CONF_SOURCE, CONF_TYPE, + CONF_URL, CONF_VARIANT, CONF_VERSION, - CONF_ADVANCED, - CONF_REFRESH, - CONF_PATH, - CONF_URL, - CONF_REF, - CONF_IGNORE_EFUSE_MAC_CRC, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_NAME, @@ -327,6 +330,32 @@ def _detect_variant(value): return value +def final_validate(config): + if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]: + return config + + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if ( + CONF_PARTITIONS in config + and pio_partitions_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + + if ( + pio_flash_size_key + in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS] + ): + raise cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + + return config + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( @@ -387,6 +416,7 @@ FRAMEWORK_SCHEMA = cv.typed_schema( FLASH_SIZES = [ + "2MB", "4MB", "8MB", "16MB", @@ -394,6 +424,7 @@ FLASH_SIZES = [ ] CONF_FLASH_SIZE = "flash_size" +CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -401,6 +432,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), + cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } @@ -410,6 +442,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) @@ -462,7 +497,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -507,7 +545,10 @@ async def to_code(config): [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], ) - cg.add_platformio_option("board_build.partitions", "partitions.csv") + if CONF_PARTITIONS in config: + cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) + else: + cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_define( "USE_ARDUINO_VERSION_CODE", @@ -518,6 +559,7 @@ async def to_code(config): APP_PARTITION_SIZES = { + "2MB": 0x0C0000, # 768 KB "4MB": 0x1C0000, # 1792 KB "8MB": 0x3C0000, # 3840 KB "16MB": 0x7C0000, # 7936 KB From 8fb6b8f1a2a113cf3a47e4f294fb6b3b20ef3c6c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 18 Nov 2023 21:15:56 +1300 Subject: [PATCH 237/270] Bump version to 2023.11.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7ceaffbe57..0abb6a8e2d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.1" +__version__ = "2023.11.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4e4fe3c26db3760db997aa27fb732c4a6d56864f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:28:35 -0600 Subject: [PATCH 238/270] dashboard: Ensure disk I/O happens in the executor (#5789) * Ensure I/O executor * safe file writer * fixes * more io * more io --- esphome/dashboard/settings.py | 4 ++- esphome/dashboard/util/file.py | 55 +++++++++++++++++++++++++++++++ esphome/dashboard/web_server.py | 52 +++++++++++++++++++++-------- tests/dashboard/__init__.py | 0 tests/dashboard/util/__init__.py | 0 tests/dashboard/util/test_file.py | 53 +++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 esphome/dashboard/util/file.py create mode 100644 tests/dashboard/__init__.py create mode 100644 tests/dashboard/util/__init__.py create mode 100644 tests/dashboard/util/test_file.py diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 76633e1bf2..61718298d2 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -3,6 +3,7 @@ from __future__ import annotations import hmac import os from pathlib import Path +from typing import Any from esphome.core import CORE from esphome.helpers import get_bool_env @@ -69,7 +70,8 @@ class DashboardSettings: # Compare password in constant running time (to prevent timing attacks) return hmac.compare_digest(self.password_hash, password_hash(password)) - def rel_path(self, *args): + def rel_path(self, *args: Any) -> str: + """Return a path relative to the ESPHome config folder.""" joined_path = os.path.join(self.config_dir, *args) # Raises ValueError if not relative to ESPHome config folder Path(joined_path).resolve().relative_to(self.absolute_config_dir) diff --git a/esphome/dashboard/util/file.py b/esphome/dashboard/util/file.py new file mode 100644 index 0000000000..5f3c5f5f1b --- /dev/null +++ b/esphome/dashboard/util/file.py @@ -0,0 +1,55 @@ +import logging +import os +import tempfile +from pathlib import Path + +_LOGGER = logging.getLogger(__name__) + + +def write_utf8_file( + filename: Path, + utf8_str: str, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + write_file(filename, utf8_str.encode("utf-8"), private) + + +# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py +def write_file( + filename: Path, + utf8_data: bytes, + private: bool = False, +) -> None: + """Write a file and rename it into place. + + Writes all or nothing. + """ + + tmp_filename = "" + try: + # Modern versions of Python tempfile create this file with mode 0o600 + with tempfile.NamedTemporaryFile( + mode="wb", dir=os.path.dirname(filename), delete=False + ) as fdesc: + fdesc.write(utf8_data) + tmp_filename = fdesc.name + if not private: + os.fchmod(fdesc.fileno(), 0o644) + os.replace(tmp_filename, filename) + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error( + "File replacement cleanup failed for %s while saving %s: %s", + tmp_filename, + filename, + err, + ) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9972808948..8901da095f 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -38,6 +38,7 @@ from esphome.yaml_util import FastestAvailableSafeLoader from .core import DASHBOARD from .entries import EntryState, entry_state_to_bool +from .util.file import write_file from .util.subprocess import async_run_system_command from .util.text import friendly_name_slugify @@ -524,9 +525,19 @@ class DownloadListRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler): + def _load_file(self, path: str, compressed: bool) -> bytes: + """Load a file from disk and compress it if requested.""" + with open(path, "rb") as f: + data = f.read() + if compressed: + return gzip.compress(data, 9) + return data + @authenticated @bind_config - async def get(self, configuration=None): + async def get(self, configuration: str | None = None): + """Download a binary file.""" + loop = asyncio.get_running_loop() compressed = self.get_argument("compressed", "0") == "1" storage_path = ext_storage_path(configuration) @@ -583,11 +594,8 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404) return - with open(path, "rb") as f: - data = f.read() - if compressed: - data = gzip.compress(data, 9) - self.write(data) + data = await loop.run_in_executor(None, self._load_file, path, compressed) + self.write(data) self.finish() @@ -746,19 +754,35 @@ class InfoRequestHandler(BaseHandler): class EditRequestHandler(BaseHandler): @authenticated @bind_config - def get(self, configuration=None): + async def get(self, configuration: str | None = None): + """Get the content of a file.""" + loop = asyncio.get_running_loop() filename = settings.rel_path(configuration) - content = "" - if os.path.isfile(filename): - with open(file=filename, encoding="utf-8") as f: - content = f.read() + content = await loop.run_in_executor(None, self._read_file, filename) self.write(content) + def _read_file(self, filename: str) -> bytes: + """Read a file and return the content as bytes.""" + with open(file=filename, encoding="utf-8") as f: + return f.read() + + def _write_file(self, filename: str, content: bytes) -> None: + """Write a file with the given content.""" + write_file(filename, content) + @authenticated @bind_config - def post(self, configuration=None): - with open(file=settings.rel_path(configuration), mode="wb") as f: - f.write(self.request.body) + async def post(self, configuration: str | None = None): + """Write the content of a file.""" + loop = asyncio.get_running_loop() + config_file = settings.rel_path(configuration) + await loop.run_in_executor( + None, self._write_file, config_file, self.request.body + ) + # Ensure the StorageJSON is updated as well + await async_run_system_command( + [*DASHBOARD_COMMAND, "compile", "--only-generate", config_file] + ) self.set_status(200) diff --git a/tests/dashboard/__init__.py b/tests/dashboard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/util/__init__.py b/tests/dashboard/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/dashboard/util/test_file.py b/tests/dashboard/util/test_file.py new file mode 100644 index 0000000000..89e6b97086 --- /dev/null +++ b/tests/dashboard/util/test_file.py @@ -0,0 +1,53 @@ +import os +from pathlib import Path +from unittest.mock import patch + +import py +import pytest + +from esphome.dashboard.util.file import write_file, write_utf8_file + + +def test_write_utf8_file(tmp_path: Path) -> None: + write_utf8_file(tmp_path.joinpath("foo.txt"), "foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + with pytest.raises(OSError): + write_utf8_file(Path("/not-writable"), "bar") + + +def test_write_file(tmp_path: Path) -> None: + write_file(tmp_path.joinpath("foo.txt"), b"foo") + assert tmp_path.joinpath("foo.txt").read_text() == "foo" + + +def test_write_utf8_file_fails_at_rename( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename fails not not remove, we do not log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(OSError), patch( + "esphome.dashboard.util.file.os.replace", side_effect=OSError + ): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert not os.path.exists(test_file) + + assert "File replacement cleanup failed" not in caplog.text + + +def test_write_utf8_file_fails_at_rename_and_remove( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test that if rename and remove both fail, we log the failed cleanup.""" + test_dir = tmpdir.mkdir("files") + test_file = Path(test_dir / "test.json") + + with pytest.raises(OSError), patch( + "esphome.dashboard.util.file.os.remove", side_effect=OSError + ), patch("esphome.dashboard.util.file.os.replace", side_effect=OSError): + write_utf8_file(test_file, '{"some":"data"}', False) + + assert "File replacement cleanup failed" in caplog.text From cd9bf29df112506387cb32f4fadaa19df21484a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:29:40 -0600 Subject: [PATCH 239/270] dashboard: Add lookup by name to entries (#5790) * Add lookup by name to entries * adj * tweak * tweak * tweak * tweak * tweak * tweak * preen --- esphome/dashboard/entries.py | 24 +++++++++++++--- esphome/dashboard/status/mdns.py | 47 ++++++-------------------------- esphome/dashboard/web_server.py | 5 ++-- 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index 42b3a2e743..c5d7f3a245 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging import os +from collections import defaultdict from typing import TYPE_CHECKING, Any from esphome import const, util @@ -68,6 +69,7 @@ class DashboardEntries: "_entry_states", "_loaded_entries", "_update_lock", + "_name_to_entry", ) def __init__(self, dashboard: ESPHomeDashboard) -> None: @@ -83,11 +85,16 @@ class DashboardEntries: self._entries: dict[str, DashboardEntry] = {} self._loaded_entries = False self._update_lock = asyncio.Lock() + self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set) def get(self, path: str) -> DashboardEntry | None: """Get an entry by path.""" return self._entries.get(path) + def get_by_name(self, name: str) -> set[DashboardEntry] | None: + """Get an entry by name.""" + return self._name_to_entry.get(name) + async def _async_all(self) -> list[DashboardEntry]: """Return all entries.""" return list(self._entries.values()) @@ -155,6 +162,7 @@ class DashboardEntries: None, self._get_path_to_cache_key ) entries = self._entries + name_to_entry = self._name_to_entry added: dict[DashboardEntry, DashboardCacheKeyType] = {} updated: dict[DashboardEntry, DashboardCacheKeyType] = {} removed: set[DashboardEntry] = { @@ -162,14 +170,17 @@ class DashboardEntries: for filename, entry in entries.items() if filename not in path_to_cache_key } + original_names: dict[DashboardEntry, str] = {} for path, cache_key in path_to_cache_key.items(): - if entry := entries.get(path): - if entry.cache_key != cache_key: - updated[entry] = cache_key - else: + if not (entry := entries.get(path)): entry = DashboardEntry(path, cache_key) added[entry] = cache_key + continue + + if entry.cache_key != cache_key: + updated[entry] = cache_key + original_names[entry] = entry.name if added or updated: await self._loop.run_in_executor( @@ -179,13 +190,18 @@ class DashboardEntries: bus = self._dashboard.bus for entry in added: entries[entry.path] = entry + name_to_entry[entry.name].add(entry) bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry}) for entry in removed: del entries[entry.path] + name_to_entry[entry.name].discard(entry) bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry}) for entry in updated: + if (original_name := original_names[entry]) != (current_name := entry.name): + name_to_entry[original_name].discard(entry) + name_to_entry[current_name].add(entry) bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry}) def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index cbe3b3309e..4f4fa560d0 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -24,17 +24,8 @@ class MDNSStatus: self.aiozc: AsyncEsphomeZeroconf | None = None # This is the current mdns state for each host (True, False, None) self.host_mdns_state: dict[str, bool | None] = {} - # This is the hostnames to path mapping - self.host_name_to_path: dict[str, str] = {} - self.path_to_host_name: dict[str, str] = {} - # This is a set of host names to track (i.e no_mdns = false) - self.host_name_with_mdns_enabled: set[set] = set() self._loop = asyncio.get_running_loop() - def get_path_to_host_name(self, path: str) -> str | None: - """Resolve a path to an address in a thread-safe manner.""" - return self.path_to_host_name.get(path) - async def async_resolve_host(self, host_name: str) -> str | None: """Resolve a host name to an address in a thread-safe manner.""" if aiozc := self.aiozc: @@ -44,53 +35,32 @@ class MDNSStatus: async def async_refresh_hosts(self): """Refresh the hosts to track.""" dashboard = DASHBOARD - current_entries = dashboard.entries.async_all() - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_mdns_state = self.host_mdns_state - host_name_to_path = self.host_name_to_path - path_to_host_name = self.path_to_host_name entries = dashboard.entries - - for entry in current_entries: - name = entry.name - # If no_mdns is set, remove it from the set + for entry in entries.async_all(): if entry.no_mdns: - host_name_with_mdns_enabled.discard(name) continue - - # We are tracking this host - host_name_with_mdns_enabled.add(name) - path = entry.path - # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if (online := host_mdns_state.get(name, SENTINEL)) != SENTINEL: + if (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) - # Make sure the mapping is up to date - # so when we get an mdns update we can map it back - # to the filename - host_name_to_path[name] = path - path_to_host_name[path] = name - async def async_run(self) -> None: dashboard = DASHBOARD entries = dashboard.entries aiozc = AsyncEsphomeZeroconf() self.aiozc = aiozc host_mdns_state = self.host_mdns_state - host_name_to_path = self.host_name_to_path - host_name_with_mdns_enabled = self.host_name_with_mdns_enabled def on_update(dat: dict[str, bool | None]) -> None: """Update the entry state.""" for name, result in dat.items(): host_mdns_state[name] = result - if name not in host_name_with_mdns_enabled: - continue - if entry := entries.get(host_name_to_path[name]): - entries.async_set_state(entry, bool_to_entry_state(result)) + if matching_entries := entries.get_by_name(name): + for entry in matching_entries: + if not entry.no_mdns: + entries.async_set_state(entry, bool_to_entry_state(result)) stat = DashboardStatus(on_update) imports = DashboardImportDiscovery() @@ -102,10 +72,11 @@ class MDNSStatus: [stat.browser_callback, imports.browser_callback], ) + ping_request = dashboard.ping_request while not dashboard.stop_event.is_set(): await self.async_refresh_hosts() - await dashboard.ping_request.wait() - dashboard.ping_request.clear() + await ping_request.wait() + ping_request.clear() await browser.async_cancel() await aiozc.async_close() diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 8901da095f..7c5f653b5b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -271,14 +271,15 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): ) -> list[str]: """Build the command to run.""" dashboard = DASHBOARD + entries = dashboard.entries configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] if ( port == "OTA" and (mdns := dashboard.mdns_status) - and (host_name := mdns.get_path_to_host_name(config_file)) - and (address := await mdns.async_resolve_host(host_name)) + and (entry := entries.get(config_file)) + and (address := await mdns.async_resolve_host(entry.name)) ): port = address From 2aaee813136eb95812910fbcd17ef568befd2a34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 21:31:00 -0600 Subject: [PATCH 240/270] Refactor StorageJSON to keep loaded_integrations a set until its converted to JSON (#5793) * Refactor StorageJSON to keep loaded_integrations a set until its converted to a dict after #5792 we will be checking loaded_integrations often. ESPHome core keep uses a set, but it would get converted to a list when passed through StorageJSON. Keep it a set until its needed to be read/write to JSON so we do not have to linear searches on it since they have a time complexity of O(n) vs O(1) * legacy --- esphome/dashboard/entries.py | 4 +- esphome/storage_json.py | 95 ++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 54 deletions(-) diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py index c5d7f3a245..8ccfa795d5 100644 --- a/esphome/dashboard/entries.py +++ b/esphome/dashboard/entries.py @@ -285,7 +285,7 @@ class DashboardEntry: "name": self.name, "friendly_name": self.friendly_name, "configuration": self.filename, - "loaded_integrations": self.loaded_integrations, + "loaded_integrations": sorted(self.loaded_integrations), "deployed_version": self.update_old, "current_version": self.update_new, "path": self.path, @@ -381,7 +381,7 @@ class DashboardEntry: return const.__version__ @property - def loaded_integrations(self) -> list[str]: + def loaded_integrations(self) -> set[str]: if self.storage is None: return [] return self.storage.loaded_integrations diff --git a/esphome/storage_json.py b/esphome/storage_json.py index a2619cb536..0a41a4f738 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -1,21 +1,15 @@ +from __future__ import annotations import binascii import codecs -from datetime import datetime import json import logging import os -from typing import Optional +from datetime import datetime from esphome import const +from esphome.const import CONF_DISABLED, CONF_MDNS from esphome.core import CORE from esphome.helpers import write_file_if_changed - - -from esphome.const import ( - CONF_MDNS, - CONF_DISABLED, -) - from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) @@ -40,48 +34,47 @@ def trash_storage_path() -> str: class StorageJSON: def __init__( self, - storage_version, - name, - friendly_name, - comment, - esphome_version, - src_version, - address, - web_port, - target_platform, - build_path, - firmware_bin_path, - loaded_integrations, - no_mdns, - ): + storage_version: int, + name: str, + friendly_name: str, + comment: str, + esphome_version: str, + src_version: int | None, + address: str, + web_port: int | None, + target_platform: str, + build_path: str, + firmware_bin_path: str, + loaded_integrations: set[str], + no_mdns: bool, + ) -> None: # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) - self.storage_version: int = storage_version + self.storage_version = storage_version # The name of the node - self.name: str = name + self.name = name # The friendly name of the node - self.friendly_name: str = friendly_name + self.friendly_name = friendly_name # The comment of the node - self.comment: str = comment + self.comment = comment # The esphome version this was compiled with - self.esphome_version: str = esphome_version + self.esphome_version = esphome_version # The version of the file in src/main.cpp - Used to migrate the file assert src_version is None or isinstance(src_version, int) - self.src_version: int = src_version + self.src_version = src_version # Address of the ESP, for example livingroom.local or a static IP - self.address: str = address + self.address = address # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) - self.web_port: int = web_port + self.web_port = web_port # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. - self.target_platform: str = target_platform + self.target_platform = target_platform # The absolute path to the platformio project - self.build_path: str = build_path + self.build_path = build_path # The absolute path to the firmware binary - self.firmware_bin_path: str = firmware_bin_path - # A list of strings of names of loaded integrations - self.loaded_integrations: list[str] = loaded_integrations - self.loaded_integrations.sort() + self.firmware_bin_path = firmware_bin_path + # A set of strings of names of loaded integrations + self.loaded_integrations = loaded_integrations # Is mDNS disabled self.no_mdns = no_mdns @@ -98,7 +91,7 @@ class StorageJSON: "esp_platform": self.target_platform, "build_path": self.build_path, "firmware_bin_path": self.firmware_bin_path, - "loaded_integrations": self.loaded_integrations, + "loaded_integrations": sorted(self.loaded_integrations), "no_mdns": self.no_mdns, } @@ -109,9 +102,7 @@ class StorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def from_esphome_core( - esph: CoreType, old: Optional["StorageJSON"] - ) -> "StorageJSON": + def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON: hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 @@ -129,7 +120,7 @@ class StorageJSON: target_platform=hardware, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, - loaded_integrations=list(esph.loaded_integrations), + loaded_integrations=esph.loaded_integrations, no_mdns=( CONF_MDNS in esph.config and CONF_DISABLED in esph.config[CONF_MDNS] @@ -140,7 +131,7 @@ class StorageJSON: @staticmethod def from_wizard( name: str, friendly_name: str, address: str, platform: str - ) -> "StorageJSON": + ) -> StorageJSON: return StorageJSON( storage_version=1, name=name, @@ -153,12 +144,12 @@ class StorageJSON: target_platform=platform, build_path=None, firmware_bin_path=None, - loaded_integrations=[], + loaded_integrations=set(), no_mdns=False, ) @staticmethod - def _load_impl(path: str) -> Optional["StorageJSON"]: + def _load_impl(path: str) -> StorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -174,7 +165,7 @@ class StorageJSON: esp_platform = storage.get("esp_platform") build_path = storage.get("build_path") firmware_bin_path = storage.get("firmware_bin_path") - loaded_integrations = storage.get("loaded_integrations", []) + loaded_integrations = set(storage.get("loaded_integrations", [])) no_mdns = storage.get("no_mdns", False) return StorageJSON( storage_version, @@ -193,7 +184,7 @@ class StorageJSON: ) @staticmethod - def load(path: str) -> Optional["StorageJSON"]: + def load(path: str) -> StorageJSON | None: try: return StorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except @@ -215,7 +206,7 @@ class EsphomeStorageJSON: # The last time ESPHome checked for an update as an isoformat encoded str self.last_update_check_str: str = last_update_check # Cache of the version gotten in the last version check - self.remote_version: Optional[str] = remote_version + self.remote_version: str | None = remote_version def as_dict(self) -> dict: return { @@ -226,7 +217,7 @@ class EsphomeStorageJSON: } @property - def last_update_check(self) -> Optional[datetime]: + def last_update_check(self) -> datetime | None: try: return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") except Exception: # pylint: disable=broad-except @@ -243,7 +234,7 @@ class EsphomeStorageJSON: write_file_if_changed(path, self.to_json()) @staticmethod - def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: + def _load_impl(path: str) -> EsphomeStorageJSON | None: with codecs.open(path, "r", encoding="utf-8") as f_handle: storage = json.load(f_handle) storage_version = storage["storage_version"] @@ -255,14 +246,14 @@ class EsphomeStorageJSON: ) @staticmethod - def load(path: str) -> Optional["EsphomeStorageJSON"]: + def load(path: str) -> EsphomeStorageJSON | None: try: return EsphomeStorageJSON._load_impl(path) except Exception: # pylint: disable=broad-except return None @staticmethod - def get_default() -> "EsphomeStorageJSON": + def get_default() -> EsphomeStorageJSON: return EsphomeStorageJSON( storage_version=1, cookie_secret=binascii.hexlify(os.urandom(64)).decode(), From e367ab26e157f31317e817edba5f0dd88200cd95 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Sun, 19 Nov 2023 22:32:46 -0500 Subject: [PATCH 241/270] wifi: Don't build SoftAP/DHCPS support unless 'ap' is in config. (#5649) --- esphome/components/wifi/__init__.py | 4 +++ esphome/components/wifi/wifi_component.cpp | 20 +++++++++-- esphome/components/wifi/wifi_component.h | 10 ++++++ .../wifi/wifi_component_esp32_arduino.cpp | 6 ++++ .../wifi/wifi_component_esp8266.cpp | 6 ++++ .../wifi/wifi_component_esp_idf.cpp | 35 ++++++++++++++----- .../wifi/wifi_component_libretiny.cpp | 6 ++++ .../components/wifi/wifi_component_pico_w.cpp | 3 ++ esphome/core/defines.h | 1 + 9 files changed, 79 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c42835f169..32c9d07046 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -403,6 +403,10 @@ async def to_code(config): lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) + cg.add_define("USE_WIFI_AP") + elif CORE.is_esp32 and CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) + add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c1d5138f7b..d023405728 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -82,6 +82,7 @@ void WiFiComponent::start() { } else { this->start_scanning(); } +#ifdef USE_WIFI_AP } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { @@ -94,6 +95,7 @@ void WiFiComponent::start() { captive_portal::global_captive_portal->start(); } #endif +#endif // USE_WIFI_AP } #ifdef USE_IMPROV if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { @@ -160,6 +162,7 @@ void WiFiComponent::loop() { return; } +#ifdef USE_WIFI_AP if (this->has_ap() && !this->ap_setup_) { if (now - this->last_connected_ > this->ap_timeout_) { ESP_LOGI(TAG, "Starting fallback AP!"); @@ -170,6 +173,7 @@ void WiFiComponent::loop() { #endif } } +#endif // USE_WIFI_AP #ifdef USE_IMPROV if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { @@ -199,11 +203,16 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } #endif + network::IPAddress WiFiComponent::get_ip_address() { if (this->has_sta()) return this->wifi_sta_ip(); + +#ifdef USE_WIFI_AP if (this->has_ap()) return this->wifi_soft_ap_ip(); +#endif // USE_WIFI_AP + return {}; } network::IPAddress WiFiComponent::get_dns_address(int num) { @@ -218,6 +227,8 @@ std::string WiFiComponent::get_use_address() const { return this->use_address_; } void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } + +#ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { this->wifi_mode_({}, true); @@ -255,13 +266,16 @@ void WiFiComponent::setup_ap_config_() { } } -float WiFiComponent::get_loop_priority() const { - return 10.0f; // before other loop components -} void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; this->has_ap_ = true; } +#endif // USE_WIFI_AP + +float WiFiComponent::get_loop_priority() const { + return 10.0f; // before other loop components +} + void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 3ee69bb5de..6cbdc51caf 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -194,6 +194,7 @@ class WiFiComponent : public Component { void add_sta(const WiFiAP &ap); void clear_sta(); +#ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. * * This can also be used without set_sta(). Then the AP will always be active. @@ -203,6 +204,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } +#endif // USE_WIFI_AP void enable(); void disable(); @@ -299,7 +301,11 @@ class WiFiComponent : public Component { protected: static std::string format_mac_addr(const uint8_t mac[6]); + +#ifdef USE_WIFI_AP void setup_ap_config_(); +#endif // USE_WIFI_AP + void print_connect_params_(); void wifi_loop_(); @@ -313,8 +319,12 @@ class WiFiComponent : public Component { void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_(); bool wifi_scan_start_(bool passive); + +#ifdef USE_WIFI_AP bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); +#endif // USE_WIFI_AP + bool wifi_disconnect_(); int32_t wifi_channel_(); network::IPAddress wifi_subnet_mask_(); diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 17b15757ef..5d8aa7f749 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -597,6 +597,8 @@ void WiFiComponent::wifi_scan_done_callback_() { WiFi.scanDelete(); this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -654,6 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -692,11 +695,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bssid_t WiFiComponent::wifi_bssid() { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a48c6c711d..15b0c65641 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -688,6 +688,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) @@ -753,6 +755,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -790,11 +793,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { struct ip_info ip {}; wifi_get_ip_info(SOFTAP_IF, &ip); return network::IPAddress(&ip.ip); } +#endif // USE_WIFI_AP + bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; uint8_t *raw_bssid = WiFi.BSSID(); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 34ecaf887d..8fcafc5c12 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -17,7 +17,11 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif + +#ifdef USE_WIFI_AP #include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + #include "lwip/err.h" #include "lwip/dns.h" @@ -35,15 +39,19 @@ static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +#ifdef USE_WIFI_AP +static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif // USE_WIFI_AP + +static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) struct IDFWiFiEvent { esp_event_base_t event_base; @@ -159,7 +167,11 @@ void WiFiComponent::wifi_pre_setup_() { } s_sta_netif = esp_netif_create_default_wifi_sta(); + +#ifdef USE_WIFI_AP s_ap_netif = esp_netif_create_default_wifi_ap(); +#endif // USE_WIFI_AP + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // cfg.nvs_enable = false; err = esp_wifi_init(&cfg); @@ -761,6 +773,8 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { scan_done_ = false; return true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { esp_err_t err; @@ -816,6 +830,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return true; } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -853,6 +868,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } +#endif // USE_WIFI_AP + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(s_sta_netif, &ip); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index d7f4406540..29c6ce64d0 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -412,6 +412,8 @@ void WiFiComponent::wifi_scan_done_callback_() { WiFi.scanDelete(); this->scan_done_ = true; } + +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) @@ -423,6 +425,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); } } + bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { // enable AP if (!this->wifi_mode_({}, true)) @@ -438,7 +441,10 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), ap.get_channel().value_or(1), ap.get_hidden()); } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } +#endif // USE_WIFI_AP + bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } bssid_t WiFiComponent::wifi_bssid() { diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index d67b466d6c..c71203a877 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -138,6 +138,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { return true; } +#ifdef USE_WIFI_AP bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { // TODO: return false; @@ -151,7 +152,9 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } + network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } +#endif // USE_WIFI_AP bool WiFiComponent::wifi_disconnect_() { int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d4187d4c08..b93b8c9270 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -50,6 +50,7 @@ #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER #define USE_WIFI +#define USE_WIFI_AP // Arduino-specific feature flags #ifdef USE_ARDUINO From d462beea6e4676362a38274f3941dc4cbe6e43cf Mon Sep 17 00:00:00 2001 From: Christian Schmitt Date: Mon, 20 Nov 2023 04:34:26 +0100 Subject: [PATCH 242/270] ssd1306: handle V_COM differently for SH1106 (#5796) --- esphome/components/ssd1306_base/ssd1306_base.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 00b5c2d5a2..749c3511c1 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -154,6 +154,7 @@ void SSD1306::setup() { // Set V_COM (0xDB) this->command(SSD1306_COMMAND_SET_VCOM_DETECT); switch (this->model_) { + case SH1106_MODEL_128_64: case SH1107_MODEL_128_64: case SH1107_MODEL_128_128: this->command(0x35); From 5744490f2fee57a83e349f0d951f7cc1b18954aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:18:00 +0100 Subject: [PATCH 243/270] Bump aioesphomeapi from 18.5.3 to 18.5.5 (#5804) 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 1866d33ab2..4203ce6304 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.3 +aioesphomeapi==18.5.5 zeroconf==0.127.0 # esp-idf requires this, but doesn't bundle it by default From d5d97c455831e5ee0830bfd15cfcdcc2e79c1e36 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 20 Nov 2023 16:59:38 -0700 Subject: [PATCH 244/270] include payload_open when a lock supports OPEN (#5809) --- esphome/components/mqtt/mqtt_lock.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 197d0c32d4..f4a5126d0c 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->lock_->traits.get_assumed_state()) root[MQTT_OPTIMISTIC] = true; + if (this->lock_->traits.get_supports_open()) + root[MQTT_PAYLOAD_OPEN] = "OPEN"; } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } From 47d42afda342b6b5f8dc45d0bdcd5742c5f9a2cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 01:15:32 +0100 Subject: [PATCH 245/270] dashboard: Fix online status when api is disabled (#5791) --- esphome/dashboard/dashboard.py | 13 +++++++++++-- esphome/zeroconf.py | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 050564d21e..dd51382db8 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -973,6 +973,7 @@ class MDNSStatusThread(threading.Thread): self.host_name_to_filename: dict[str, str] = {} # This is a set of host names to track (i.e no_mdns = false) self.host_name_with_mdns_enabled: set[set] = set() + self.zc: EsphomeZeroconf | None = None self._refresh_hosts() def _refresh_hosts(self): @@ -996,7 +997,14 @@ class MDNSStatusThread(threading.Thread): # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if name in host_mdns_state: + if self.zc and ( + entry.loaded_integrations and "api" not in entry.loaded_integrations + ): + # No api available so we have to poll since + # the device won't respond to a request to ._esphomelib._tcp.local. + PING_RESULT[filename] = bool(self.zc.resolve_host(entry.name)) + elif name in host_mdns_state: + # We already have a state for this host PING_RESULT[filename] = host_mdns_state[name] # Make sure the mapping is up to date @@ -1007,7 +1015,8 @@ class MDNSStatusThread(threading.Thread): def run(self): global IMPORT_RESULT - zc = EsphomeZeroconf() + self.zc = EsphomeZeroconf() + zc = self.zc host_mdns_state = self.host_mdns_state host_name_to_filename = self.host_name_to_filename host_name_with_mdns_enabled = self.host_name_with_mdns_enabled diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index f4cb7f080b..2585d5adc0 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -150,7 +150,11 @@ class EsphomeZeroconf(Zeroconf): def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: """Resolve a host name to an IP address.""" name = host.partition(".")[0] - info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + info = HostResolver( + ESPHOME_SERVICE_TYPE, + f"{name}.{ESPHOME_SERVICE_TYPE}", + server=f"{name}.local.", + ) if ( info.load_from_cache(self) or (timeout and info.request(self, timeout * 1000)) From 7d5ebeda524e36c17d1123118a6c8c453ab31856 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 01:16:06 +0100 Subject: [PATCH 246/270] dashboard: Fix online status when api is disabled (#5792) --- esphome/dashboard/status/mdns.py | 19 +++++++++++++++++-- esphome/zeroconf.py | 4 +++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index 4f4fa560d0..bd212bc563 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -12,7 +12,7 @@ from esphome.zeroconf import ( from ..const import SENTINEL from ..core import DASHBOARD -from ..entries import bool_to_entry_state +from ..entries import DashboardEntry, bool_to_entry_state class MDNSStatus: @@ -37,15 +37,30 @@ class MDNSStatus: dashboard = DASHBOARD host_mdns_state = self.host_mdns_state entries = dashboard.entries + poll_names: dict[str, set[DashboardEntry]] = {} for entry in entries.async_all(): if entry.no_mdns: continue # If we just adopted/imported this host, we likely # already have a state for it, so we should make sure # to set it so the dashboard shows it as online - if (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: + if entry.loaded_integrations and "api" not in entry.loaded_integrations: + # No api available so we have to poll since + # the device won't respond to a request to ._esphomelib._tcp.local. + poll_names.setdefault(entry.name, set()).add(entry) + elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) + if poll_names and self.aiozc: + results = await asyncio.gather( + *(self.aiozc.async_resolve_host(name) for name in poll_names) + ) + for name, address in zip(poll_names, results): + result = bool(address) + host_mdns_state[name] = result + for entry in poll_names[name]: + entries.async_set_state(entry, bool_to_entry_state(result)) + async def async_run(self) -> None: dashboard = DASHBOARD entries = dashboard.entries diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 956e348e07..72cc4c00c6 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -169,7 +169,9 @@ class DashboardImportDiscovery: def _make_host_resolver(host: str) -> HostResolver: """Create a new HostResolver for the given host name.""" name = host.partition(".")[0] - info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") + info = HostResolver( + ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local." + ) return info From 55f13dc3479aa683ecbf24fa8d95d4e3b4e163de Mon Sep 17 00:00:00 2001 From: CVan Date: Mon, 20 Nov 2023 19:19:36 -0500 Subject: [PATCH 247/270] fix: compile errors with fonts (#5808) --- docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a892e1df38..1bf754464d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,6 +48,8 @@ RUN \ libfreetype-dev=2.12.1+dfsg-5 \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ + libopenjp2-7=2.5.0-2 \ + libtiff6=4.5.0-6 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ From cf6b56c1ac7a6204d04a8a2f6d705b1fb597a8a2 Mon Sep 17 00:00:00 2001 From: Pavlo Dudnytskyi Date: Tue, 21 Nov 2023 02:12:36 +0100 Subject: [PATCH 248/270] Haier component updated to support new protocol variations (#5713) Co-authored-by: Pavlo Dudnytskyi --- esphome/components/haier/climate.py | 40 +- esphome/components/haier/haier_base.cpp | 264 ++++---- esphome/components/haier/haier_base.h | 86 ++- esphome/components/haier/hon_climate.cpp | 640 +++++++++++------- esphome/components/haier/hon_climate.h | 49 +- esphome/components/haier/hon_packet.h | 90 +-- .../components/haier/smartair2_climate.cpp | 376 +++++----- esphome/components/haier/smartair2_climate.h | 22 +- esphome/components/haier/smartair2_packet.h | 18 +- platformio.ini | 2 +- 10 files changed, 870 insertions(+), 717 deletions(-) diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index d796f13581..49d42a231f 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0 PROTOCOL_MAX_TEMPERATURE = 30.0 PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 +PROTOCOL_CONTROL_PACKET_SIZE = 10 CODEOWNERS = ["@paveldn"] AUTO_LOAD = ["sensor"] DEPENDENCIES = ["climate", "uart"] -CONF_WIFI_SIGNAL = "wifi_signal" +CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control" CONF_ANSWER_TIMEOUT = "answer_timeout" +CONF_CONTROL_METHOD = "control_method" +CONF_CONTROL_PACKET_SIZE = "control_packet_size" CONF_DISPLAY = "display" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_VERTICAL_AIRFLOW = "vertical_airflow" -CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" +CONF_WIFI_SIGNAL = "wifi_signal" PROTOCOL_HON = "HON" PROTOCOL_SMARTAIR2 = "SMARTAIR2" @@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, } +HonControlMethod = haier_ns.enum("HonControlMethod", True) +SUPPORTED_HON_CONTROL_METHODS = { + "MONITOR_ONLY": HonControlMethod.MONITOR_ONLY, + "SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS, + "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, +} + def validate_visual(config): if CONF_VISUAL in config: @@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All( PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Smartair2Climate), + cv.Optional( + CONF_ALTERNATIVE_SWING_CONTROL, default=False + ): cv.boolean, cv.Optional( CONF_SUPPORTED_PRESETS, default=list( @@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All( PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(HonClimate), + cv.Optional( + CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" + ): cv.ensure_list( + cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) + ), cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional( + CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE + ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), cv.Optional( CONF_SUPPORTED_PRESETS, default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), @@ -408,6 +430,8 @@ async def to_code(config): await climate.register_climate(var, config) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) + if CONF_CONTROL_METHOD in config: + cg.add(var.set_control_method(config[CONF_CONTROL_METHOD])) if CONF_BEEPER in config: cg.add(var.set_beeper_state(config[CONF_BEEPER])) if CONF_DISPLAY in config: @@ -423,5 +447,15 @@ async def to_code(config): cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) if CONF_ANSWER_TIMEOUT in config: cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) + if CONF_ALTERNATIVE_SWING_CONTROL in config: + cg.add( + var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL]) + ) + if CONF_CONTROL_PACKET_SIZE in config: + cg.add( + var.set_extra_control_packet_bytes_size( + config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE + ) + ) # https://github.com/paveldn/HaierProtocol - cg.add_library("pavlodn/HaierProtocol", "0.9.20") + cg.add_library("pavlodn/HaierProtocol", "0.9.24") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 22899b1a70..6943fc7d9c 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; -constexpr size_t CONTROL_TIMEOUT_MS = 7000; -constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied -#if (HAIER_LOG_LEVEL > 4) -// To reduce size of binary this function only available when log level is Verbose const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { static const char *phase_names[] = { "SENDING_INIT_1", - "WAITING_INIT_1_ANSWER", "SENDING_INIT_2", - "WAITING_INIT_2_ANSWER", "SENDING_FIRST_STATUS_REQUEST", - "WAITING_FIRST_STATUS_ANSWER", "SENDING_ALARM_STATUS_REQUEST", - "WAITING_ALARM_STATUS_ANSWER", "IDLE", - "UNKNOWN", "SENDING_STATUS_REQUEST", - "WAITING_STATUS_ANSWER", "SENDING_UPDATE_SIGNAL_REQUEST", - "WAITING_UPDATE_SIGNAL_ANSWER", "SENDING_SIGNAL_LEVEL", - "WAITING_SIGNAL_LEVEL_ANSWER", "SENDING_CONTROL", - "WAITING_CONTROL_ANSWER", - "SENDING_POWER_ON_COMMAND", - "WAITING_POWER_ON_ANSWER", - "SENDING_POWER_OFF_COMMAND", - "WAITING_POWER_OFF_ANSWER", + "SENDING_ACTION_COMMAND", "UNKNOWN" // Should be the last! }; + static_assert( + (sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1), + "Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases"); int phase_index = (int) phase; if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; return phase_names[phase_index]; } -#endif + +bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, + size_t timeout) { + return std::chrono::duration_cast(now - tpoint).count() > timeout; +} HaierClimateBase::HaierClimateBase() : haier_protocol_(*this), protocol_phase_(ProtocolPhases::SENDING_INIT_1), - action_request_(ActionRequest::NO_ACTION), display_status_(true), health_mode_(false), force_send_control_(false), - forced_publish_(false), forced_request_status_(false), - first_control_attempt_(false), reset_protocol_request_(false), - send_wifi_signal_(true) { + send_wifi_signal_(true), + use_crc_(false) { this->traits_ = climate::ClimateTraits(); this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, @@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {} void HaierClimateBase::set_phase(ProtocolPhases phase) { if (this->protocol_phase_ != phase) { -#if (HAIER_LOG_LEVEL > 4) ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); -#else - ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase); -#endif this->protocol_phase_ = phase; } } -bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, - std::chrono::steady_clock::time_point tpoint, size_t timeout) { - return std::chrono::duration_cast(now - tpoint).count() > timeout; +void HaierClimateBase::reset_phase_() { + this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE + : ProtocolPhases::SENDING_INIT_1); +} + +void HaierClimateBase::reset_to_idle_() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + this->forced_request_status_ = true; + this->set_phase(ProtocolPhases::IDLE); + this->action_request_.reset(); } bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); + return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); } bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); -} - -bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS); + return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); } bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); + return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); } bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { - return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); + return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); } #ifdef USE_WIFI -haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { +haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() { static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; if (wifi::global_wifi_component->is_connected()) { wifi_status_data[1] = 0; @@ -131,7 +121,8 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t wifi_status_data[1] = 1; wifi_status_data[3] = 0; } - return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); + return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data, + sizeof(wifi_status_data)); } #endif @@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_; void HaierClimateBase::set_display_state(bool state) { if (this->display_status_ != state) { this->display_status_ = state; - this->set_force_send_control_(true); + this->force_send_control_ = true; } } @@ -149,15 +140,24 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } void HaierClimateBase::set_health_mode(bool state) { if (this->health_mode_ != state) { this->health_mode_ = state; - this->set_force_send_control_(true); + this->force_send_control_ = true; } } -void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } +void HaierClimateBase::send_power_on_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional()}); +} -void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } +void HaierClimateBase::send_power_off_command() { + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); +} -void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } +void HaierClimateBase::toggle_power() { + this->action_request_ = + PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional()}); +} void HaierClimateBase::set_supported_swing_modes(const std::set &modes) { this->traits_.set_supported_swing_modes(modes); @@ -165,9 +165,7 @@ void HaierClimateBase::set_supported_swing_modes(const std::settraits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); } -void HaierClimateBase::set_answer_timeout(uint32_t timeout) { - this->answer_timeout_ = std::chrono::milliseconds(timeout); -} +void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } void HaierClimateBase::set_supported_modes(const std::set &modes) { this->traits_.set_supported_modes(modes); @@ -183,29 +181,42 @@ void HaierClimateBase::set_supported_presets(const std::setsend_wifi_signal_ = send_wifi; } -haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, - uint8_t expected_request_message_type, - uint8_t answer_message_type, - uint8_t expected_answer_message_type, - ProtocolPhases expected_phase) { +void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { + this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); +} + +haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( + haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, + ProtocolPhases expected_phase) { haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; - if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) + if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (request_message_type != expected_request_message_type)) result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) + if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && + (answer_message_type != expected_answer_message_type)) result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) + if (!this->haier_protocol_.is_waiting_for_answer() || + ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))) result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; - if (is_message_invalid(answer_message_type)) + if (answer_message_type == haier_protocol::FrameType::INVALID) result = haier_protocol::HandlerError::INVALID_ANSWER; return result; } -haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); -#else - ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); -#endif +haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, + haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL); + this->set_phase(ProtocolPhases::IDLE); + return result; +} + +haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) { + ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type, + phase_to_string_(this->protocol_phase_)); if (this->protocol_phase_ > ProtocolPhases::IDLE) { this->set_phase(ProtocolPhases::IDLE); } else { @@ -219,79 +230,95 @@ void HaierClimateBase::setup() { // Set timestamp here to give AC time to boot this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); - this->set_handlers(); this->haier_protocol_.set_default_timeout_handler( std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + this->set_handlers(); } void HaierClimateBase::dump_config() { LOG_CLIMATE("", "Haier Climate", this); - ESP_LOGCONFIG(TAG, " Device communication status: %s", - (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); + ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none"); } void HaierClimateBase::loop() { std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); if ((std::chrono::duration_cast(now - this->last_valid_status_timestamp_).count() > COMMUNICATION_TIMEOUT_MS) || - (this->reset_protocol_request_)) { + (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) { + this->last_valid_status_timestamp_ = now; if (this->protocol_phase_ >= ProtocolPhases::IDLE) { // No status too long, reseting protocol + // No need to reset protocol if we didn't pass initialization phase if (this->reset_protocol_request_) { this->reset_protocol_request_ = false; ESP_LOGW(TAG, "Protocol reset requested"); } else { ESP_LOGW(TAG, "Communication timeout, reseting protocol"); } - this->last_valid_status_timestamp_ = now; - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->set_phase(ProtocolPhases::SENDING_INIT_1); + this->process_protocol_reset(); return; - } else { - // No need to reset protocol if we didn't pass initialization phase - this->last_valid_status_timestamp_ = now; } }; - if ((this->protocol_phase_ == ProtocolPhases::IDLE) || - (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || - (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || - (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { + if ((!this->haier_protocol_.is_waiting_for_answer()) && + ((this->protocol_phase_ == ProtocolPhases::IDLE) || + (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || + (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) { // If control message or action is pending we should send it ASAP unless we are in initialisation // procedure or waiting for an answer - if (this->action_request_ != ActionRequest::NO_ACTION) { - this->process_pending_action(); - } else if (this->hvac_settings_.valid || this->force_send_control_) { + if (this->action_request_.has_value() && this->prepare_pending_action()) { + this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND); + } else if (this->next_hvac_settings_.valid || this->force_send_control_) { ESP_LOGV(TAG, "Control packet is pending..."); this->set_phase(ProtocolPhases::SENDING_CONTROL); + if (this->next_hvac_settings_.valid) { + this->current_hvac_settings_ = this->next_hvac_settings_; + this->next_hvac_settings_.reset(); + } else { + this->current_hvac_settings_.reset(); + } } } this->process_phase(now); this->haier_protocol_.loop(); } -void HaierClimateBase::process_pending_action() { - ActionRequest request = this->action_request_; - if (this->action_request_ == ActionRequest::TOGGLE_POWER) { - request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; - } - switch (request) { - case ActionRequest::TURN_POWER_ON: - this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); - break; - case ActionRequest::TURN_POWER_OFF: - this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); - break; - case ActionRequest::TOGGLE_POWER: - case ActionRequest::NO_ACTION: - // shouldn't get here, do nothing - break; - default: - ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); - break; - } - this->action_request_ = ActionRequest::NO_ACTION; +void HaierClimateBase::process_protocol_reset() { + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + if (this->next_hvac_settings_.valid) + this->next_hvac_settings_.reset(); + this->mode = CLIMATE_MODE_OFF; + this->current_temperature = NAN; + this->target_temperature = NAN; + this->fan_mode.reset(); + this->preset.reset(); + this->publish_state(); + this->set_phase(ProtocolPhases::SENDING_INIT_1); +} + +bool HaierClimateBase::prepare_pending_action() { + if (this->action_request_.has_value()) { + switch (this->action_request_.value().action) { + case ActionRequest::SEND_CUSTOM_COMMAND: + return true; + case ActionRequest::TURN_POWER_ON: + this->action_request_.value().message = this->get_power_message(true); + return true; + case ActionRequest::TURN_POWER_OFF: + this->action_request_.value().message = this->get_power_message(false); + return true; + case ActionRequest::TOGGLE_POWER: + this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF); + return true; + default: + ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action); + this->action_request_.reset(); + return false; + } + } else + return false; } ClimateTraits HaierClimateBase::traits() { return traits_; } @@ -302,23 +329,22 @@ void HaierClimateBase::control(const ClimateCall &call) { ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); return; // cancel the control, we cant do it without a poll answer. } - if (this->hvac_settings_.valid) { - ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); + if (this->current_hvac_settings_.valid) { + ESP_LOGW(TAG, "New settings come faster then processed!"); } { if (call.get_mode().has_value()) - this->hvac_settings_.mode = call.get_mode(); + this->next_hvac_settings_.mode = call.get_mode(); if (call.get_fan_mode().has_value()) - this->hvac_settings_.fan_mode = call.get_fan_mode(); + this->next_hvac_settings_.fan_mode = call.get_fan_mode(); if (call.get_swing_mode().has_value()) - this->hvac_settings_.swing_mode = call.get_swing_mode(); + this->next_hvac_settings_.swing_mode = call.get_swing_mode(); if (call.get_target_temperature().has_value()) - this->hvac_settings_.target_temperature = call.get_target_temperature(); + this->next_hvac_settings_.target_temperature = call.get_target_temperature(); if (call.get_preset().has_value()) - this->hvac_settings_.preset = call.get_preset(); - this->hvac_settings_.valid = true; + this->next_hvac_settings_.preset = call.get_preset(); + this->next_hvac_settings_.valid = true; } - this->first_control_attempt_ = true; } void HaierClimateBase::HvacSettings::reset() { @@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() { this->preset.reset(); } -void HaierClimateBase::set_force_send_control_(bool status) { - this->force_send_control_ = status; - if (status) { - this->first_control_attempt_ = true; - } -} - -void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { - if (this->answer_timeout_.has_value()) { - this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value()); - } else { - this->haier_protocol_.send_message(command, use_crc); - } +void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats, + std::chrono::milliseconds interval) { + this->haier_protocol_.send_message(command, use_crc, num_repeats, interval); this->last_request_timestamp_ = std::chrono::steady_clock::now(); } diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index b2446d6fb5..75abbc20fb 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -11,7 +11,7 @@ namespace esphome { namespace haier { enum class ActionRequest : uint8_t { - NO_ACTION = 0, + SEND_CUSTOM_COMMAND = 0, TURN_POWER_ON = 1, TURN_POWER_OFF = 2, TOGGLE_POWER = 3, @@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component, void control(const esphome::climate::ClimateCall &call) override; void dump_config() override; float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } - void set_fahrenheit(bool fahrenheit); void set_display_state(bool state); bool get_display_state() const; void set_health_mode(bool state); @@ -45,6 +44,7 @@ class HaierClimateBase : public esphome::Component, void set_supported_modes(const std::set &modes); void set_supported_swing_modes(const std::set &modes); void set_supported_presets(const std::set &presets); + bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t read_array(uint8_t *data, size_t len) noexcept override { return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; @@ -55,63 +55,56 @@ class HaierClimateBase : public esphome::Component, bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; void set_answer_timeout(uint32_t timeout); void set_send_wifi(bool send_wifi); + void send_custom_command(const haier_protocol::HaierMessage &message); protected: enum class ProtocolPhases { UNKNOWN = -1, // INITIALIZATION SENDING_INIT_1 = 0, - WAITING_INIT_1_ANSWER = 1, - SENDING_INIT_2 = 2, - WAITING_INIT_2_ANSWER = 3, - SENDING_FIRST_STATUS_REQUEST = 4, - WAITING_FIRST_STATUS_ANSWER = 5, - SENDING_ALARM_STATUS_REQUEST = 6, - WAITING_ALARM_STATUS_ANSWER = 7, + SENDING_INIT_2, + SENDING_FIRST_STATUS_REQUEST, + SENDING_ALARM_STATUS_REQUEST, // FUNCTIONAL STATE - IDLE = 8, - SENDING_STATUS_REQUEST = 10, - WAITING_STATUS_ANSWER = 11, - SENDING_UPDATE_SIGNAL_REQUEST = 12, - WAITING_UPDATE_SIGNAL_ANSWER = 13, - SENDING_SIGNAL_LEVEL = 14, - WAITING_SIGNAL_LEVEL_ANSWER = 15, - SENDING_CONTROL = 16, - WAITING_CONTROL_ANSWER = 17, - SENDING_POWER_ON_COMMAND = 18, - WAITING_POWER_ON_ANSWER = 19, - SENDING_POWER_OFF_COMMAND = 20, - WAITING_POWER_OFF_ANSWER = 21, + IDLE, + SENDING_STATUS_REQUEST, + SENDING_UPDATE_SIGNAL_REQUEST, + SENDING_SIGNAL_LEVEL, + SENDING_CONTROL, + SENDING_ACTION_COMMAND, NUM_PROTOCOL_PHASES }; -#if (HAIER_LOG_LEVEL > 4) const char *phase_to_string_(ProtocolPhases phase); -#endif virtual void set_handlers() = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual haier_protocol::HaierMessage get_control_message() = 0; - virtual bool is_message_invalid(uint8_t message_type) = 0; - virtual void process_pending_action(); + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; + virtual bool prepare_pending_action(); + virtual void process_protocol_reset(); esphome::climate::ClimateTraits traits() override; - // Answers handlers - haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, - uint8_t answer_message_type, uint8_t expected_answer_message_type, + // Answer handlers + haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type, + haier_protocol::FrameType expected_request_message_type, + haier_protocol::FrameType answer_message_type, + haier_protocol::FrameType expected_answer_message_type, ProtocolPhases expected_phase); + haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, + const uint8_t *data, size_t data_size); // Timeout handler - haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); + haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type); // Helper functions - void set_force_send_control_(bool status); - void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); + void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0, + std::chrono::milliseconds interval = std::chrono::milliseconds::zero()); virtual void set_phase(ProtocolPhases phase); - bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, - size_t timeout); + void reset_phase_(); + void reset_to_idle_(); bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); - bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); #ifdef USE_WIFI - haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); + haier_protocol::HaierMessage get_wifi_signal_message_(); #endif struct HvacSettings { @@ -122,29 +115,34 @@ class HaierClimateBase : public esphome::Component, esphome::optional preset; bool valid; HvacSettings() : valid(false){}; + HvacSettings(const HvacSettings &) = default; + HvacSettings &operator=(const HvacSettings &) = default; void reset(); }; + struct PendingAction { + ActionRequest action; + esphome::optional message; + }; haier_protocol::ProtocolHandler haier_protocol_; ProtocolPhases protocol_phase_; - ActionRequest action_request_; + esphome::optional action_request_; uint8_t fan_mode_speed_; uint8_t other_modes_fan_speed_; bool display_status_; bool health_mode_; bool force_send_control_; - bool forced_publish_; bool forced_request_status_; - bool first_control_attempt_; bool reset_protocol_request_; + bool send_wifi_signal_; + bool use_crc_; esphome::climate::ClimateTraits traits_; - HvacSettings hvac_settings_; + HvacSettings current_hvac_settings_; + HvacSettings next_hvac_settings_; + std::unique_ptr last_status_message_; std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout std::chrono::steady_clock::time_point last_status_request_; // To request AC status - std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message - optional answer_timeout_; // Message answer timeout - bool send_wifi_signal_; - std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level + std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level }; } // namespace haier diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index d4944410f7..09f90fffa8 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -14,6 +14,8 @@ namespace haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { switch (direction) { @@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir } HonClimate::HonClimate() - : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), - cleaning_status_(CleaningState::NO_CLEANING), + : cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), - hvac_hardware_info_available_(false), - hvac_functions_{false, false, false, false, false}, - use_crc_(hvac_functions_[2]), active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, outdoor_sensor_(nullptr) { + last_status_message_ = std::unique_ptr(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]); this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; } @@ -72,14 +71,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this- void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { this->vertical_direction_ = direction; - this->set_force_send_control_(true); + this->force_send_control_ = true; } AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { this->horizontal_direction_ = direction; - this->set_force_send_control_(true); + this->force_send_control_ = true; } std::string HonClimate::get_cleaning_status_text() const { @@ -98,35 +97,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st void HonClimate::start_self_cleaning() { if (this->cleaning_status_ == CleaningState::NO_CLEANING) { ESP_LOGI(TAG, "Sending self cleaning start request"); - this->action_request_ = ActionRequest::START_SELF_CLEAN; - this->set_force_send_control_(true); + this->action_request_ = + PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional()}); } } void HonClimate::start_steri_cleaning() { if (this->cleaning_status_ == CleaningState::NO_CLEANING) { ESP_LOGI(TAG, "Sending steri cleaning start request"); - this->action_request_ = ActionRequest::START_STERI_CLEAN; - this->set_force_send_control_(true); + this->action_request_ = + PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional()}); } } -haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { // Should check this before preprocess - if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { + if (message_type == haier_protocol::FrameType::INVALID) { ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " "protocol instead of hOn"); this->set_phase(ProtocolPhases::SENDING_INIT_1); return haier_protocol::HandlerError::INVALID_ANSWER; } - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type, + haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1); if (result == haier_protocol::HandlerError::HANDLER_OK) { if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { // Wrong structure - this->set_phase(ProtocolPhases::SENDING_INIT_1); return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; } // All OK @@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint char tmp[9]; tmp[8] = 0; strncpy(tmp, answr->protocol_version, 8); - this->hvac_protocol_version_ = std::string(tmp); + this->hvac_hardware_info_ = HardwareInfo(); + this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); strncpy(tmp, answr->software_version, 8); - this->hvac_software_version_ = std::string(tmp); + this->hvac_hardware_info_.value().software_version_ = std::string(tmp); strncpy(tmp, answr->hardware_version, 8); - this->hvac_hardware_version_ = std::string(tmp); + this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); strncpy(tmp, answr->device_name, 8); - this->hvac_device_name_ = std::string(tmp); - this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support - this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support - this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support - this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support - this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support - this->hvac_hardware_info_available_ = true; + this->hvac_hardware_info_.value().device_name_ = std::string(tmp); + this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support + this->hvac_hardware_info_.value().functions_[1] = + (answr->functions[1] & 0x02) != 0; // controller-device mode support + this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support + this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support + this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support + this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; this->set_phase(ProtocolPhases::SENDING_INIT_2); return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, - (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); + haier_protocol::HandlerError result = + this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type, + haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2); if (result == haier_protocol::HandlerError::HANDLER_OK) { this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size) { +haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, - (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); if (result == haier_protocol::HandlerError::HANDLER_OK) { result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; } else { if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); @@ -189,36 +191,48 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, sizeof(hon_protocol::HaierPacketControl)); } - if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { - ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); - } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || - (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || - (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + if (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); + if (this->control_messages_queue_.empty()) { + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + } else { + this->set_phase(ProtocolPhases::SENDING_CONTROL); + } + break; + default: + break; } } return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_INIT_1); + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); return result; } } -haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, - ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); +haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + haier_protocol::HandlerError result = this->answer_preprocess_( + request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type, + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); if (result == haier_protocol::HandlerError::HANDLER_OK) { this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); return result; @@ -228,25 +242,16 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl } } -haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, size_t data_size) { - haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, - (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); - this->set_phase(ProtocolPhases::IDLE); - return result; -} - -haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { - if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { - if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { + if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) { + if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { // Unexpected answer to request this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; } - if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { + if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) { // Don't expect this answer now this->set_phase(ProtocolPhases::IDLE); return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; @@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), + haier_protocol::FrameType::GET_DEVICE_VERSION, std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), + haier_protocol::FrameType::GET_DEVICE_ID, std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::CONTROL), + haier_protocol::FrameType::CONTROL, std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), + haier_protocol::FrameType::GET_ALARM_STATUS, std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), + haier_protocol::FrameType::REPORT_NETWORK_STATUS, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); } @@ -291,14 +296,18 @@ void HonClimate::set_handlers() { void HonClimate::dump_config() { HaierClimateBase::dump_config(); ESP_LOGCONFIG(TAG, " Protocol version: hOn"); - if (this->hvac_hardware_info_available_) { - ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); - ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); - ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), - (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), - (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); + ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_); + if (this->hvac_hardware_info_.has_value()) { + ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str()); + ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", + (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), + (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), + (this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""), + (this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""), + (this->hvac_hardware_info_.value().functions_[4] ? " role" : "")); ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); } } @@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { - this->hvac_hardware_info_available_ = false; // Indicate device capabilities: // bit 0 - if 1 module support interactive mode // bit 1 - if 1 module support controller-device mode @@ -316,109 +324,95 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { // bit 4..bit 15 - not used uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); } break; case ProtocolPhases::SENDING_INIT_2: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); + static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID); this->send_message_(DEVICEID_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER); } break; case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage STATUS_REQUEST( - (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); this->send_message_(STATUS_REQUEST, this->use_crc_); this->last_status_request_ = now; - this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; #ifdef USE_WIFI case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); + haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION); this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); this->last_signal_request_ = now; - this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); } break; case ProtocolPhases::SENDING_SIGNAL_LEVEL: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), - this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); } break; - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - break; #else case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: this->set_phase(ProtocolPhases::IDLE); break; #endif case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( - (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); + static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); - this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); } break; case ProtocolPhases::SENDING_CONTROL: - if (this->first_control_attempt_) { - this->control_request_timestamp_ = now; - this->first_control_attempt_ = false; + if (this->control_messages_queue_.empty()) { + switch (this->control_method_) { + case HonControlMethod::SET_GROUP_PARAMETERS: { + haier_protocol::HaierMessage control_message = this->get_control_message(); + this->control_messages_queue_.push(control_message); + } break; + case HonControlMethod::SET_SINGLE_PARAMETER: + this->fill_control_messages_queue_(); + break; + case HonControlMethod::MONITOR_ONLY: + ESP_LOGI(TAG, "AC control is disabled, monitor only"); + this->reset_to_idle_(); + return; + default: + ESP_LOGW(TAG, "Unsupported control method for hOn protocol!"); + this->reset_to_idle_(); + return; + } } - if (this->is_control_message_timeout_exceeded_(now)) { - ESP_LOGW(TAG, "Sending control packet timeout!"); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->forced_request_status_ = true; - this->forced_publish_ = true; - this->set_phase(ProtocolPhases::IDLE); + if (this->control_messages_queue_.empty()) { + ESP_LOGW(TAG, "Control message queue is empty!"); + this->reset_to_idle_(); } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { - haier_protocol::HaierMessage control_message = get_control_message(); - this->send_message_(control_message, this->use_crc_); - ESP_LOGI(TAG, "Control packet sent"); - this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); + ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size()); + this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); } break; - case ProtocolPhases::SENDING_POWER_ON_COMMAND: - case ProtocolPhases::SENDING_POWER_OFF_COMMAND: - if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; - if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) - pwr_cmd_buf[1] = 0x01; - haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, - ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, - pwr_cmd_buf, sizeof(pwr_cmd_buf)); - this->send_message_(power_cmd, this->use_crc_); - this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); + this->set_phase(ProtocolPhases::IDLE); } break; - - case ProtocolPhases::WAITING_INIT_1_ANSWER: - case ProtocolPhases::WAITING_INIT_2_ANSWER: - case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: - case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: - case ProtocolPhases::WAITING_STATUS_ANSWER: - case ProtocolPhases::WAITING_CONTROL_ANSWER: - case ProtocolPhases::WAITING_POWER_ON_ANSWER: - case ProtocolPhases::WAITING_POWER_OFF_ANSWER: - break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); @@ -433,26 +427,35 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { } break; default: // Shouldn't get here -#if (HAIER_LOG_LEVEL > 4) ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); -#else - ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); -#endif this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } +haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x01}).begin(), 2); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message( + haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, + std::initializer_list({0x00, 0x00}).begin(), 2); + return power_off_message; + } +} + haier_protocol::HaierMessage HonClimate::get_control_message() { uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; bool has_hvac_settings = false; - if (this->hvac_settings_.valid) { + if (this->current_hvac_settings_.valid) { has_hvac_settings = true; - HvacSettings climate_control; - climate_control = this->hvac_settings_; + HvacSettings &climate_control = this->current_hvac_settings_; if (climate_control.mode.has_value()) { switch (climate_control.mode.value()) { case CLIMATE_MODE_OFF: @@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { } if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); - out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { @@ -587,50 +590,28 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { control_out_buffer[4] = 0; // This byte should be cleared before setting values out_data->display_status = this->display_status_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0; - switch (this->action_request_) { - case ActionRequest::START_SELF_CLEAN: - this->action_request_ = ActionRequest::NO_ACTION; - out_data->self_cleaning_status = 1; - out_data->steri_clean = 0; - out_data->set_point = 0x06; - out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; - out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; - out_data->ac_power = 1; - out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; - out_data->light_status = 0; - break; - case ActionRequest::START_STERI_CLEAN: - this->action_request_ = ActionRequest::NO_ACTION; - out_data->self_cleaning_status = 0; - out_data->steri_clean = 1; - out_data->set_point = 0x06; - out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; - out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; - out_data->ac_power = 1; - out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; - out_data->light_status = 0; - break; - default: - // No change - break; - } - return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL, + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); } haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < sizeof(hon_protocol::HaierStatus)) + if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; - hon_protocol::HaierStatus packet; - if (size < sizeof(hon_protocol::HaierStatus)) - size = sizeof(hon_protocol::HaierStatus); - memcpy(&packet, packet_buffer, size); + struct { + hon_protocol::HaierPacketControl control; + hon_protocol::HaierPacketSensors sensors; + } packet; + memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl)); + memcpy(&packet.sensors, + packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_, + sizeof(hon_protocol::HaierPacketSensors)); if (packet.sensors.error_status != 0) { ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); } - if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { - got_valid_outdoor_temp_ = true; + if ((this->outdoor_sensor_ != nullptr) && + (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { + this->got_valid_outdoor_temp_ = true; float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) this->outdoor_sensor_->publish_state(otemp); @@ -703,7 +684,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Do something only if display status changed if (this->mode == CLIMATE_MODE_OFF) { // AC just turned on from remote need to turn off display - this->set_force_send_control_(true); + this->force_send_control_ = true; } else { this->display_status_ = disp_status; } @@ -732,7 +713,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); if (new_cleaning == CleaningState::NO_CLEANING) { // Turning AC off after cleaning - this->action_request_ = ActionRequest::TURN_POWER_OFF; + this->action_request_ = + PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional()}); } this->cleaning_status_ = new_cleaning; } @@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * should_publish = should_publish || (old_swing_mode != this->swing_mode); } this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); - if (this->forced_publish_ || should_publish) { -#if (HAIER_LOG_LEVEL > 4) - std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); -#endif + if (should_publish) { this->publish_state(); -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGV(TAG, "Publish delay: %lld ms", - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - - _publish_start) - .count()); -#endif - this->forced_publish_ = false; } if (should_publish) { ESP_LOGI(TAG, "HVAC values changed"); } - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "HVAC Mode = 0x%X", packet.control.ac_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Fan speed Status = 0x%X", packet.control.fan_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Set Point Status = 0x%X", packet.control.set_point); + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); return haier_protocol::HandlerError::HANDLER_OK; } -bool HonClimate::is_message_invalid(uint8_t message_type) { - return message_type == (uint8_t) hon_protocol::FrameType::INVALID; +void HonClimate::fill_control_messages_queue_() { + static uint8_t one_buf[] = {0x00, 0x01}; + static uint8_t zero_buf[] = {0x00, 0x00}; + if (!this->current_hvac_settings_.valid && !this->force_send_control_) + return; + this->clear_control_messages_queue_(); + HvacSettings climate_control; + climate_control = this->current_hvac_settings_; + // Beeper command + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS, + this->beeper_status_ ? zero_buf : one_buf, 2)); + } + // Health mode + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::HEALTH_MODE, + this->health_mode_ ? one_buf : zero_buf, 2)); + } + // Climate mode + bool new_power = this->mode != CLIMATE_MODE_OFF; + uint8_t fan_mode_buf[] = {0x00, 0xFF}; + uint8_t quiet_mode_buf[] = {0x00, 0xFF}; + if (climate_control.mode.has_value()) { + uint8_t buffer[2] = {0x00, 0x00}; + switch (climate_control.mode.value()) { + case CLIMATE_MODE_OFF: + new_power = false; + break; + case CLIMATE_MODE_HEAT_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_HEAT: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_DRY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + case CLIMATE_MODE_FAN_ONLY: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode + // Disabling eco mode for Fan only + quiet_mode_buf[1] = 0; + break; + case CLIMATE_MODE_COOL: + new_power = true; + buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_MODE, + buffer, 2)); + fan_mode_buf[1] = this->other_modes_fan_speed_; + break; + default: + ESP_LOGE("Control", "Unsupported climate mode"); + break; + } + } + // Climate power + { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::AC_POWER, + new_power ? one_buf : zero_buf, 2)); + } + // CLimate preset + { + uint8_t fast_mode_buf[] = {0x00, 0xFF}; + if (!new_power) { + // If AC is off - no presets allowed + quiet_mode_buf[1] = 0x00; + fast_mode_buf[1] = 0x00; + } else if (climate_control.preset.has_value()) { + switch (climate_control.preset.value()) { + case CLIMATE_PRESET_NONE: + quiet_mode_buf[1] = 0x00; + fast_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_ECO: + // Eco is not supported in Fan only mode + quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + fast_mode_buf[1] = 0x00; + break; + case CLIMATE_PRESET_BOOST: + quiet_mode_buf[1] = 0x00; + // Boost is not supported in Fan only mode + fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; + break; + default: + ESP_LOGE("Control", "Unsupported preset"); + break; + } + } + if (quiet_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::QUIET_MODE, + quiet_mode_buf, 2)); + } + if (fast_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAST_MODE, + fast_mode_buf, 2)); + } + } + // Target temperature + if (climate_control.target_temperature.has_value()) { + uint8_t buffer[2] = {0x00, 0x00}; + buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16; + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::SET_POINT, + buffer, 2)); + } + // Fan mode + if (climate_control.fan_mode.has_value()) { + switch (climate_control.fan_mode.value()) { + case CLIMATE_FAN_LOW: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW; + break; + case CLIMATE_FAN_MEDIUM: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID; + break; + case CLIMATE_FAN_HIGH: + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_HIGH; + break; + case CLIMATE_FAN_AUTO: + if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode + fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO; + break; + default: + ESP_LOGE("Control", "Unsupported fan mode"); + break; + } + if (fan_mode_buf[1] != 0xFF) { + this->control_messages_queue_.push( + haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::FAN_MODE, + fan_mode_buf, 2)); + } + } } -void HonClimate::process_pending_action() { - switch (this->action_request_) { - case ActionRequest::START_SELF_CLEAN: - case ActionRequest::START_STERI_CLEAN: - // Will reset action with control message sending - this->set_phase(ProtocolPhases::SENDING_CONTROL); - break; +void HonClimate::clear_control_messages_queue_() { + while (!this->control_messages_queue_.empty()) + this->control_messages_queue_.pop(); +} + +bool HonClimate::prepare_pending_action() { + switch (this->action_request_.value().action) { + case ActionRequest::START_SELF_CLEAN: { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 1; + out_data->steri_clean = 0; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); + } + return true; + case ActionRequest::START_STERI_CLEAN: { + uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; + memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); + hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; + out_data->self_cleaning_status = 0; + out_data->steri_clean = 1; + out_data->set_point = 0x06; + out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER; + out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER; + out_data->ac_power = 1; + out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; + out_data->light_status = 0; + this->action_request_.value().message = haier_protocol::HaierMessage( + haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, + control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); + } + return true; default: - HaierClimateBase::process_pending_action(); - break; + return HaierClimateBase::prepare_pending_action(); } } +void HonClimate::process_protocol_reset() { + HaierClimateBase::process_protocol_reset(); + if (this->outdoor_sensor_ != nullptr) { + this->outdoor_sensor_->publish_state(NAN); + } + this->got_valid_outdoor_temp_ = false; + this->hvac_hardware_info_.reset(); +} + } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index cf566e3b8e..1ba6a8e041 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -30,6 +30,8 @@ enum class CleaningState : uint8_t { STERI_CLEAN = 2, }; +enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; + class HonClimate : public HaierClimateBase { public: HonClimate(); @@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase { CleaningState get_cleaning_status() const; void start_self_cleaning(); void start_steri_cleaning(); + void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; + void set_control_method(HonControlMethod method) { this->control_method_ = method; }; protected: void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; haier_protocol::HaierMessage get_control_message() override; - bool is_message_invalid(uint8_t message_type) override; - void process_pending_action() override; + haier_protocol::HaierMessage get_power_message(bool state) override; + bool prepare_pending_action() override; + void process_protocol_reset() override; // Answers handlers - haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); - std::unique_ptr last_status_message_; + void fill_control_messages_queue_(); + void clear_control_messages_queue_(); + + struct HardwareInfo { + std::string protocol_version_; + std::string software_version_; + std::string hardware_version_; + std::string device_name_; + bool functions_[5]; + }; + bool beeper_status_; CleaningState cleaning_status_; bool got_valid_outdoor_temp_; AirflowVerticalDirection vertical_direction_; AirflowHorizontalDirection horizontal_direction_; - bool hvac_hardware_info_available_; - std::string hvac_protocol_version_; - std::string hvac_software_version_; - std::string hvac_hardware_version_; - std::string hvac_device_name_; - bool hvac_functions_[5]; - bool &use_crc_; + esphome::optional hvac_hardware_info_; uint8_t active_alarms_[8]; + int extra_control_packet_bytes_; + HonControlMethod control_method_; esphome::sensor::Sensor *outdoor_sensor_; + std::queue control_messages_queue_; }; } // namespace haier diff --git a/esphome/components/haier/hon_packet.h b/esphome/components/haier/hon_packet.h index c6b32df200..7724b43854 100644 --- a/esphome/components/haier/hon_packet.h +++ b/esphome/components/haier/hon_packet.h @@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t { FAN = 0x06 }; +enum class DataParameters : uint8_t { + AC_POWER = 0x01, + SET_POINT = 0x02, + AC_MODE = 0x04, + FAN_MODE = 0x05, + USE_FAHRENHEIT = 0x07, + TEN_DEGREE = 0x0A, + HEALTH_MODE = 0x0B, + BEEPER_STATUS = 0x16, + LOCK_REMOTE = 0x17, + QUIET_MODE = 0x19, + FAST_MODE = 0x1A, +}; + enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; @@ -124,11 +138,7 @@ struct HaierPacketSensors { uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) }; -struct HaierStatus { - uint16_t subcommand; - HaierPacketControl control; - HaierPacketSensors sensors; -}; +constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors); struct DeviceVersionAnswer { char protocol_version[8]; @@ -140,76 +150,6 @@ struct DeviceVersionAnswer { uint8_t functions[2]; }; -// In this section comments: -// - module is the ESP32 control module (communication module in Haier protocol document) -// - device is the conditioner control board (network appliances in Haier protocol document) -enum class FrameType : uint8_t { - CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required) - STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device, - // required) - INVALID = 0x03, // Communication error indication (module <-> device, required) - ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required) - CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module - // <-> device, required) - REPORT = 0x06, // Report frame (module <-> device, interactive, required) - STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required) - SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional) - DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional) - SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional) - SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional) - DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional) - DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional) - GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional) - GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required) - GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_ - GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional) - GET_ALL_ADDRESSES_RESPONSE = - 0x68, // Answer to request of all devices addresses (module <- device , interactive, optional) - HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional) - GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required) - GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required) - GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required) - GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required) - GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required) - GET_DEVICE_CONFIGURATION_RESPONSE = - 0x7D, // Response to device configuration request (module <- device, interactive, required) - DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device) - // (module -> device, interactive, optional) - UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module - // <- device, interactive, optional) - START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required) - START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required) - GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required) - GET_FIRMWARE_CONTENT_RESPONSE = - 0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?) - CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required) - CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required) - GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required) - GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required) - GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required) - GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required) - GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required) - GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required) - GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional) - GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional) - START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required) - START_WIFI_CONFIGURATION_RESPONSE = - 0xF3, // Response to start WiFi configuration request (module -> device, interactive, required) - STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required) - STOP_WIFI_CONFIGURATION_RESPONSE = - 0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required) - REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required) - CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional) - BIG_DATA_REPORT_CONFIGURATION = - 0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional) - BIG_DATA_REPORT_CONFIGURATION_RESPONSE = - 0xFB, // Response to set big data configuration (module <- device, interactive, optional) - GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required) - GET_MANAGEMENT_INFORMATION_RESPONSE = - 0xFD, // Response to management information request (module <- device, required) - WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional) -}; - enum class SubcommandsControl : uint16_t { GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index f29f840088..c2326883f7 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -12,21 +12,28 @@ namespace haier { static const char *const TAG = "haier.climate"; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; +constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; +constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); +constexpr uint8_t INIT_REQUESTS_RETRY = 2; +constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000); -Smartair2Climate::Smartair2Climate() - : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} +Smartair2Climate::Smartair2Climate() { + last_status_message_ = std::unique_ptr(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]); +} -haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, +haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { haier_protocol::HandlerError result = - this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, - (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); + this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, + haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); if (result == haier_protocol::HandlerError::HANDLER_OK) { result = this->process_status_message_(data, data_size); if (result != haier_protocol::HandlerError::HANDLER_OK) { ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->reset_phase_(); + this->action_request_.reset(); + this->force_send_control_ = false; } else { if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); @@ -34,36 +41,45 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, sizeof(smartair2_protocol::HaierPacketControl)); } - if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { - ESP_LOGI(TAG, "First HVAC status received"); - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { - this->set_phase(ProtocolPhases::IDLE); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); + switch (this->protocol_phase_) { + case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: + ESP_LOGI(TAG, "First HVAC status received"); + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + // Do nothing, phase will be changed in process_phase + break; + case ProtocolPhases::SENDING_STATUS_REQUEST: + this->set_phase(ProtocolPhases::IDLE); + break; + case ProtocolPhases::SENDING_CONTROL: + this->set_phase(ProtocolPhases::IDLE); + this->force_send_control_ = false; + if (this->current_hvac_settings_.valid) + this->current_hvac_settings_.reset(); + break; + default: + break; } } return result; } else { - this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE - : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); + this->action_request_.reset(); + this->force_send_control_ = false; + this->reset_phase_(); return result; } } -haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) +haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_( + haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, + size_t data_size) { + if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION) return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; - if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) + if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_) return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; // Invalid packet is expected answer - if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && + if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && ((data[37] & 0x04) != 0)) { ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " "instead of smartAir2"); @@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler return haier_protocol::HandlerError::HANDLER_OK; } -haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, - uint8_t message_type, - const uint8_t *data, - size_t data_size) { - haier_protocol::HandlerError result = this->answer_preprocess_( - request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, - (uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); - this->set_phase(ProtocolPhases::IDLE); - return result; -} - -haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) { +haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_( + haier_protocol::FrameType message_type) { if (this->protocol_phase_ >= ProtocolPhases::IDLE) return HaierClimateBase::timeout_default_handler_(message_type); - this->timeouts_counter_++; - ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, - (int) this->protocol_phase_, this->timeouts_counter_); - if (this->timeouts_counter_ >= 3) { - ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); - if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) - new_phase = ProtocolPhases::SENDING_INIT_1; - this->set_phase(new_phase); - } else { - // Returning to the previous state to try again - this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1)); - } + ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, + phase_to_string_(this->protocol_phase_)); + ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); + if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) + new_phase = ProtocolPhases::SENDING_INIT_1; + this->set_phase(new_phase); return haier_protocol::HandlerError::HANDLER_OK; } void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), + haier_protocol::FrameType::GET_DEVICE_VERSION, std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::CONTROL), + haier_protocol::FrameType::CONTROL, std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); this->haier_protocol_.set_answer_handler( - (uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), + haier_protocol::FrameType::REPORT_NETWORK_STATUS, std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); - this->haier_protocol_.set_timeout_handler( - (uint8_t) (smartair2_protocol::FrameType::CONTROL), - std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); + this->haier_protocol_.set_default_timeout_handler( + std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); } void Smartair2Climate::dump_config() { @@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() { void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { switch (this->protocol_phase_) { case ProtocolPhases::SENDING_INIT_1: - if (this->can_send_message() && - (((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) || - ((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) { + if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { // Indicate device capabilities: // bit 0 - if 1 module support interactive mode // bit 1 - if 1 module support controller-device mode @@ -145,92 +136,65 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) // bit 4..bit 15 - not used uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( - (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, - sizeof(module_capabilities)); - this->send_message_(DEVICE_VERSION_REQUEST, false); - this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); + haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); + this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); } break; case ProtocolPhases::SENDING_INIT_2: - case ProtocolPhases::WAITING_INIT_2_ANSWER: this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); break; case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, - 0x4D01); - this->send_message_(STATUS_REQUEST, false); + static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01); + if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) { + this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); + } else { + this->send_message_(STATUS_REQUEST, this->use_crc_); + } this->last_status_request_ = now; - this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); } break; #ifdef USE_WIFI case ProtocolPhases::SENDING_SIGNAL_LEVEL: if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - this->send_message_( - this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false); + this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); this->last_signal_request_ = now; - this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); } break; - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: - break; #else case ProtocolPhases::SENDING_SIGNAL_LEVEL: - case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: this->set_phase(ProtocolPhases::IDLE); break; #endif case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: - case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); break; case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: - case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: this->set_phase(ProtocolPhases::SENDING_INIT_1); break; case ProtocolPhases::SENDING_CONTROL: - if (this->first_control_attempt_) { - this->control_request_timestamp_ = now; - this->first_control_attempt_ = false; + if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { + ESP_LOGI(TAG, "Sending control packet"); + this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES, + CONTROL_MESSAGE_RETRIES_INTERVAL); } - if (this->is_control_message_timeout_exceeded_(now)) { - ESP_LOGW(TAG, "Sending control packet timeout!"); - this->set_force_send_control_(false); - if (this->hvac_settings_.valid) - this->hvac_settings_.reset(); - this->forced_request_status_ = true; - this->forced_publish_ = true; + break; + case ProtocolPhases::SENDING_ACTION_COMMAND: + if (this->action_request_.has_value()) { + if (this->action_request_.value().message.has_value()) { + this->send_message_(this->action_request_.value().message.value(), this->use_crc_); + this->action_request_.value().message.reset(); + } else { + // Message already sent, reseting request and return to idle + this->action_request_.reset(); + this->set_phase(ProtocolPhases::IDLE); + } + } else { + ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); this->set_phase(ProtocolPhases::IDLE); - } else if (this->can_send_message() && this->is_control_message_interval_exceeded_( - now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests - { - haier_protocol::HaierMessage control_message = get_control_message(); - this->send_message_(control_message, false); - ESP_LOGI(TAG, "Control packet sent"); - this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); } break; - case ProtocolPhases::SENDING_POWER_ON_COMMAND: - case ProtocolPhases::SENDING_POWER_OFF_COMMAND: - if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { - haier_protocol::HaierMessage power_cmd( - (uint8_t) smartair2_protocol::FrameType::CONTROL, - this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03); - this->send_message_(power_cmd, false); - this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND - ? ProtocolPhases::WAITING_POWER_ON_ANSWER - : ProtocolPhases::WAITING_POWER_OFF_ANSWER); - } - break; - case ProtocolPhases::WAITING_INIT_1_ANSWER: - case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER: - case ProtocolPhases::WAITING_STATUS_ANSWER: - case ProtocolPhases::WAITING_CONTROL_ANSWER: - case ProtocolPhases::WAITING_POWER_ON_ANSWER: - case ProtocolPhases::WAITING_POWER_OFF_ANSWER: - break; case ProtocolPhases::IDLE: { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); @@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) } break; default: // Shouldn't get here -#if (HAIER_LOG_LEVEL > 4) ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); -#else - ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_); -#endif this->set_phase(ProtocolPhases::SENDING_INIT_1); break; } } +haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) { + if (state) { + static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02); + return power_on_message; + } else { + static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03); + return power_off_message; + } +} + haier_protocol::HaierMessage Smartair2Climate::get_control_message() { uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; out_data->cntrl = 0; - if (this->hvac_settings_.valid) { - HvacSettings climate_control; - climate_control = this->hvac_settings_; + if (this->current_hvac_settings_.valid) { + HvacSettings &climate_control = this->current_hvac_settings_; if (climate_control.mode.has_value()) { switch (climate_control.mode.value()) { case CLIMATE_MODE_OFF: out_data->ac_power = 0; break; - case CLIMATE_MODE_HEAT_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_HEAT: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_DRY: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; out_data->fan_mode = this->other_modes_fan_speed_; break; - case CLIMATE_MODE_FAN_ONLY: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode break; - case CLIMATE_MODE_COOL: out_data->ac_power = 1; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; @@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } // Set swing mode if (climate_control.swing_mode.has_value()) { - switch (climate_control.swing_mode.value()) { - case CLIMATE_SWING_OFF: - out_data->use_swing_bits = 0; - out_data->swing_both = 0; - break; - case CLIMATE_SWING_VERTICAL: - out_data->swing_both = 0; - out_data->vertical_swing = 1; - out_data->horizontal_swing = 0; - break; - case CLIMATE_SWING_HORIZONTAL: - out_data->swing_both = 0; - out_data->vertical_swing = 0; - out_data->horizontal_swing = 1; - break; - case CLIMATE_SWING_BOTH: - out_data->swing_both = 1; - out_data->use_swing_bits = 0; - out_data->vertical_swing = 0; - out_data->horizontal_swing = 0; - break; + if (this->use_alternative_swing_control_) { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 1; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 2; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 3; + break; + } + } else { + switch (climate_control.swing_mode.value()) { + case CLIMATE_SWING_OFF: + out_data->use_swing_bits = 0; + out_data->swing_mode = 0; + break; + case CLIMATE_SWING_VERTICAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 1; + out_data->horizontal_swing = 0; + break; + case CLIMATE_SWING_HORIZONTAL: + out_data->swing_mode = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 1; + break; + case CLIMATE_SWING_BOTH: + out_data->swing_mode = 1; + out_data->use_swing_bits = 0; + out_data->vertical_swing = 0; + out_data->horizontal_swing = 0; + break; + } } } if (climate_control.target_temperature.has_value()) { float target_temp = climate_control.target_temperature.value(); - out_data->set_point = target_temp - 16; // set the temperature with offset 16 + out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16 out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; } if (out_data->ac_power == 0) { @@ -383,7 +364,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } out_data->display_status = this->display_status_ ? 0 : 1; out_data->health_mode = this->health_mode_ ? 1 : 0; - return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, + return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, sizeof(smartair2_protocol::HaierPacketControl)); } @@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin // Do something only if display status changed if (this->mode == CLIMATE_MODE_OFF) { // AC just turned on from remote need to turn off display - this->set_force_send_control_(true); + this->force_send_control_ = true; } else { this->display_status_ = disp_status; } } } } + { + // Health mode + bool old_health_mode = this->health_mode_; + this->health_mode_ = packet.control.health_mode == 1; + should_publish = should_publish || (old_health_mode != this->health_mode_); + } { // Climate mode ClimateMode old_mode = this->mode; @@ -493,70 +480,57 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } should_publish = should_publish || (old_mode != this->mode); } - { - // Health mode - bool old_health_mode = this->health_mode_; - this->health_mode_ = packet.control.health_mode == 1; - should_publish = should_publish || (old_health_mode != this->health_mode_); - } { // Swing mode ClimateSwingMode old_swing_mode = this->swing_mode; - if (packet.control.swing_both == 0) { - if (packet.control.vertical_swing != 0) { - this->swing_mode = CLIMATE_SWING_VERTICAL; - } else if (packet.control.horizontal_swing != 0) { - this->swing_mode = CLIMATE_SWING_HORIZONTAL; - } else { - this->swing_mode = CLIMATE_SWING_OFF; + if (this->use_alternative_swing_control_) { + switch (packet.control.swing_mode) { + case 1: + this->swing_mode = CLIMATE_SWING_VERTICAL; + break; + case 2: + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + break; + case 3: + this->swing_mode = CLIMATE_SWING_BOTH; + break; + default: + this->swing_mode = CLIMATE_SWING_OFF; + break; } } else { - swing_mode = CLIMATE_SWING_BOTH; + if (packet.control.swing_mode == 0) { + if (packet.control.vertical_swing != 0) { + this->swing_mode = CLIMATE_SWING_VERTICAL; + } else if (packet.control.horizontal_swing != 0) { + this->swing_mode = CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = CLIMATE_SWING_OFF; + } + } else { + swing_mode = CLIMATE_SWING_BOTH; + } } should_publish = should_publish || (old_swing_mode != this->swing_mode); } this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); - if (this->forced_publish_ || should_publish) { -#if (HAIER_LOG_LEVEL > 4) - std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now(); -#endif + if (should_publish) { this->publish_state(); -#if (HAIER_LOG_LEVEL > 4) - ESP_LOGV(TAG, "Publish delay: %lld ms", - std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - - _publish_start) - .count()); -#endif - this->forced_publish_ = false; } if (should_publish) { ESP_LOGI(TAG, "HVAC values changed"); } - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "HVAC Mode = 0x%X", packet.control.ac_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Fan speed Status = 0x%X", packet.control.fan_mode); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Vertical Swing Status = 0x%X", packet.control.vertical_swing); - esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, - "Set Point Status = 0x%X", packet.control.set_point); + int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; + esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); + esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing); + esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); return haier_protocol::HandlerError::HANDLER_OK; } -bool Smartair2Climate::is_message_invalid(uint8_t message_type) { - return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; -} - -void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) { - int old_phase = (int) this->protocol_phase_; - int new_phase = (int) phase; - int min_p = std::min(old_phase, new_phase); - int max_p = std::max(old_phase, new_phase); - if ((min_p % 2 != 0) || (max_p - min_p > 1)) - this->timeouts_counter_ = 0; - HaierClimateBase::set_phase(phase); +void Smartair2Climate::set_alternative_swing_control(bool swing_control) { + this->use_alternative_swing_control_ = swing_control; } } // namespace haier diff --git a/esphome/components/haier/smartair2_climate.h b/esphome/components/haier/smartair2_climate.h index f173b10749..6914d8a1fb 100644 --- a/esphome/components/haier/smartair2_climate.h +++ b/esphome/components/haier/smartair2_climate.h @@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase { Smartair2Climate &operator=(const Smartair2Climate &) = delete; ~Smartair2Climate(); void dump_config() override; + void set_alternative_swing_control(bool swing_control); protected: void set_handlers() override; void process_phase(std::chrono::steady_clock::time_point now) override; + haier_protocol::HaierMessage get_power_message(bool state) override; haier_protocol::HaierMessage get_control_message() override; - bool is_message_invalid(uint8_t message_type) override; - void set_phase(HaierClimateBase::ProtocolPhases phase) override; - // Answer and timeout handlers - haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, + // Answer handlers + haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, + haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, + haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size); - haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, - const uint8_t *data, size_t data_size); - haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type); + haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type); // Helper functions haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); - std::unique_ptr last_status_message_; - unsigned int timeouts_counter_; + bool use_alternative_swing_control_; }; } // namespace haier diff --git a/esphome/components/haier/smartair2_packet.h b/esphome/components/haier/smartair2_packet.h index f791c21af2..22570ff048 100644 --- a/esphome/components/haier/smartair2_packet.h +++ b/esphome/components/haier/smartair2_packet.h @@ -41,8 +41,9 @@ struct HaierPacketControl { // 24 uint8_t : 8; // 25 - uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define - // vertical/horizontal/off + uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and + // vertical_swing define vertical/horizontal/off + // In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both // 26 uint8_t : 3; uint8_t use_fahrenheit : 1; @@ -82,19 +83,6 @@ struct HaierStatus { HaierPacketControl control; }; -enum class FrameType : uint8_t { - CONTROL = 0x01, - STATUS = 0x02, - INVALID = 0x03, - CONFIRM = 0x05, - GET_DEVICE_VERSION = 0x61, - GET_DEVICE_VERSION_RESPONSE = 0x62, - GET_DEVICE_ID = 0x70, - GET_DEVICE_ID_RESPONSE = 0x71, - REPORT_NETWORK_STATUS = 0xF7, - NO_COMMAND = 0xFF, -}; - } // namespace smartair2_protocol } // namespace haier } // namespace esphome diff --git a/platformio.ini b/platformio.ini index cbd87155be..68c4220aab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,7 +39,7 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 - pavlodn/HaierProtocol@0.9.20 ; haier + pavlodn/HaierProtocol@0.9.24 ; haier ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library build_flags = From e7038d077a98f3fd8f6420bc81267a1aa0132de9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:24:47 +1300 Subject: [PATCH 249/270] Early return when there are no wifi scan results (#5797) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 8fcafc5c12..0035733553 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -686,6 +686,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } + if (it.number == 0) { + // no results + return; + } + uint16_t number = it.number; std::vector records(number); err = esp_wifi_scan_get_ap_records(&number, records.data()); From b809d0284662ca6798cb807e9d5dc47f8ea97f17 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Nov 2023 15:09:14 -0600 Subject: [PATCH 250/270] Add some additional VA triggers, part 2 (#5811) --- .../components/voice_assistant/__init__.py | 34 +++++++++++++++++++ .../voice_assistant/voice_assistant.cpp | 6 ++++ .../voice_assistant/voice_assistant.h | 8 +++++ 3 files changed, 48 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 5715604605..d05f39072c 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -29,6 +29,8 @@ CONF_ON_STT_VAD_END = "on_stt_vad_end" CONF_ON_STT_VAD_START = "on_stt_vad_start" CONF_ON_TTS_END = "on_tts_end" CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_STREAM_START = "on_tts_stream_start" +CONF_ON_TTS_STREAM_END = "on_tts_stream_end" CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" CONF_SILENCE_DETECTION = "silence_detection" @@ -56,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) + +def tts_stream_validate(config): + if CONF_SPEAKER not in config and ( + CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config + ): + raise cv.Invalid( + f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}" + ) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -105,8 +118,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), + tts_stream_validate, ) @@ -222,6 +242,20 @@ async def to_code(config): config[CONF_ON_STT_VAD_END], ) + if CONF_ON_TTS_STREAM_START in config: + await automation.build_automation( + var.get_tts_stream_start_trigger(), + [], + config[CONF_ON_TTS_STREAM_START], + ) + + if CONF_ON_TTS_STREAM_END in config: + await automation.build_automation( + var.get_tts_stream_end_trigger(), + [], + config[CONF_ON_TTS_STREAM_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ebbe762b3..9b13a71039 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -632,11 +632,17 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->tts_stream_start_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); +#ifdef USE_SPEAKER + ESP_LOGD(TAG, "TTS stream end"); + this->tts_stream_end_trigger_->trigger(); +#endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a985bc4678..f6dcd1c563 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -107,6 +107,10 @@ class VoiceAssistant : public Component { Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } +#ifdef USE_SPEAKER + Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } +#endif Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } @@ -135,6 +139,10 @@ class VoiceAssistant : public Component { Trigger<> *start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); +#ifdef USE_SPEAKER + Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); + Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); +#endif Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); From 1c4b06700fe3b89bb2bc1f4bcbb6f9af655c286e Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 20 Nov 2023 16:59:38 -0700 Subject: [PATCH 251/270] include payload_open when a lock supports OPEN (#5809) --- esphome/components/mqtt/mqtt_lock.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 197d0c32d4..f4a5126d0c 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { if (this->lock_->traits.get_assumed_state()) root[MQTT_OPTIMISTIC] = true; + if (this->lock_->traits.get_supports_open()) + root[MQTT_PAYLOAD_OPEN] = "OPEN"; } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } From d0ac202a3f81822c16aff0cef3a53a5938cd018a Mon Sep 17 00:00:00 2001 From: CVan Date: Mon, 20 Nov 2023 19:19:36 -0500 Subject: [PATCH 252/270] fix: compile errors with fonts (#5808) --- docker/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a892e1df38..1bf754464d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,6 +48,8 @@ RUN \ libfreetype-dev=2.12.1+dfsg-5 \ libssl-dev=3.0.11-1~deb12u2 \ libffi-dev=3.4.4-1 \ + libopenjp2-7=2.5.0-2 \ + libtiff6=4.5.0-6 \ cargo=0.66.0+ds1-1 \ pkg-config=1.8.1-1 \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \ From 10ca05b686cf0ffa5d1483bf38aa5e95e2fde77d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:24:47 +1300 Subject: [PATCH 253/270] Early return when there are no wifi scan results (#5797) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 34ecaf887d..56cae1b1cc 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -674,6 +674,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } + if (it.number == 0) { + // no results + return; + } + uint16_t number = it.number; std::vector records(number); err = esp_wifi_scan_get_ap_records(&number, records.data()); From b421fccc08c5824df5cc4fed9b3d8602344e4ffa Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Nov 2023 15:09:14 -0600 Subject: [PATCH 254/270] Add some additional VA triggers, part 2 (#5811) --- .../components/voice_assistant/__init__.py | 34 +++++++++++++++++++ .../voice_assistant/voice_assistant.cpp | 6 ++++ .../voice_assistant/voice_assistant.h | 8 +++++ 3 files changed, 48 insertions(+) diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 5715604605..d05f39072c 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -29,6 +29,8 @@ CONF_ON_STT_VAD_END = "on_stt_vad_end" CONF_ON_STT_VAD_START = "on_stt_vad_start" CONF_ON_TTS_END = "on_tts_end" CONF_ON_TTS_START = "on_tts_start" +CONF_ON_TTS_STREAM_START = "on_tts_stream_start" +CONF_ON_TTS_STREAM_END = "on_tts_stream_end" CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected" CONF_SILENCE_DETECTION = "silence_detection" @@ -56,6 +58,17 @@ IsRunningCondition = voice_assistant_ns.class_( "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) ) + +def tts_stream_validate(config): + if CONF_SPEAKER not in config and ( + CONF_ON_TTS_STREAM_START in config or CONF_ON_TTS_STREAM_END in config + ): + raise cv.Invalid( + f"{CONF_SPEAKER} is required when using {CONF_ON_TTS_STREAM_START} and/or {CONF_ON_TTS_STREAM_END}" + ) + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -105,8 +118,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_TTS_STREAM_START): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_TTS_STREAM_END): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), + tts_stream_validate, ) @@ -222,6 +242,20 @@ async def to_code(config): config[CONF_ON_STT_VAD_END], ) + if CONF_ON_TTS_STREAM_START in config: + await automation.build_automation( + var.get_tts_stream_start_trigger(), + [], + config[CONF_ON_TTS_STREAM_START], + ) + + if CONF_ON_TTS_STREAM_END in config: + await automation.build_automation( + var.get_tts_stream_end_trigger(), + [], + config[CONF_ON_TTS_STREAM_END], + ) + cg.add_define("USE_VOICE_ASSISTANT") diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ebbe762b3..9b13a71039 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -632,11 +632,17 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->tts_stream_start_trigger_->trigger(); #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { this->set_state_(State::RESPONSE_FINISHED, State::IDLE); +#ifdef USE_SPEAKER + ESP_LOGD(TAG, "TTS stream end"); + this->tts_stream_end_trigger_->trigger(); +#endif break; } case api::enums::VOICE_ASSISTANT_STT_VAD_START: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a985bc4678..f6dcd1c563 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -107,6 +107,10 @@ class VoiceAssistant : public Component { Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } +#ifdef USE_SPEAKER + Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } +#endif Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } @@ -135,6 +139,10 @@ class VoiceAssistant : public Component { Trigger<> *start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); +#ifdef USE_SPEAKER + Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); + Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); +#endif Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); From 3f40e32eba514e3561c092c49d16d3032fd84305 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:08:47 +1300 Subject: [PATCH 255/270] Bump version to 2023.11.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0abb6a8e2d..5f6e9f5eb1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.2" +__version__ = "2023.11.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 8738cef5a38a6212b20418e17200a6b9475cda98 Mon Sep 17 00:00:00 2001 From: matt7aylor Date: Wed, 22 Nov 2023 19:48:38 +0000 Subject: [PATCH 256/270] sen5x fix temperature compensation and gas tuning (#4901) --- esphome/components/sen5x/sen5x.cpp | 12 ++++++--- esphome/components/sen5x/sen5x.h | 40 +++++++++++++++++------------- esphome/components/sen5x/sensor.py | 13 +++++++++- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 42951d6089..c90880bc9f 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -201,13 +201,19 @@ void SEN5XComponent::setup() { ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); } } - if (this->voc_tuning_params_.has_value()) + 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()) + delay(20); + } + if (this->nox_tuning_params_.has_value()) { this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + delay(20); + } - if (this->temperature_compensation_.has_value()) + if (this->temperature_compensation_.has_value()) { this->write_temperature_compensation_(this->temperature_compensation_.value()); + delay(20); + } // Finally start sensor measurements auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index f306003a82..6d90636a89 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -41,8 +41,8 @@ struct GasTuning { }; struct TemperatureCompensation { - uint16_t offset; - uint16_t normalized_offset_slope; + int16_t offset; + int16_t normalized_offset_slope; uint16_t time_constant; }; @@ -70,27 +70,33 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri 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; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = std_initial; + tuning_params.gain_factor = gain_factor; + voc_tuning_params_ = tuning_params; } 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; + GasTuning tuning_params; + tuning_params.index_offset = index_offset; + tuning_params.learning_time_offset_hours = learning_time_offset_hours; + tuning_params.learning_time_gain_hours = learning_time_gain_hours; + tuning_params.gating_max_duration_minutes = gating_max_duration_minutes; + tuning_params.std_initial = 50; + tuning_params.gain_factor = gain_factor; + nox_tuning_params_ = tuning_params; } 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; + TemperatureCompensation temp_comp; + temp_comp.offset = offset * 200; + temp_comp.normalized_offset_slope = normalized_offset_slope * 10000; + temp_comp.time_constant = time_constant; + temperature_compensation_ = temp_comp; } bool start_fan_cleaning(); diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 392510e417..4bc4a138a3 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -88,6 +88,15 @@ GAS_SENSOR = cv.Schema( } ) + +def float_previously_pct(value): + if isinstance(value, str) and "%" in value: + raise cv.Invalid( + f"The value '{value}' is a percentage. Suggested value: {float(value.strip('%')) / 100}" + ) + return value + + CONFIG_SCHEMA = ( cv.Schema( { @@ -151,7 +160,9 @@ CONFIG_SCHEMA = ( 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_NORMALIZED_OFFSET_SLOPE, default=0): cv.All( + float_previously_pct, cv.float_ + ), cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, } ), From 3ac59180ab5bb3e1730e44017297666e61e03ee3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:31:23 +1100 Subject: [PATCH 257/270] Add startup_delay to interval. (#5327) --- esphome/components/interval/__init__.py | 6 +++++- esphome/components/interval/interval.h | 20 +++++++++++++++++++- tests/test2.yaml | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index 4514f80ba3..db3232c4b0 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_INTERVAL +from esphome.const import CONF_ID, CONF_INTERVAL, CONF_STARTUP_DELAY CODEOWNERS = ["@esphome/core"] interval_ns = cg.esphome_ns.namespace("interval") @@ -13,6 +13,9 @@ CONFIG_SCHEMA = automation.validate_automation( cv.Schema( { cv.GenerateID(): cv.declare_id(IntervalTrigger), + cv.Optional( + CONF_STARTUP_DELAY, default="0s" + ): cv.positive_time_period_milliseconds, cv.Required(CONF_INTERVAL): cv.positive_time_period_milliseconds, } ).extend(cv.COMPONENT_SCHEMA) @@ -26,3 +29,4 @@ async def to_code(config): await automation.build_automation(var, [], conf) cg.add(var.set_update_interval(conf[CONF_INTERVAL])) + cg.add(var.set_startup_delay(conf[CONF_STARTUP_DELAY])) diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 605ac868f3..5b8bc3081f 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -8,8 +8,26 @@ namespace interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: - void update() override { this->trigger(); } + void update() override { + if (this->started_) + this->trigger(); + } + + void setup() override { + if (this->startup_delay_ == 0) { + this->started_ = true; + } else { + this->set_timeout(this->startup_delay_, [this] { this->started_ = true; }); + } + } + + void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + uint32_t startup_delay_{0}; + bool started_{false}; }; } // namespace interval diff --git a/tests/test2.yaml b/tests/test2.yaml index bfc886eaa4..aaf14eaa57 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -718,6 +718,7 @@ stepper: interval: interval: 5s + startup_delay: 10s then: - logger.log: Interval Run From 49c09afb8755f62c96f11368027f9ba46017ffda Mon Sep 17 00:00:00 2001 From: Landon Rohatensky Date: Thu, 23 Nov 2023 11:10:33 -0800 Subject: [PATCH 258/270] Allow images to be downloaded from URLs (#5214) Co-authored-by: guillempages Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + esphome/components/image/__init__.py | 121 +++++++++++++++++++++------ esphome/external_files.py | 75 +++++++++++++++++ requirements.txt | 1 + tests/test2.yaml | 12 +++ 5 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 esphome/external_files.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bf754464d..ee7c70bb0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ RUN \ openssh-client=1:9.2p1-2+deb12u1 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ + libmagic1=1:5.44-3 \ patch=2.7.6-7; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 1b7c654b0b..c11021fc9c 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import logging +import hashlib import io from pathlib import Path import re import requests +from magic import Magic + +from PIL import Image from esphome import core from esphome.components import font +from esphome import external_files import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( + __version__, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -19,6 +27,7 @@ from esphome.const import ( CONF_RESIZE, CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt @@ -43,34 +52,74 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. -MDI_DOWNLOAD_TIMEOUT = 30 # seconds +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" +SOURCE_WEB = "web" + Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value) -> Path: - base_dir = Path(CORE.data_dir) / DOMAIN / "mdi" +def _compute_local_icon_path(value: dict) -> Path: + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" return base_dir / f"{value[CONF_ICON]}.svg" -def download_mdi(value): - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) - if path.is_file(): - return value - url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) +def _compute_local_image_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def download_content(url: str, path: Path) -> None: + if not external_files.has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed %s", url) + return + + _LOGGER.debug( + "Remote file has changed, downloading from %s to %s", + url, + path, + ) + try: - req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req = requests.get( + url, + timeout=IMAGE_DOWNLOAD_TIMEOUT, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) req.raise_for_status() except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(req.content) + + +def download_mdi(value): + validate_cairosvg_installed(value) + + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + + download_content(url, path) + + return value + + +def download_image(value): + url = value[CONF_URL] + path = _compute_local_image_path(value) + + download_content(url, path) + return value @@ -139,6 +188,13 @@ def validate_file_shorthand(value): CONF_ICON: icon, } ) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) return FILE_SCHEMA( { CONF_SOURCE: SOURCE_LOCAL, @@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All( download_mdi, ) +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + TYPED_FILE_SCHEMA = cv.typed_schema( { SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_MDI: MDI_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, }, key=CONF_SOURCE, ) @@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -def load_svg_image(file: str, resize: tuple[int, int]): - from PIL import Image - +def load_svg_image(file: bytes, resize: tuple[int, int]): # This import is only needed in case of SVG images; adding it # to the top would force configurations not using SVG to also have it # installed for no reason. @@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]): if resize: req_width, req_height = resize svg_image = svg2png( - url=file, + file, output_width=req_width, output_height=req_height, ) else: - svg_image = svg2png(url=file) + svg_image = svg2png(file) return Image.open(io.BytesIO(svg_image)) async def to_code(config): - from PIL import Image - conf_file = config[CONF_FILE] if conf_file[CONF_SOURCE] == SOURCE_LOCAL: @@ -233,17 +293,26 @@ async def to_code(config): elif conf_file[CONF_SOURCE] == SOURCE_MDI: path = _compute_local_icon_path(conf_file).as_posix() + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = _compute_local_image_path(conf_file).as_posix() + try: - resize = config.get(CONF_RESIZE) - if path.lower().endswith(".svg"): - image = load_svg_image(path, resize) - else: - image = Image.open(path) - if resize: - image.thumbnail(resize) + with open(path, "rb") as f: + file_contents = f.read() except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + mime = Magic(mime=True) + file_type = mime.from_buffer(file_contents) + + resize = config.get(CONF_RESIZE) + if "svg" in file_type: + image = load_svg_image(file_contents, resize) + else: + image = Image.open(io.BytesIO(file_contents)) + if resize: + image.thumbnail(resize) + width, height = image.size if CONF_RESIZE not in config and (width > 500 or height > 500): diff --git a/esphome/external_files.py b/esphome/external_files.py new file mode 100644 index 0000000000..5b476286f3 --- /dev/null +++ b/esphome/external_files.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import logging +from pathlib import Path +import os +from datetime import datetime +import requests +import esphome.config_validation as cv +from esphome.core import CORE, TimePeriodSeconds + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@landonr"] + +NETWORK_TIMEOUT = 30 + +IF_MODIFIED_SINCE = "If-Modified-Since" +CACHE_CONTROL = "Cache-Control" +CACHE_CONTROL_MAX_AGE = "max-age=" +CONTENT_DISPOSITION = "content-disposition" +TEMP_DIR = "temp" + + +def has_remote_file_changed(url, local_file_path): + if os.path.exists(local_file_path): + _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) + try: + local_modification_time = os.path.getmtime(local_file_path) + local_modification_time_str = datetime.utcfromtimestamp( + local_modification_time + ).strftime("%a, %d %b %Y %H:%M:%S GMT") + + headers = { + IF_MODIFIED_SINCE: local_modification_time_str, + CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", + } + response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT) + + _LOGGER.debug( + "has_remote_file_changed: File %s, Local modified %s, response code %d", + local_file_path, + local_modification_time_str, + response.status_code, + ) + + if response.status_code == 304: + _LOGGER.debug( + "has_remote_file_changed: File not modified since %s", + local_modification_time_str, + ) + return False + _LOGGER.debug("has_remote_file_changed: File modified") + return True + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not check if {url} has changed, please check if file exists " + f"({e})" + ) + + _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) + return True + + +def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: + if os.path.exists(file_path): + creation_time = os.path.getctime(file_path) + current_time = datetime.now().timestamp() + return current_time - creation_time <= refresh.total_seconds + return False + + +def compute_local_file_dir(domain: str) -> Path: + base_directory = Path(CORE.data_dir) / domain + base_directory.mkdir(parents=True, exist_ok=True) + + return base_directory diff --git a/requirements.txt b/requirements.txt index 4203ce6304..abbdcf66d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.5.5 zeroconf==0.127.0 +python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/tests/test2.yaml b/tests/test2.yaml index aaf14eaa57..b258b103d3 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -753,6 +753,18 @@ image: file: pnglogo.png type: RGB565 use_transparency: no + - id: web_svg_image + file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + resize: 256x48 + type: TRANSPARENT_BINARY + - id: web_tiff_image + file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + type: RGB24 + resize: 48x48 + - id: web_redirect_image + file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + type: RGB24 + resize: 48x48 - id: mdi_alert file: mdi:alert-circle-outline From 2076db1ccd6a258b24aab00d54cc743575aaca24 Mon Sep 17 00:00:00 2001 From: Ilia Sotnikov Date: Thu, 23 Nov 2023 21:15:58 +0200 Subject: [PATCH 259/270] Pillow: bump to 10.1.0 (#5815) --- esphome/components/font/__init__.py | 8 ++++---- requirements_optional.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7e34dff22d..22a5f6b2c5 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -67,13 +67,13 @@ def validate_pillow_installed(value): except ImportError as err: raise cv.Invalid( "Please install the pillow python package to use this feature. " - '(pip install "pillow==10.0.1")' + '(pip install "pillow==10.1.0")' ) from err - if version.parse(PIL.__version__) != version.parse("10.0.1"): + if version.parse(PIL.__version__) != version.parse("10.1.0"): raise cv.Invalid( - "Please update your pillow installation to 10.0.1. " - '(pip install "pillow==10.0.1")' + "Please update your pillow installation to 10.1.0. " + '(pip install "pillow==10.1.0")' ) return value diff --git a/requirements_optional.txt b/requirements_optional.txt index 40c27f8547..bc4ea08c92 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,3 +1,3 @@ -pillow==10.0.1 +pillow==10.1.0 cairosvg==2.7.1 cryptography==41.0.4 From 1204b4f1bd96b636086d7118e7f79550d86c37e0 Mon Sep 17 00:00:00 2001 From: Landon Rohatensky Date: Thu, 23 Nov 2023 11:10:33 -0800 Subject: [PATCH 260/270] Allow images to be downloaded from URLs (#5214) Co-authored-by: guillempages Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + esphome/components/image/__init__.py | 121 +++++++++++++++++++++------ esphome/external_files.py | 75 +++++++++++++++++ requirements.txt | 1 + tests/test2.yaml | 12 +++ 5 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 esphome/external_files.py diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bf754464d..ee7c70bb0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,6 +38,7 @@ RUN \ openssh-client=1:9.2p1-2+deb12u1 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ + libmagic1=1:5.44-3 \ patch=2.7.6-7; \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ apt-get install -y --no-install-recommends \ diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 1b7c654b0b..c11021fc9c 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import logging +import hashlib import io from pathlib import Path import re import requests +from magic import Magic + +from PIL import Image from esphome import core from esphome.components import font +from esphome import external_files import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( + __version__, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -19,6 +27,7 @@ from esphome.const import ( CONF_RESIZE, CONF_SOURCE, CONF_TYPE, + CONF_URL, ) from esphome.core import CORE, HexInt @@ -43,34 +52,74 @@ IMAGE_TYPE = { CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. -MDI_DOWNLOAD_TIMEOUT = 30 # seconds +IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" +SOURCE_WEB = "web" + Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value) -> Path: - base_dir = Path(CORE.data_dir) / DOMAIN / "mdi" +def _compute_local_icon_path(value: dict) -> Path: + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" return base_dir / f"{value[CONF_ICON]}.svg" -def download_mdi(value): - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) - if path.is_file(): - return value - url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) +def _compute_local_image_path(value: dict) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def download_content(url: str, path: Path) -> None: + if not external_files.has_remote_file_changed(url, path): + _LOGGER.debug("Remote file has not changed %s", url) + return + + _LOGGER.debug( + "Remote file has changed, downloading from %s to %s", + url, + path, + ) + try: - req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req = requests.get( + url, + timeout=IMAGE_DOWNLOAD_TIMEOUT, + headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, + ) req.raise_for_status() except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(req.content) + + +def download_mdi(value): + validate_cairosvg_installed(value) + + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + + download_content(url, path) + + return value + + +def download_image(value): + url = value[CONF_URL] + path = _compute_local_image_path(value) + + download_content(url, path) + return value @@ -139,6 +188,13 @@ def validate_file_shorthand(value): CONF_ICON: icon, } ) + if value.startswith("http://") or value.startswith("https://"): + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_WEB, + CONF_URL: value, + } + ) return FILE_SCHEMA( { CONF_SOURCE: SOURCE_LOCAL, @@ -160,10 +216,18 @@ MDI_SCHEMA = cv.All( download_mdi, ) +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.string, + }, + download_image, +) + TYPED_FILE_SCHEMA = cv.typed_schema( { SOURCE_LOCAL: LOCAL_SCHEMA, SOURCE_MDI: MDI_SCHEMA, + SOURCE_WEB: WEB_SCHEMA, }, key=CONF_SOURCE, ) @@ -201,9 +265,7 @@ IMAGE_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) -def load_svg_image(file: str, resize: tuple[int, int]): - from PIL import Image - +def load_svg_image(file: bytes, resize: tuple[int, int]): # This import is only needed in case of SVG images; adding it # to the top would force configurations not using SVG to also have it # installed for no reason. @@ -212,19 +274,17 @@ def load_svg_image(file: str, resize: tuple[int, int]): if resize: req_width, req_height = resize svg_image = svg2png( - url=file, + file, output_width=req_width, output_height=req_height, ) else: - svg_image = svg2png(url=file) + svg_image = svg2png(file) return Image.open(io.BytesIO(svg_image)) async def to_code(config): - from PIL import Image - conf_file = config[CONF_FILE] if conf_file[CONF_SOURCE] == SOURCE_LOCAL: @@ -233,17 +293,26 @@ async def to_code(config): elif conf_file[CONF_SOURCE] == SOURCE_MDI: path = _compute_local_icon_path(conf_file).as_posix() + elif conf_file[CONF_SOURCE] == SOURCE_WEB: + path = _compute_local_image_path(conf_file).as_posix() + try: - resize = config.get(CONF_RESIZE) - if path.lower().endswith(".svg"): - image = load_svg_image(path, resize) - else: - image = Image.open(path) - if resize: - image.thumbnail(resize) + with open(path, "rb") as f: + file_contents = f.read() except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + mime = Magic(mime=True) + file_type = mime.from_buffer(file_contents) + + resize = config.get(CONF_RESIZE) + if "svg" in file_type: + image = load_svg_image(file_contents, resize) + else: + image = Image.open(io.BytesIO(file_contents)) + if resize: + image.thumbnail(resize) + width, height = image.size if CONF_RESIZE not in config and (width > 500 or height > 500): diff --git a/esphome/external_files.py b/esphome/external_files.py new file mode 100644 index 0000000000..5b476286f3 --- /dev/null +++ b/esphome/external_files.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import logging +from pathlib import Path +import os +from datetime import datetime +import requests +import esphome.config_validation as cv +from esphome.core import CORE, TimePeriodSeconds + +_LOGGER = logging.getLogger(__name__) +CODEOWNERS = ["@landonr"] + +NETWORK_TIMEOUT = 30 + +IF_MODIFIED_SINCE = "If-Modified-Since" +CACHE_CONTROL = "Cache-Control" +CACHE_CONTROL_MAX_AGE = "max-age=" +CONTENT_DISPOSITION = "content-disposition" +TEMP_DIR = "temp" + + +def has_remote_file_changed(url, local_file_path): + if os.path.exists(local_file_path): + _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) + try: + local_modification_time = os.path.getmtime(local_file_path) + local_modification_time_str = datetime.utcfromtimestamp( + local_modification_time + ).strftime("%a, %d %b %Y %H:%M:%S GMT") + + headers = { + IF_MODIFIED_SINCE: local_modification_time_str, + CACHE_CONTROL: CACHE_CONTROL_MAX_AGE + "3600", + } + response = requests.head(url, headers=headers, timeout=NETWORK_TIMEOUT) + + _LOGGER.debug( + "has_remote_file_changed: File %s, Local modified %s, response code %d", + local_file_path, + local_modification_time_str, + response.status_code, + ) + + if response.status_code == 304: + _LOGGER.debug( + "has_remote_file_changed: File not modified since %s", + local_modification_time_str, + ) + return False + _LOGGER.debug("has_remote_file_changed: File modified") + return True + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not check if {url} has changed, please check if file exists " + f"({e})" + ) + + _LOGGER.debug("has_remote_file_changed: File doesn't exists at %s", local_file_path) + return True + + +def is_file_recent(file_path: str, refresh: TimePeriodSeconds) -> bool: + if os.path.exists(file_path): + creation_time = os.path.getctime(file_path) + current_time = datetime.now().timestamp() + return current_time - creation_time <= refresh.total_seconds + return False + + +def compute_local_file_dir(domain: str) -> Path: + base_directory = Path(CORE.data_dir) / domain + base_directory.mkdir(parents=True, exist_ok=True) + + return base_directory diff --git a/requirements.txt b/requirements.txt index 7a3bb421ed..2c2bf1ba19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ click==8.1.7 esphome-dashboard==20231107.0 aioesphomeapi==18.5.2 zeroconf==0.123.0 +python-magic==0.4.27 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/tests/test2.yaml b/tests/test2.yaml index bfc886eaa4..91fb554146 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -752,6 +752,18 @@ image: file: pnglogo.png type: RGB565 use_transparency: no + - id: web_svg_image + file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + resize: 256x48 + type: TRANSPARENT_BINARY + - id: web_tiff_image + file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + type: RGB24 + resize: 48x48 + - id: web_redirect_image + file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + type: RGB24 + resize: 48x48 - id: mdi_alert file: mdi:alert-circle-outline From 711faab329d5ff22bb0264def03461c3f1d77ca7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:24:25 +1300 Subject: [PATCH 261/270] Bump version to 2023.11.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5f6e9f5eb1..5596cbcd36 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2023.11.3" +__version__ = "2023.11.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 1762204b0039386a561a489c37138e6197ffdd32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Nov 2023 23:49:55 +0100 Subject: [PATCH 262/270] dashboard: set nodelay on the websocket to avoid a delay seeing log messages (#5802) --- esphome/dashboard/web_server.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 7c5f653b5b..9a9ccb462b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -27,6 +27,7 @@ import tornado.process import tornado.queues import tornado.web import tornado.websocket +import tornado.httputil import yaml from tornado.log import access_log @@ -136,7 +137,15 @@ def websocket_method(name): @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): - def __init__(self, application, request, **kwargs): + """Base class for ESPHome websocket commands.""" + + def __init__( + self, + application: tornado.web.Application, + request: tornado.httputil.HTTPServerRequest, + **kwargs: Any, + ) -> None: + """Initialize the websocket.""" super().__init__(application, request, **kwargs) self._proc = None self._queue = None @@ -145,6 +154,12 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # use Popen() with a reading thread instead self._use_popen = os.name == "nt" + def open(self, *args: str, **kwargs: str) -> None: + """Handle new WebSocket connection.""" + # Ensure messages from the subprocess are sent immediately + # to avoid a 200-500ms delay when nodelay is not set. + self.set_nodelay(True) + @authenticated async def on_message( # pylint: disable=invalid-overridden-method self, message: str From 9f8a896e13136809ae0f7e585246db4be06d3d1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:02:44 -0600 Subject: [PATCH 263/270] Bump aioesphomeapi from 18.5.5 to 18.5.7 (#5822) 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 abbdcf66d5..af53dc8e94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.5 +aioesphomeapi==18.5.7 zeroconf==0.127.0 python-magic==0.4.27 From 5c31bec8c20626c0fb2558f08145ca1ab46e85be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Nov 2023 00:29:08 +0100 Subject: [PATCH 264/270] Ensure names containing characters other than `a-z` `A-Z` `0-9` or `_` are unique (#5810) --- esphome/core/helpers.cpp | 11 +++++++---- esphome/helpers.py | 5 ++++- tests/unit_tests/test_helpers.py | 6 +++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 714a1642f8..c95c0470de 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -278,10 +278,13 @@ std::string str_snake_case(const std::string &str) { return result; } std::string str_sanitize(const std::string &str) { - std::string out; - std::copy_if(str.begin(), str.end(), std::back_inserter(out), [](const char &c) { - return c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - }); + std::string out = str; + std::replace_if( + out.begin(), out.end(), + [](const char &c) { + return !(c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); + }, + '_'); return out; } std::string str_snprintf(const char *fmt, size_t len, ...) { diff --git a/esphome/helpers.py b/esphome/helpers.py index 4012b2067f..00416b591f 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -357,6 +357,9 @@ def snake_case(value): return value.replace(" ", "_").lower() +_DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9_]") + + def sanitize(value): """Same behaviour as `helpers.cpp` method `str_sanitize`.""" - return re.sub("[^-_0-9a-zA-Z]", r"", value) + return _DISALLOWED_CHARS.sub("_", value) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 67fabd7af8..79d39901f0 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -258,9 +258,9 @@ def test_snake_case(text, expected): "text, expected", ( ("foo_bar", "foo_bar"), - ('!"§$%&/()=?foo_bar', "foo_bar"), - ('foo_!"§$%&/()=?bar', "foo_bar"), - ('foo_bar!"§$%&/()=?', "foo_bar"), + ('!"§$%&/()=?foo_bar', "___________foo_bar"), + ('foo_!"§$%&/()=?bar', "foo____________bar"), + ('foo_bar!"§$%&/()=?', "foo_bar___________"), ), ) def test_sanitize(text, expected): From f456603c1b6e3428aa7204e7a58ca068b3a72353 Mon Sep 17 00:00:00 2001 From: Vincent Schmandt Date: Fri, 24 Nov 2023 05:31:07 +0100 Subject: [PATCH 265/270] Add ENS160 Sensor (#4243) Co-authored-by: Keith Burzinski Co-authored-by: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ens160/__init__.py | 1 + esphome/components/ens160/ens160.cpp | 321 ++++++++++++++++++++++++++ esphome/components/ens160/ens160.h | 60 +++++ esphome/components/ens160/sensor.py | 88 +++++++ tests/test3.1.yaml | 11 +- tests/test3.yaml | 1 - 7 files changed, 478 insertions(+), 5 deletions(-) create mode 100644 esphome/components/ens160/__init__.py create mode 100644 esphome/components/ens160/ens160.cpp create mode 100644 esphome/components/ens160/ens160.h create mode 100644 esphome/components/ens160/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index dd1586d039..af23f679c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -90,6 +90,7 @@ esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/* @jesserockz esphome/components/emc2101/* @ellull +esphome/components/ens160/* @vincentscode esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz diff --git a/esphome/components/ens160/__init__.py b/esphome/components/ens160/__init__.py new file mode 100644 index 0000000000..d26770a89d --- /dev/null +++ b/esphome/components/ens160/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@vincentscode"] diff --git a/esphome/components/ens160/ens160.cpp b/esphome/components/ens160/ens160.cpp new file mode 100644 index 0000000000..c7a6ccbb73 --- /dev/null +++ b/esphome/components/ens160/ens160.cpp @@ -0,0 +1,321 @@ +// ENS160 sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf +// +// Implementation based on: +// https://github.com/sciosense/ENS160_driver + +#include "ens160.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens160 { + +static const char *const TAG = "ens160"; + +static const uint8_t ENS160_BOOTING = 10; + +static const uint16_t ENS160_PART_ID = 0x0160; + +static const uint8_t ENS160_REG_PART_ID = 0x00; +static const uint8_t ENS160_REG_OPMODE = 0x10; +static const uint8_t ENS160_REG_CONFIG = 0x11; +static const uint8_t ENS160_REG_COMMAND = 0x12; +static const uint8_t ENS160_REG_TEMP_IN = 0x13; +static const uint8_t ENS160_REG_DATA_STATUS = 0x20; +static const uint8_t ENS160_REG_DATA_AQI = 0x21; +static const uint8_t ENS160_REG_DATA_TVOC = 0x22; +static const uint8_t ENS160_REG_DATA_ECO2 = 0x24; + +static const uint8_t ENS160_REG_GPR_READ_0 = 0x48; +static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4; + +static const uint8_t ENS160_COMMAND_NOP = 0x00; +static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC; +static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E; + +static const uint8_t ENS160_OPMODE_RESET = 0xF0; +static const uint8_t ENS160_OPMODE_IDLE = 0x01; +static const uint8_t ENS160_OPMODE_STD = 0x02; + +static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80; +static const uint8_t ENS160_DATA_STATUS_STATER = 0x40; +static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C; +static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02; +static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01; + +// helps remove reserved bits in aqi data register +static const uint8_t ENS160_DATA_AQI = 0x07; + +void ENS160Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS160..."); + + // check part_id + uint16_t part_id; + if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast(&part_id), 2)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + if (part_id != ENS160_PART_ID) { + this->error_code_ = INVALID_ID; + this->mark_failed(); + return; + } + + // set mode to reset + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + delay(ENS160_BOOTING); + + // check status + uint8_t status_value; + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + if (this->validity_flag_ == INVALID_OUTPUT) { + this->error_code_ = VALIDITY_INVALID; + this->mark_failed(); + return; + } + + // set mode to idle + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + // clear command + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read firmware version + if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + uint8_t version_data[3]; + if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + this->firmware_ver_major_ = version_data[0]; + this->firmware_ver_minor_ = version_data[1]; + this->firmware_ver_build_ = version_data[2]; + + // set mode to standard + if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) { + this->error_code_ = WRITE_FAILED; + this->mark_failed(); + return; + } + + // read opmode and check standard mode is achieved before finishing Setup + uint8_t op_mode; + if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) { + this->error_code_ = READ_FAILED; + this->mark_failed(); + return; + } + + if (op_mode != ENS160_OPMODE_STD) { + this->error_code_ = STD_OPMODE_FAILED; + this->mark_failed(); + return; + } +} + +void ENS160Component::update() { + uint8_t status_value, data_ready; + + if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { + ESP_LOGW(TAG, "Error reading status register"); + this->status_set_warning(); + return; + } + + // verbose status logging + ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x", + (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS); + ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x", + (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER); + ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x", + (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT); + ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x", + (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR); + + data_ready = ENS160_DATA_STATUS_NEWDAT & status_value; + this->validity_flag_ = static_cast((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); + + switch (validity_flag_) { + case NORMAL_OPERATION: + if (data_ready != ENS160_DATA_STATUS_NEWDAT) { + ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready"); + return; + } + break; + case INITIAL_STARTUP: + if (!this->initial_startup_) { + this->initial_startup_ = true; + ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on"); + } + return; + case WARMING_UP: + if (!this->warming_up_) { + this->warming_up_ = true; + ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes"); + this->send_env_data_(); + } + return; + case INVALID_OUTPUT: + ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output"); + this->status_set_warning(); + return; + } + + // read new data + uint16_t data_eco2; + if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast(&data_eco2), 2)) { + ESP_LOGW(TAG, "Error reading eCO2 data register"); + this->status_set_warning(); + return; + } + if (this->co2_ != nullptr) { + this->co2_->publish_state(data_eco2); + } + + uint16_t data_tvoc; + if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast(&data_tvoc), 2)) { + ESP_LOGW(TAG, "Error reading TVOC data register"); + this->status_set_warning(); + return; + } + if (this->tvoc_ != nullptr) { + this->tvoc_->publish_state(data_tvoc); + } + + uint8_t data_aqi; + if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) { + ESP_LOGW(TAG, "Error reading AQI data register"); + this->status_set_warning(); + return; + } + if (this->aqi_ != nullptr) { + // remove reserved bits, just in case they are used in future + data_aqi = ENS160_DATA_AQI & data_aqi; + + this->aqi_->publish_state(data_aqi); + } + + this->status_clear_warning(); + + // set temperature and humidity compensation data + this->send_env_data_(); +} + +void ENS160Component::send_env_data_() { + if (this->temperature_ == nullptr && this->humidity_ == nullptr) + return; + + float temperature = NAN; + if (this->temperature_ != nullptr) + temperature = this->temperature_->state; + + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature); + } + + float humidity = NAN; + if (this->humidity_ != nullptr) + humidity = this->humidity_->state; + + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated"); + return; + } else { + ESP_LOGV(TAG, "External humidity compensation: %.1f%%", humidity); + } + + uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f); + uint16_t h = (uint16_t) (humidity * 512.0f); + + uint8_t data[4]; + data[0] = t & 0xff; + data[1] = (t >> 8) & 0xff; + data[2] = h & 0xff; + data[3] = (h >> 8) & 0xff; + + if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) { + ESP_LOGE(TAG, "Error writing compensation values"); + this->status_set_warning(); + return; + } +} + +void ENS160Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS160:"); + + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGE(TAG, "Communication failed! Is the sensor connected?"); + break; + case READ_FAILED: + ESP_LOGE(TAG, "Error reading from register"); + break; + case WRITE_FAILED: + ESP_LOGE(TAG, "Error writing to register"); + break; + case INVALID_ID: + ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?"); + break; + case VALIDITY_INVALID: + ESP_LOGE(TAG, "Invalid Device Status - No valid output"); + break; + case STD_OPMODE_FAILED: + ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode"); + break; + case NONE: + ESP_LOGD(TAG, "Setup successful"); + break; + } + ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_, + this->firmware_ver_build_); + + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2 Sensor:", this->co2_); + LOG_SENSOR(" ", "TVOC Sensor:", this->tvoc_); + LOG_SENSOR(" ", "AQI Sensor:", this->aqi_); + + if (this->temperature_ != nullptr && this->humidity_ != nullptr) { + LOG_SENSOR(" ", " Temperature Compensation:", this->temperature_); + LOG_SENSOR(" ", " Humidity Compensation:", this->humidity_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: Not configured"); + } +} + +} // namespace ens160 +} // namespace esphome diff --git a/esphome/components/ens160/ens160.h b/esphome/components/ens160/ens160.h new file mode 100644 index 0000000000..88bc8e3501 --- /dev/null +++ b/esphome/components/ens160/ens160.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens160 { + +class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void set_co2(sensor::Sensor *co2) { co2_ = co2; } + void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } + void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; } + + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void send_env_data_(); + + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + INVALID_ID, + VALIDITY_INVALID, + READ_FAILED, + WRITE_FAILED, + STD_OPMODE_FAILED, + } error_code_{NONE}; + + enum ValidityFlag { + NORMAL_OPERATION = 0, + WARMING_UP, + INITIAL_STARTUP, + INVALID_OUTPUT, + } validity_flag_; + + bool warming_up_{false}; + bool initial_startup_{false}; + + uint8_t firmware_ver_major_{0}; + uint8_t firmware_ver_minor_{0}; + uint8_t firmware_ver_build_{0}; + + sensor::Sensor *co2_{nullptr}; + sensor::Sensor *tvoc_{nullptr}; + sensor::Sensor *aqi_{nullptr}; + + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *temperature_{nullptr}; +}; + +} // namespace ens160 +} // namespace esphome diff --git a/esphome/components/ens160/sensor.py b/esphome/components/ens160/sensor.py new file mode 100644 index 0000000000..55f0ff7b6f --- /dev/null +++ b/esphome/components/ens160/sensor.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ECO2, + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + CONF_TVOC, + DEVICE_CLASS_AQI, + DEVICE_CLASS_CARBON_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ICON_CHEMICAL_WEAPON, + ICON_MOLECULE_CO2, + ICON_RADIATOR, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@vincentscode"] +DEPENDENCIES = ["i2c"] + +ens160_ns = cg.esphome_ns.namespace("ens160") +ENS160Component = ens160_ns.class_( + "ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor +) + +CONF_AQI = "aqi" +CONF_COMPENSATION = "compensation" +UNIT_INDEX = "index" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ENS160Component), + cv.Required(CONF_ECO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_TVOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Required(CONF_AQI): sensor.sensor_schema( + unit_of_measurement=UNIT_INDEX, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x53)) +) + + +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) + + sens = await sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_co2(sens)) + sens = await sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc(sens)) + sens = await sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi(sens)) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 151e53fd62..9000636f63 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -225,6 +225,13 @@ sensor: name: "ADE7953 Reactive Power B" update_interval: 1s + - platform: ens160 + eco2: + name: "ENS160 eCO2" + tvoc: + name: "ENS160 Total Volatile Organic Compounds" + aqi: + name: "ENS160 Air Quality Index" - platform: tmp102 name: TMP102 Temperature - platform: hm3301 @@ -424,7 +431,6 @@ switch: direction: BACKWARD id: test_motor - custom_component: lambda: |- auto s = new CustomComponent(); @@ -613,7 +619,6 @@ mcp23017: mcp23008: id: mcp23008_hub - light: - platform: hbridge name: Icicle Lights @@ -633,7 +638,6 @@ ttp229_bsf: sdo_pin: D2 scl_pin: D1 - display: - platform: max7219digit cs_pin: GPIO15 @@ -645,7 +649,6 @@ display: lambda: |- it.printdigit("hello"); - http_request: useragent: esphome/device timeout: 10s diff --git a/tests/test3.yaml b/tests/test3.yaml index 41ded7ee39..0a405a2841 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -480,7 +480,6 @@ sensor: name: PZEMDC Power energy: name: PZEMDC Energy - - platform: pmsx003 uart_id: uart_9 type: PMSX003 From 9f84b6390dd7808ac6f6a5e9c68f44ebf0c3c417 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:27:56 +0100 Subject: [PATCH 266/270] Bump actions/github-script from 6.4.1 to 7.0.1 (#5803) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/needs-docs.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml index 5019d64752..628b5cc5e3 100644 --- a/.github/workflows/needs-docs.yml +++ b/.github/workflows/needs-docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check for needs-docs label - uses: actions/github-script@v6.4.1 + uses: actions/github-script@v7.0.1 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e23db521a..69456619e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,7 +203,7 @@ jobs: needs: [deploy-manifest] steps: - name: Trigger Workflow - uses: actions/github-script@v6.4.1 + uses: actions/github-script@v7.0.1 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | From 91f1aa05ad31d94d79d92e516dab4719b70331fe Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 25 Nov 2023 00:18:36 +0100 Subject: [PATCH 267/270] Run all tests when local testing. (#5717) --- script/test | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/script/test b/script/test index 36a58cd75a..e227c17f9f 100755 --- a/script/test +++ b/script/test @@ -6,12 +6,6 @@ cd "$(dirname "$0")/.." set -x -esphome compile tests/test1.yaml -esphome compile tests/test2.yaml -esphome compile tests/test3.yaml -esphome compile tests/test3.1.yaml -esphome compile tests/test4.yaml -esphome compile tests/test5.yaml -esphome compile tests/test6.yaml -esphome compile tests/test7.yaml -esphome compile tests/test8.yaml +for f in ./tests/test*.yaml; do + esphome compile $f +done From 636ee2b59713f0f0bad72565658d1f3a176ff0a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:06:57 -0600 Subject: [PATCH 268/270] Bump aioesphomeapi from 18.5.7 to 18.5.9 (#5830) 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 af53dc8e94..3d096ffd52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile esptool==4.6.2 click==8.1.7 esphome-dashboard==20231107.0 -aioesphomeapi==18.5.7 +aioesphomeapi==18.5.9 zeroconf==0.127.0 python-magic==0.4.27 From c43518c391120866d31163f6890773a475b0124d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 25 Nov 2023 14:56:32 +1100 Subject: [PATCH 269/270] Allow split uart pin inversion for ESP-IDF (#5831) --- esphome/components/uart/__init__.py | 3 ++- tests/test5.yaml | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 36f2bb5851..9005422ce6 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -75,12 +75,13 @@ def validate_rx_pin(value): def validate_invert_esp32(config): if ( CORE.is_esp32 + and CORE.using_arduino and CONF_TX_PIN in config and CONF_RX_PIN in config and config[CONF_TX_PIN][CONF_INVERTED] != config[CONF_RX_PIN][CONF_INVERTED] ): raise cv.Invalid( - "Different invert values for TX and RX pin are not (yet) supported for ESP32." + "Different invert values for TX and RX pin are not supported for ESP32 when using Arduino." ) return config diff --git a/tests/test5.yaml b/tests/test5.yaml index 82c201f017..46cedcabd2 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -41,7 +41,9 @@ uart: rx_pin: 3 baud_rate: 9600 - id: uart_2 - tx_pin: 17 + tx_pin: + number: 17 + inverted: true rx_pin: 16 baud_rate: 19200 From ccd7f0661c3fa3fa283dd59058a0779ea1360df2 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Sat, 25 Nov 2023 09:38:45 +0100 Subject: [PATCH 270/270] Add `is_detected()` for Nextion displays (#5825) --- esphome/components/nextion/nextion.cpp | 3 ++- esphome/components/nextion/nextion_base.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 0134595050..92afdfe3dc 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -93,7 +93,8 @@ bool Nextion::check_connect_() { connect_info.push_back(response.substr(start, end - start)); } - if (connect_info.size() == 7) { + this->is_detected_ = (connect_info.size() == 7); + if (this->is_detected_) { ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); this->device_model_ = connect_info[2]; diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index a24fd74060..ffc431ed13 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -48,10 +48,12 @@ class NextionBase { bool is_sleeping() { return this->is_sleeping_; } bool is_setup() { return this->is_setup_; } + bool is_detected() { return this->is_detected_; } protected: bool is_setup_ = false; bool is_sleeping_ = false; + bool is_detected_ = false; }; } // namespace nextion