diff --git a/.gitignore b/.gitignore index 71b66b2499..d180b58259 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,6 @@ tests/.esphome/ sdkconfig.* !sdkconfig.defaults -.tests/ \ No newline at end of file +.tests/ + +/components diff --git a/docker/Dockerfile b/docker/Dockerfile index 2d9a8a9ae4..e1f3c46a3e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,16 +22,22 @@ RUN \ python3=3.9.2-3 \ python3-pip=20.3.4-4+deb11u1 \ python3-setuptools=52.0.0-4 \ - python3-pil=8.1.2+dfsg-0.3+deb11u1 \ python3-cryptography=3.3.2-1 \ python3-venv=3.9.2-3 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1+deb11u2 \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ - libcairo2=1.16.0-5 \ - python3-cffi=1.14.5-1 \ - && rm -rf \ + python3-cffi=1.14.5-1; \ + 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; \ + fi; \ + rm -rf \ /tmp/* \ /var/{cache,log}/* \ /var/lib/apt/lists/* diff --git a/esphome/__main__.py b/esphome/__main__.py index ecf0092b05..ca5fc1c008 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -365,10 +365,16 @@ def command_wizard(args): def command_config(args, config): - _LOGGER.info("Configuration is valid!") if not CORE.verbose: config = strip_default_ids(config) - safe_print(yaml_util.dump(config, args.show_secrets)) + output = yaml_util.dump(config, args.show_secrets) + # 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 + ) + safe_print(output) + _LOGGER.info("Configuration is valid!") return 0 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/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 738fd8d00d..6233014a96 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -114,7 +114,7 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei if (!decoded.has_value()) return false; // Decoded remote state y 3 bytes long code. - uint32_t remote_state = *decoded; + uint32_t remote_state = (*decoded).second; ESP_LOGV(TAG, "Decoded 0x%06X", remote_state); if ((remote_state & 0xFF0000) != 0xB20000) return false; diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 42eb0fc56b..6b6a726ef3 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -51,7 +51,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet if (universe < first_universe_ || universe > get_last_universe()) return false; - int output_offset = (universe - first_universe_) * get_lights_per_universe(); + int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 903031c77a..7daaaf7433 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Union +from typing import Union, Optional from pathlib import Path import logging import os @@ -42,6 +42,7 @@ from .const import ( # noqa KEY_REFRESH, KEY_REPO, KEY_SDKCONFIG_OPTIONS, + KEY_SUBMODULES, KEY_VARIANT, VARIANT_ESP32C3, VARIANT_FRIENDLY, @@ -120,17 +121,28 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( - name: str, repo: str, ref: str = None, path: str = None, refresh: TimePeriod = None + *, + name: str, + repo: str, + ref: str = None, + path: str = None, + refresh: TimePeriod = None, + components: Optional[list[str]] = None, + submodules: Optional[list[str]] = None, ): """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: raise ValueError("Not an esp-idf project") + if components is None: + components = [] if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, KEY_REFRESH: refresh, + KEY_COMPONENTS: components, + KEY_SUBMODULES: submodules, } @@ -163,23 +175,23 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) # The platformio/espressif32 version to use for arduino frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ARDUINO_PLATFORM_VERSION = cv.Version(5, 3, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 4) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0) +ESP_IDF_PLATFORM_VERSION = cv.Version(5, 4, 0) def _arduino_check_versions(value): value = value.copy() lookups = { "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(2, 0, 7), None), + "latest": (cv.Version(2, 0, 9), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -214,7 +226,7 @@ def _esp_idf_check_versions(value): value = value.copy() lookups = { "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 0, 1), None), + "latest": (cv.Version(5, 1, 0), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } @@ -536,20 +548,41 @@ def copy_files(): ref=component[KEY_REF], refresh=component[KEY_REFRESH], domain="idf_components", + submodules=component[KEY_SUBMODULES], ) mkdir_p(CORE.relative_build_path("components")) component_dir = repo_dir if component[KEY_PATH] is not None: component_dir = component_dir / component[KEY_PATH] - shutil.copytree( - component_dir, - CORE.relative_build_path(f"components/{name}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git", ".github"), - symlinks=True, - ignore_dangling_symlinks=True, - ) + if component[KEY_COMPONENTS] == ["*"]: + shutil.copytree( + component_dir, + CORE.relative_build_path("components"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) + elif len(component[KEY_COMPONENTS]) > 0: + for comp in component[KEY_COMPONENTS]: + shutil.copytree( + component_dir / comp, + CORE.relative_build_path(f"components/{comp}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) + else: + shutil.copytree( + component_dir, + CORE.relative_build_path(f"components/{name}"), + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(".git*"), + symlinks=True, + ignore_dangling_symlinks=True, + ) dir = os.path.dirname(__file__) post_build_file = os.path.join(dir, "post_build.py.script") diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 30297654bc..61cb8cdc3f 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1201,6 +1201,10 @@ BOARDS = { "name": "BPI-Bit", "variant": VARIANT_ESP32, }, + "bpi_leaf_s3": { + "name": "BPI-Leaf-S3", + "variant": VARIANT_ESP32S3, + }, "briki_abc_esp32": { "name": "Briki ABC (MBC-WB) - ESP32", "variant": VARIANT_ESP32, @@ -1217,6 +1221,10 @@ BOARDS = { "name": "Connaxio's Espoir", "variant": VARIANT_ESP32, }, + "cytron_maker_feather_aiot_s3": { + "name": "Cytron Maker Feather AIoT S3", + "variant": VARIANT_ESP32S3, + }, "d-duino-32": { "name": "D-duino-32", "variant": VARIANT_ESP32, @@ -1225,6 +1233,10 @@ BOARDS = { "name": "Deneyap Kart 1A", "variant": VARIANT_ESP32, }, + "deneyapkart1Av2": { + "name": "Deneyap Kart 1A v2", + "variant": VARIANT_ESP32S3, + }, "deneyapkartg": { "name": "Deneyap Kart G", "variant": VARIANT_ESP32C3, @@ -1237,6 +1249,10 @@ BOARDS = { "name": "Deneyap Mini", "variant": VARIANT_ESP32S2, }, + "deneyapminiv2": { + "name": "Deneyap Mini v2", + "variant": VARIANT_ESP32S2, + }, "denky32": { "name": "Denky32 (WROOM32)", "variant": VARIANT_ESP32, @@ -1265,6 +1281,10 @@ BOARDS = { "name": "Espressif ESP32-C3-DevKitM-1", "variant": VARIANT_ESP32C3, }, + "esp32-c3-m1i-kit": { + "name": "Ai-Thinker ESP-C3-M1-I-Kit", + "variant": VARIANT_ESP32C3, + }, "esp32cam": { "name": "AI Thinker ESP32-CAM", "variant": VARIANT_ESP32, @@ -1329,6 +1349,10 @@ BOARDS = { "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)", "variant": VARIANT_ESP32S3, }, + "esp32-s3-korvo-2": { + "name": "Espressif ESP32-S3-Korvo-2", + "variant": VARIANT_ESP32S3, + }, "esp32thing": { "name": "SparkFun ESP32 Thing", "variant": VARIANT_ESP32, @@ -1637,6 +1661,10 @@ BOARDS = { "name": "Noduino Quantum", "variant": VARIANT_ESP32, }, + "redpill_esp32s3": { + "name": "Munich Labs RedPill ESP32-S3", + "variant": VARIANT_ESP32S3, + }, "seeed_xiao_esp32c3": { "name": "Seeed Studio XIAO ESP32C3", "variant": VARIANT_ESP32C3, diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index d13df01d3a..698310dacb 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -9,6 +9,7 @@ KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" KEY_PATH = "path" +KEY_SUBMODULES = "submodules" VARIANT_ESP32 = "ESP32" VARIANT_ESP32S2 = "ESP32S2" 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", diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index d9b36c7b09..e7d700d149 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -86,10 +86,10 @@ async def to_code(config): 5, 0, 0 ): add_idf_component( - "mdns", - "https://github.com/espressif/esp-protocols.git", - "mdns-v1.0.9", - "components/mdns", + name="mdns", + repo="https://github.com/espressif/esp-protocols.git", + ref="mdns-v1.0.9", + path="components/mdns", ) if config[CONF_DISABLED]: diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 73fbfd6e90..e6ad545d70 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY_STORAGE, @@ -42,6 +43,7 @@ from esphome.const import ( DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -81,6 +83,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_DATA_RATE, DEVICE_CLASS_DATA_SIZE, DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY_STORAGE, @@ -95,6 +98,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 2ef33f3711..0666b96d1e 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, + CONF_SECOND, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, @@ -39,6 +40,7 @@ AUTO_LOAD = ["binary_sensor"] CONF_RECEIVER_ID = "receiver_id" CONF_TRANSMITTER_ID = "transmitter_id" +CONF_FIRST = "first" ns = remote_base_ns = cg.esphome_ns.namespace("remote_base") RemoteProtocol = ns.class_("RemoteProtocol") @@ -349,19 +351,48 @@ async def canalsatld_action(var, config, args): CoolixAction, CoolixDumper, ) = declare_protocol("Coolix") -COOLIX_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) -@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SCHEMA) +COOLIX_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_FIRST): cv.hex_int_range(0, 16777215), + cv.Optional(CONF_SECOND, default=0): cv.hex_int_range(0, 16777215), + cv.Optional(CONF_DATA): cv.invalid( + "'data' option has been removed in ESPHome 2023.8. " + "Use the 'first' and 'second' options instead." + ), + } +) + +COOLIX_SENSOR_SCHEMA = cv.Any(cv.hex_int_range(0, 16777215), COOLIX_BASE_SCHEMA) + + +@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SENSOR_SCHEMA) def coolix_binary_sensor(var, config): - cg.add( - var.set_data( - cg.StructInitializer( - CoolixData, - ("data", config[CONF_DATA]), + if isinstance(config, dict): + cg.add( + var.set_data( + cg.StructInitializer( + CoolixData, + ("first", config[CONF_FIRST]), + ("second", config[CONF_SECOND]), + ) ) ) - ) + else: + cg.add( + var.set_data( + cg.StructInitializer(CoolixData, ("first", 0), ("second", config)) + ) + ) + + +@register_action("coolix", CoolixAction, COOLIX_BASE_SCHEMA) +async def coolix_action(var, config, args): + template_ = await cg.templatable(config[CONF_FIRST], args, cg.uint32) + cg.add(var.set_first(template_)) + template_ = await cg.templatable(config[CONF_SECOND], args, cg.uint32) + cg.add(var.set_second(template_)) @register_trigger("coolix", CoolixTrigger, CoolixData) @@ -374,12 +405,6 @@ def coolix_dumper(var, config): pass -@register_action("coolix", CoolixAction, COOLIX_SCHEMA) -async def coolix_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) - cg.add(var.set_data(template_)) - - # Dish DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( "Dish" diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp index 252b6f0e91..3c9dadcd1c 100644 --- a/esphome/components/remote_base/coolix_protocol.cpp +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -15,11 +15,21 @@ static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; static const int32_t FOOTER_MARK_US = 1 * TICK_US; static const int32_t FOOTER_SPACE_US = 10 * TICK_US; -static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { - // Break data into bytes, starting at the Most Significant - // Byte. Each byte then being sent normal, then followed inverted. +bool CoolixData::operator==(const CoolixData &other) const { + if (this->first == 0) + return this->second == other.first || this->second == other.second; + if (other.first == 0) + return other.second == this->first || other.second == this->second; + return this->first == other.first && this->second == other.second; +} + +static void encode_frame(RemoteTransmitData *dst, const uint32_t &src) { + // Append header + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + // Break data into bytes, starting at the Most Significant + // Byte. Each byte then being sent normal, then followed inverted. for (unsigned shift = 16;; shift -= 8) { - // Grab a bytes worth of data. + // Grab a bytes worth of data const uint8_t byte = src >> shift; // Normal for (uint8_t mask = 1 << 7; mask; mask >>= 1) @@ -27,27 +37,33 @@ static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { // Inverted for (uint8_t mask = 1 << 7; mask; mask >>= 1) dst->item(BIT_MARK_US, (byte & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); - // Data end - if (shift == 0) + // End of frame + if (shift == 0) { + // Append footer + dst->mark(FOOTER_MARK_US); break; + } } } void CoolixProtocol::encode(RemoteTransmitData *dst, const CoolixData &data) { dst->set_carrier_frequency(38000); - dst->reserve(2 + 2 * 48 + 2 + 2 + 2 * 48 + 1); - dst->item(HEADER_MARK_US, HEADER_SPACE_US); - encode_data(dst, data); - dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); - dst->item(HEADER_MARK_US, HEADER_SPACE_US); - encode_data(dst, data); - dst->mark(FOOTER_MARK_US); + dst->reserve(100 + 100 * data.has_second()); + encode_frame(dst, data.first); + if (data.has_second()) { + dst->space(FOOTER_SPACE_US); + encode_frame(dst, data.second); + } } -static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { +static bool decode_frame(RemoteReceiveData &src, uint32_t &dst) { + // Checking for header + if (!src.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) + return false; + // Reading data uint32_t data = 0; for (unsigned n = 3;; data <<= 8) { - // Read byte + // Reading byte for (uint32_t mask = 1 << 7; mask; mask >>= 1) { if (!src.expect_mark(BIT_MARK_US)) return false; @@ -57,13 +73,16 @@ static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { return false; } } - // Check for inverse byte + // Checking for inverted byte for (uint32_t mask = 1 << 7; mask; mask >>= 1) { if (!src.expect_item(BIT_MARK_US, (data & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) return false; } - // Checking the end of reading + // End of frame if (--n == 0) { + // Checking for footer + if (!src.expect_mark(FOOTER_MARK_US)) + return false; dst = data; return true; } @@ -71,15 +90,24 @@ static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { } optional CoolixProtocol::decode(RemoteReceiveData data) { - CoolixData first, second; - if (data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(data, first) && - data.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && - decode_data(data, second) && data.expect_mark(FOOTER_MARK_US) && first == second) - return first; - return {}; + CoolixData result; + const auto size = data.size(); + if ((size != 200 && size != 100) || !decode_frame(data, result.first)) + return {}; + if (size == 100 || !data.expect_space(FOOTER_SPACE_US) || !decode_frame(data, result.second)) + result.second = 0; + return result; } -void CoolixProtocol::dump(const CoolixData &data) { ESP_LOGD(TAG, "Received Coolix: 0x%06X", data); } +void CoolixProtocol::dump(const CoolixData &data) { + if (data.is_strict()) { + ESP_LOGD(TAG, "Received Coolix: 0x%06X", data.first); + } else if (data.has_second()) { + ESP_LOGD(TAG, "Received unstrict Coolix: [0x%06X, 0x%06X]", data.first, data.second); + } else { + ESP_LOGD(TAG, "Received unstrict Coolix: [0x%06X]", data.first); + } +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h index 9ce3eabb0e..50ac839200 100644 --- a/esphome/components/remote_base/coolix_protocol.h +++ b/esphome/components/remote_base/coolix_protocol.h @@ -7,7 +7,16 @@ namespace esphome { namespace remote_base { -using CoolixData = uint32_t; +struct CoolixData { + CoolixData() {} + CoolixData(uint32_t a) : first(a), second(a) {} + CoolixData(uint32_t a, uint32_t b) : first(a), second(b) {} + bool operator==(const CoolixData &other) const; + bool is_strict() const { return this->first == this->second; } + bool has_second() const { return this->second != 0; } + uint32_t first; + uint32_t second; +}; class CoolixProtocol : public RemoteProtocol { public: @@ -19,10 +28,10 @@ class CoolixProtocol : public RemoteProtocol { DECLARE_REMOTE_PROTOCOL(Coolix) template class CoolixAction : public RemoteTransmitterActionBase { - TEMPLATABLE_VALUE(CoolixData, data) + TEMPLATABLE_VALUE(uint32_t, first) + TEMPLATABLE_VALUE(uint32_t, second) void encode(RemoteTransmitData *dst, Ts... x) override { - CoolixData data = this->data_.value(x...); - CoolixProtocol().encode(dst, data); + CoolixProtocol().encode(dst, {this->first_.value(x...), this->second_.value(x...)}); } }; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index caaffd9701..2aebf7bb17 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -57,6 +57,7 @@ from esphome.const import ( DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, @@ -114,6 +115,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_NITROGEN_MONOXIDE, DEVICE_CLASS_NITROUS_OXIDE, DEVICE_CLASS_OZONE, + DEVICE_CLASS_PH, DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, 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}; 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 diff --git a/esphome/const.py b/esphome/const.py index 1c4ccdf3a4..4977726361 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -988,6 +988,7 @@ DEVICE_CLASS_OCCUPANCY = "occupancy" DEVICE_CLASS_OPENING = "opening" DEVICE_CLASS_OUTLET = "outlet" DEVICE_CLASS_OZONE = "ozone" +DEVICE_CLASS_PH = "ph" DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PM1 = "pm1" DEVICE_CLASS_PM10 = "pm10" diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 05a7eaa4cc..63b6949fe9 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -475,6 +475,7 @@ template class CallbackManager { for (auto &cb : this->callbacks_) cb(args...); } + size_t size() const { return this->callbacks_.size(); } /// Call all callbacks in this manager. void operator()(Ts... args) { call(args...); } diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index dd800f534c..b33cb2df5e 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 @@ -92,6 +93,10 @@ class DashboardSettings: 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 @@ -130,7 +135,7 @@ def template_args(): "docs_link": docs_link, "get_static_file_url": get_static_file_url, "relative_url": settings.relative_url, - "streamer_mode": get_bool_env("ESPHOME_STREAMER_MODE"), + "streamer_mode": settings.streamer_mode, "config_dir": settings.config_dir, } @@ -202,7 +207,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 +233,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 +276,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 +290,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 +313,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 @@ -354,7 +400,10 @@ class EsphomeCompileHandler(EsphomeCommandWebSocket): class EsphomeValidateHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) - return ["esphome", "--dashboard", "config", config_file] + command = ["esphome", "--dashboard", "config", config_file] + if not settings.streamer_mode: + command.append("--show-secrets") + return command class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): @@ -1105,7 +1154,7 @@ class JsonConfigRequestHandler(BaseHandler): self.send_error(404) return - args = ["esphome", "config", settings.rel_path(configuration), "--show-secrets"] + args = ["esphome", "config", filename, "--show-secrets"] rc, stdout, _ = run_system_command(*args) diff --git a/esphome/git.py b/esphome/git.py index a607325b73..dcc3e4d0c8 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -15,6 +15,7 @@ _LOGGER = logging.getLogger(__name__) def run_git_command(cmd, cwd=None) -> str: + _LOGGER.debug("Running git command: %s", " ".join(cmd)) try: ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) except FileNotFoundError as err: @@ -48,6 +49,7 @@ def clone_or_update( domain: str, username: str = None, password: str = None, + submodules: Optional[list[str]] = None, ) -> tuple[Path, Optional[Callable[[], None]]]: key = f"{url}@{ref}" @@ -74,6 +76,14 @@ def clone_or_update( run_git_command(["git", "fetch", "--", "origin", ref], str(repo_dir)) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + if submodules is not None: + _LOGGER.info( + "Initialising submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ) + else: # Check refresh needed file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") @@ -97,6 +107,14 @@ def clone_or_update( # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) + if submodules is not None: + _LOGGER.info( + "Updating submodules (%s) for %s", ", ".join(submodules), key + ) + run_git_command( + ["git", "submodule", "update", "--init"] + submodules, str(repo_dir) + ) + def revert(): _LOGGER.info("Reverting changes to %s -> %s", key, old_sha) run_git_command(["git", "reset", "--hard", old_sha], str(repo_dir)) diff --git a/esphome/log.py b/esphome/log.py index e7ba0fdd82..b5d72e774c 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -71,6 +71,8 @@ def setup_log( ) -> None: import colorama + colorama.init() + if debug: log_level = logging.DEBUG CORE.verbose = True @@ -82,7 +84,6 @@ def setup_log( logging.getLogger("urllib3").setLevel(logging.WARNING) - colorama.init() logging.getLogger().handlers[0].setFormatter( ESPHomeLogFormatter(include_timestamp=include_timestamp) ) diff --git a/platformio.ini b/platformio.ini index bcb108bf3a..7c43ba0d95 100644 --- a/platformio.ini +++ b/platformio.ini @@ -105,7 +105,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = platformio/espressif32@5.3.0 +platform = platformio/espressif32@5.4.0 platform_packages = platformio/framework-arduinoespressif32@~3.20005.0 @@ -134,9 +134,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = platformio/espressif32@5.3.0 +platform = platformio/espressif32@5.4.0 platform_packages = - platformio/framework-espidf@~3.40404.0 + platformio/framework-espidf@~3.40405.0 framework = espidf lib_deps = diff --git a/requirements.txt b/requirements.txt index 618fc94e0b..781ed03a49 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 @@ -8,7 +8,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.7 # When updating platformio, also update Dockerfile esptool==4.6.2 -click==8.1.3 +click==8.1.5 esphome-dashboard==20230711.0 aioesphomeapi==15.0.0 zeroconf==0.69.0 diff --git a/tests/test1.yaml b/tests/test1.yaml index d86be748a5..7df4e9cd1d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1609,6 +1609,18 @@ binary_sensor: -2267, 1709, ] + - platform: remote_receiver + name: Coolix Test 1 + coolix: 0xB21F98 + - platform: remote_receiver + name: Coolix Test 2 + coolix: + first: 0xB2E003 + - platform: remote_receiver + name: Coolix Test 3 + coolix: + first: 0xB2E003 + second: 0xB21F98 - platform: as3935 name: Storm Alert - platform: analog_threshold @@ -2283,8 +2295,16 @@ switch: - platform: template name: MIDEA_RAW turn_on_action: - remote_transmitter.transmit_midea: - code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] + - remote_transmitter.transmit_coolix: + first: 0xB21F98 + - remote_transmitter.transmit_coolix: + first: 0xB21F98 + second: 0xB21F98 + - remote_transmitter.transmit_coolix: + first: !lambda "return 0xB21F98;" + second: !lambda "return 0xB21F98;" + - remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] - platform: gpio name: "MCP23S08 Pin #0" pin: @@ -2868,6 +2888,9 @@ tm1651: remote_receiver: pin: GPIO32 dump: all + on_coolix: + then: + delay: !lambda "return x.first + x.second;" status_led: pin: GPIO2