diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b04d046721b..95e7619a195e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.5.2 - name: Run yamllint - uses: frenck/action-yamllint@v1.4.0 + uses: frenck/action-yamllint@v1.4.1 black: name: Check black diff --git a/docker/Dockerfile b/docker/Dockerfile index 95b6677815dc..2d9a8a9ae487 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,8 @@ RUN \ 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 \ /tmp/* \ /var/{cache,log}/* \ diff --git a/esphome/__main__.py b/esphome/__main__.py index 603a06bdd239..c7c83ad83b2f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -980,7 +980,7 @@ def run_esphome(argv): _LOGGER.error(e, exc_info=args.verbose) return 1 - safe_print(f"ESPHome {const.__version__}") + _LOGGER.info("ESPHome %s", const.__version__) for conf_path in args.configuration: if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4cc98c91d91a..34678fde0f4d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1397,6 +1397,7 @@ message VoiceAssistantRequest { option (ifdef) = "USE_VOICE_ASSISTANT"; bool start = 1; + string conversation_id = 2; } message VoiceAssistantResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c350197e68c5..3983c6403b8d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -895,11 +895,12 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(bool start) { +bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) { if (!this->voice_assistant_subscription_) return false; VoiceAssistantRequest msg; msg.start = start; + msg.conversation_id = conversation_id; 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 78ecbb98e65b..d4e9cc656e4a 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -128,7 +128,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); + bool request_voice_assistant(bool start, const std::string &conversation_id); 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 1dd8c82e00fe..29e8e207ddd4 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -6187,7 +6187,20 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) return false; } } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); } +bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->conversation_id = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->start); + buffer.encode_string(2, this->conversation_id); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6195,6 +6208,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" start: "); out.append(YESNO(this->start)); out.append("\n"); + + out.append(" conversation_id: "); + out.append("'").append(this->conversation_id).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0f4b79de1978..cd1cfb595a33 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1604,12 +1604,14 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { class VoiceAssistantRequest : public ProtoMessage { public: bool start{false}; + std::string conversation_id{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class VoiceAssistantResponse : public ProtoMessage { diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 068f74315c08..3dd47c4dd8d7 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -428,16 +428,16 @@ void APIServer::on_shutdown() { } #ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant() { +bool APIServer::start_voice_assistant(const std::string &conversation_id) { for (auto &c : this->clients_) { - if (c->request_voice_assistant(true)) + if (c->request_voice_assistant(true, conversation_id)) 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, "")) return; } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a1bec2802fc4..79ba7b17f1bd 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -96,7 +96,7 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(); + bool start_voice_assistant(const std::string &conversation_id); void stop_voice_assistant(); #endif diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index e4fdaec0aa2f..30297654bc5e 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -42,6 +42,39 @@ } ESP32_BOARD_PINS = { + "adafruit_feather_esp32s2_tft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 2, + "TX": 1, + "D13": 13, + "D12": 12, + "D11": 11, + "D10": 10, + "D9": 9, + "D6": 6, + "D5": 5, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 34, + "SCL": 41, + "SDA": 42, + "TFT_I2C_POWER": 21, + "TFT_CS": 7, + "TFT_DC": 39, + "TFT_RESET": 40, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, "adafruit_qtpy_esp32c3": { "A0": 4, "A1": 3, diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index eec1bdc992f5..df6ee2ce2f2f 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -1,3 +1,4 @@ +#include #include "led_strip.h" #ifdef USE_ESP32 @@ -195,7 +196,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { break; } ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); - ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); } diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 07f51581887a..b917da30450a 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -62,7 +62,7 @@ def validate_esp32_variant(config): cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), - cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( _validate_bits, cv.enum(BITS_PER_SAMPLE) ), } diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4260636fa999..113c5a2df1c2 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,5 +1,10 @@ import logging +import io +from pathlib import Path +import re +import requests + from esphome import core from esphome.components import display, font import esphome.config_validation as cv @@ -7,15 +12,19 @@ from esphome.const import ( CONF_DITHER, CONF_FILE, + CONF_ICON, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_RESIZE, + CONF_SOURCE, CONF_TYPE, ) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True @@ -31,9 +40,58 @@ CONF_USE_TRANSPARENCY = "use_transparency" +# If the MDI file cannot be downloaded within this time, abort. +MDI_DOWNLOAD_TIMEOUT = 30 # seconds + +SOURCE_LOCAL = "local" +SOURCE_MDI = "mdi" + Image_ = display.display_ns.class_("Image") +def _compute_local_icon_path(value) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / 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) + try: + req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(req.content) + return value + + +def validate_cairosvg_installed(value): + """Validate that cairosvg is installed""" + try: + import cairosvg + except ImportError as err: + raise cv.Invalid( + "Please install the cairosvg python package to use this feature. " + "(pip install cairosvg)" + ) from err + + major, minor, _ = cairosvg.__version__.split(".") + if major < "2" or major == "2" and minor < "2": + raise cv.Invalid( + "Please update your cairosvg installation to at least 2.2.0. " + "(pip install -U cairosvg)" + ) + + return value + + def validate_cross_dependencies(config): """ Validate fields whose possible values depend on other fields. @@ -41,6 +99,13 @@ def validate_cross_dependencies(config): have "use_transparency" set to True. Also set the default value for those kind of dependent fields. """ + is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI + if CONF_TYPE not in config: + if is_mdi: + config[CONF_TYPE] = "TRANSPARENT_BINARY" + else: + config[CONF_TYPE] = "BINARY" + image_type = config[CONF_TYPE] is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] @@ -51,16 +116,74 @@ def validate_cross_dependencies(config): if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: raise cv.Invalid(f"Image type {image_type} must always be transparent.") + if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]: + raise cv.Invalid("MDI images must be binary images.") + return config +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("mdi:"): + validate_cairosvg_installed(value) + + match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) + if match is None: + raise cv.Invalid("Could not parse mdi icon name.") + icon = match.group(1) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_MDI, + CONF_ICON: icon, + } + ) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_LOCAL, + CONF_PATH: value, + } + ) + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +MDI_SCHEMA = cv.All( + { + cv.Required(CONF_ICON): cv.string, + }, + download_mdi, +) + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_MDI: MDI_SCHEMA, + }, + key=CONF_SOURCE, +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + IMAGE_SCHEMA = cv.Schema( cv.All( { cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the source type + # (file or mdi), and will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), # Not setting default here on purpose; the default depends on the image type, # and thus will be set in the "validate_cross_dependencies" validator. cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, @@ -79,24 +202,43 @@ def validate_cross_dependencies(config): async def to_code(config): from PIL import Image - path = CORE.relative_config_path(config[CONF_FILE]) - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") + conf_file = config[CONF_FILE] - width, height = image.size + if conf_file[CONF_SOURCE] == SOURCE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + try: + image = Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + if CONF_RESIZE in config: + image.thumbnail(config[CONF_RESIZE]) + elif conf_file[CONF_SOURCE] == SOURCE_MDI: + # Those imports are only needed in case of MDI images; adding them + # to the top would force configurations not using MDI to also have them + # installed for no reason. + from cairosvg import svg2png - if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) - width, height = image.size - else: - if width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, + svg_file = _compute_local_icon_path(conf_file) + if CONF_RESIZE in config: + req_width, req_height = config[CONF_RESIZE] + svg_image = svg2png( + url=svg_file.as_posix(), + output_width=req_width, + output_height=req_height, ) + else: + svg_image = svg2png(url=svg_file.as_posix()) + + image = Image.open(io.BytesIO(svg_image)) + + width, height = image.size + + if CONF_RESIZE not in config and (width > 500 or height > 500): + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) transparent = config[CONF_USE_TRANSPARENCY] diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index fb4e45b3b6c5..8dc5d4fbe7f6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -1,3 +1,4 @@ +#include #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" @@ -283,7 +284,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); this->effect_.reset(); } diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 7688629e396b..319a1482f16c 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -21,25 +21,35 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // The following function takes longer than the 5 seconds timeout of WDT #if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdtc; - wdtc.timeout_ms = 15000; wdtc.idle_core_mask = 0; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdtc.idle_core_mask |= (1 << 0); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdtc.idle_core_mask |= (1 << 1); +#endif + wdtc.timeout_ms = 15000; wdtc.trigger_panic = false; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(15, false); +#endif #endif esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // Set the WDT back to the configured timeout #if ESP_IDF_VERSION_MAJOR >= 5 - wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S; + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif #endif if (err != ESP_OK) { diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 39ba3dbed463..acf9e923b625 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -99,7 +99,7 @@ void OTAComponent::dump_config() { #endif if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %d restarts", + ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts", this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_); } } @@ -191,7 +191,7 @@ void OTAComponent::handle_() { this->writeall_(buf, 1); md5::MD5Digest md5{}; md5.init(); - sprintf(sbuf, "%08X", random_uint32()); + sprintf(sbuf, "%08" PRIx32, random_uint32()); md5.add(sbuf, 8); md5.calculate(); md5.get_hex(sbuf); @@ -466,7 +466,7 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ if (is_manual_safe_mode) { ESP_LOGI(TAG, "Safe mode has been entered manually"); } else { - ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); + ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); } if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index f416ecf246fd..0ae2856ce43f 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -5,6 +5,7 @@ #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" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 1eba0bf19278..ad66ce6d1803 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -157,7 +157,6 @@ async def to_code(config): "platform_packages", [ f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}", - "earlephilhower/tool-pioasm-rp2040-earlephilhower", ], ) diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 432ff6935ab3..a2ba72318f04 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -265,3 +265,9 @@ async def to_code(config): time_to_cycles(config[CONF_BIT1_LOW]), ), ) + cg.add_platformio_option( + "platform_packages", + [ + "earlephilhower/tool-pioasm-rp2040-earlephilhower", + ], + ) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 23258d578500..8ca9a69d1c75 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -17,6 +17,13 @@ namespace select { } \ } +#define SUB_SELECT(name) \ + protected: \ + select::Select *name##_select_{nullptr}; \ +\ + public: \ + void set_##name##_select(select::Select *select) { this->name##_select_ = select; } + /** Base-class for all selects. * * A select can use publish_state to send out a new value. diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 4c70a6b09f97..987dd23a19e0 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -22,7 +22,7 @@ class SNTPComponent : public time::RealTimeClock { this->server_2_ = server_2; this->server_3_ = server_3; } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } void update() override; void loop() override; diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index d18e305cc216..a81101f2d1f3 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.components import display, spi +from esphome.components import display, spi, power_supply from esphome.const import ( CONF_BACKLIGHT_PIN, CONF_DC_PIN, @@ -11,6 +11,7 @@ CONF_MODEL, CONF_RESET_PIN, CONF_WIDTH, + CONF_POWER_SUPPLY, ) from . import st7789v_ns @@ -32,6 +33,7 @@ "TTGO_TDISPLAY_135X240": ST7789VModel.ST7789V_MODEL_TTGO_TDISPLAY_135_240, "ADAFRUIT_FUNHOUSE_240X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, "ADAFRUIT_RR_280X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_RR_280_240, + "ADAFRUIT_S2_TFT_FEATHER_240X135": ST7789VModel.ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135, "CUSTOM": ST7789VModel.ST7789V_MODEL_CUSTOM, } @@ -58,6 +60,14 @@ def validate_st7789v(config): raise cv.Invalid( f'Do not specify {CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} or {CONF_OFFSET_WIDTH} when using {CONF_MODEL} that is not "CUSTOM"' ) + + if ( + config[CONF_MODEL].upper() == "ADAFRUIT_S2_TFT_FEATHER_240X135" + and CONF_POWER_SUPPLY not in config + ): + raise cv.Invalid( + f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is "ADAFRUIT_S2_TFT_FEATHER_240X135"' + ) return config @@ -69,6 +79,7 @@ def validate_st7789v(config): cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, cv.Optional(CONF_HEIGHT): cv.int_, cv.Optional(CONF_WIDTH): cv.int_, @@ -113,3 +124,7 @@ async def to_code(config): config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + + if CONF_POWER_SUPPLY in config: + ps = await cg.get_variable(config[CONF_POWER_SUPPLY]) + cg.add(var.set_power_supply(ps)) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 8a4fcfb179e6..0e7c9b9123e8 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -8,6 +8,10 @@ static const char *const TAG = "st7789v"; void ST7789V::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); +#ifdef USE_POWER_SUPPLY + this->power_.request(); + // the PowerSupply component takes care of post turn-on delay +#endif this->spi_setup(); this->dc_pin_->setup(); // OUTPUT @@ -128,6 +132,9 @@ void ST7789V::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" B/L Pin: ", this->backlight_pin_); LOG_UPDATE_INTERVAL(this); +#ifdef USE_POWER_SUPPLY + ESP_LOGCONFIG(TAG, " Power Supply Configured: yes"); +#endif } float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } @@ -162,6 +169,13 @@ void ST7789V::set_model(ST7789VModel model) { this->offset_width_ = 20; break; + case ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135: + this->height_ = 240; + this->width_ = 135; + this->offset_height_ = 52; + this->offset_width_ = 40; + break; + default: break; } @@ -323,6 +337,8 @@ const char *ST7789V::model_str_() { return "Adafruit Funhouse 240x240"; case ST7789V_MODEL_ADAFRUIT_RR_280_240: return "Adafruit Round-Rectangular 280x240"; + case ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135: + return "Adafruit ESP32-S2 TFT Feather"; default: return "Custom"; } diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 96e97c9d7865..ccbe50cf8553 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -3,6 +3,9 @@ #include "esphome/core/component.h" #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" +#ifdef USE_POWER_SUPPLY +#include "esphome/components/power_supply/power_supply.h" +#endif namespace esphome { namespace st7789v { @@ -11,6 +14,7 @@ enum ST7789VModel { ST7789V_MODEL_TTGO_TDISPLAY_135_240, ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, ST7789V_MODEL_ADAFRUIT_RR_280_240, + ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135, ST7789V_MODEL_CUSTOM }; @@ -120,6 +124,9 @@ class ST7789V : public PollingComponent, void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } +#ifdef USE_POWER_SUPPLY + void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } +#endif void set_eightbitcolor(bool eightbitcolor) { this->eightbitcolor_ = eightbitcolor; } void set_height(uint32_t height) { this->height_ = height; } @@ -143,6 +150,9 @@ class ST7789V : public PollingComponent, GPIOPin *dc_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; GPIOPin *backlight_pin_{nullptr}; +#ifdef USE_POWER_SUPPLY + power_supply::PowerSupplyRequester power_; +#endif bool eightbitcolor_{false}; uint16_t height_{0}; diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 9daac4ee230e..b5395a2c8397 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -8,6 +8,13 @@ namespace esphome { namespace switch_ { +#define SUB_SWITCH(name) \ + protected: \ + switch_::Switch *name##_switch_{nullptr}; \ +\ + public: \ + void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; } + // bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled const int RESTORE_MODE_ON_MASK = 0x01; const int RESTORE_MODE_PERSISTENT_MASK = 0x02; diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 624fcdf52cba..55d995be88f1 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,16 +1,23 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_MICROPHONE, CONF_SPEAKER +from esphome.const import ( + CONF_ID, + CONF_MICROPHONE, + CONF_SPEAKER, + CONF_MEDIA_PLAYER, +) from esphome import automation -from esphome.automation import register_action -from esphome.components import microphone, speaker +from esphome.automation import register_action, register_condition +from esphome.components import microphone, speaker, media_player AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] +CONF_SILENCE_DETECTION = "silence_detection" +CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" CONF_ON_STT_END = "on_stt_end" CONF_ON_TTS_START = "on_tts_start" @@ -25,16 +32,25 @@ StartAction = voice_assistant_ns.class_( "StartAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +StartContinuousAction = voice_assistant_ns.class_( + "StartContinuousAction", automation.Action, cg.Parented.template(VoiceAssistant) +) StopAction = voice_assistant_ns.class_( "StopAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +IsRunningCondition = voice_assistant_ns.class_( + "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(VoiceAssistant), cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), - cv.Optional(CONF_SPEAKER): cv.use_id(speaker.Speaker), + cv.Exclusive(CONF_SPEAKER, "output"): cv.use_id(speaker.Speaker), + cv.Exclusive(CONF_MEDIA_PLAYER, "output"): cv.use_id(media_player.MediaPlayer), + cv.Optional(CONF_SILENCE_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_ON_LISTENING): automation.validate_automation(single=True), cv.Optional(CONF_ON_START): automation.validate_automation(single=True), cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), @@ -56,6 +72,17 @@ async def to_code(config): spkr = await cg.get_variable(config[CONF_SPEAKER]) cg.add(var.set_speaker(spkr)) + if CONF_MEDIA_PLAYER in config: + mp = await cg.get_variable(config[CONF_MEDIA_PLAYER]) + cg.add(var.set_media_player(mp)) + + cg.add(var.set_silence_detection(config[CONF_SILENCE_DETECTION])) + + if CONF_ON_LISTENING in config: + await automation.build_automation( + var.get_listening_trigger(), [], config[CONF_ON_LISTENING] + ) + if CONF_ON_START in config: await automation.build_automation( var.get_start_trigger(), [], config[CONF_ON_START] @@ -96,6 +123,11 @@ async def to_code(config): VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssistant)}) +@register_action( + "voice_assistant.start_continuous", + StartContinuousAction, + VOICE_ASSISTANT_ACTION_SCHEMA, +) @register_action("voice_assistant.start", StartAction, VOICE_ASSISTANT_ACTION_SCHEMA) async def voice_assistant_listen_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -108,3 +140,12 @@ async def voice_assistant_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +@register_condition( + "voice_assistant.is_running", IsRunningCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_is_running_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 42455787111f..44d640ff394a 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -69,17 +69,42 @@ void VoiceAssistant::setup() { void VoiceAssistant::loop() { #ifdef USE_SPEAKER - if (this->speaker_ == nullptr) { + if (this->speaker_ != nullptr) { + uint8_t buf[1024]; + auto len = this->socket_->read(buf, sizeof(buf)); + if (len == -1) { + return; + } + this->speaker_->play(buf, len); + this->set_timeout("data-incoming", 200, [this]() { + if (this->continuous_) { + this->request_start(true); + } + }); return; } - - uint8_t buf[1024]; - auto len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { +#endif +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + if (!this->playing_tts_ || + this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING) { + return; + } + this->set_timeout("playing-media", 1000, [this]() { + this->playing_tts_ = false; + if (this->continuous_) { + this->request_start(true); + } + }); return; } - this->speaker_->play(buf, len); #endif + // Set a 1 second timeout to start the voice assistant again. + this->set_timeout("continuous-no-sound", 1000, [this]() { + if (this->continuous_) { + this->request_start(true); + } + }); } void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { @@ -100,14 +125,19 @@ void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { } this->running_ = true; this->mic_->start(); + this->listening_trigger_->trigger(); } -void VoiceAssistant::request_start() { +void VoiceAssistant::request_start(bool continuous) { ESP_LOGD(TAG, "Requesting start..."); - if (!api::global_api_server->start_voice_assistant()) { + if (!api::global_api_server->start_voice_assistant(this->conversation_id_)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); + this->continuous_ = false; + return; } + this->continuous_ = continuous; + this->set_timeout("reset-conversation_id", 5 * 60 * 1000, [this]() { this->conversation_id_ = ""; }); } void VoiceAssistant::signal_stop() { @@ -136,9 +166,18 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); + this->signal_stop(); this->stt_end_trigger_->trigger(text); 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); + } + } + break; + } case api::enums::VOICE_ASSISTANT_TTS_START: { std::string text; for (auto arg : msg.data) { @@ -166,6 +205,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->playing_tts_ = true; + this->media_player_->make_call().set_media_url(url).perform(); + } +#endif this->tts_end_trigger_->trigger(url); break; } @@ -184,6 +229,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); + this->continuous_ = false; + this->signal_stop(); this->error_trigger_->trigger(code, message); } default: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index c1a6e8883b86..b103584509d3 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -15,6 +15,9 @@ #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" #endif +#ifdef USE_MEDIA_PLAYER +#include "esphome/components/media_player/media_player.h" +#endif #include "esphome/components/socket/socket.h" namespace esphome { @@ -22,8 +25,10 @@ namespace voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support +// Version 3: Adds continuous support 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: @@ -36,20 +41,34 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif +#ifdef USE_MEDIA_PLAYER + void set_media_player(media_player::MediaPlayer *media_player) { this->media_player_ = media_player; } +#endif uint32_t get_version() const { #ifdef USE_SPEAKER - if (this->speaker_ != nullptr) + if (this->speaker_ != nullptr) { + if (this->silence_detection_) { + return SILENCE_DETECTION_SUPPORT; + } return SPEAKER_SUPPORT; + } #endif return INITIAL_VERSION; } - void request_start(); + void request_start(bool continuous = false); void signal_stop(); void on_event(const api::VoiceAssistantEventResponse &msg); + bool is_running() const { return this->running_; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } + bool is_continuous() const { return this->continuous_; } + + void set_silence_detection(bool silence_detection) { this->silence_detection_ = silence_detection; } + + Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } @@ -61,6 +80,7 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *listening_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_start_trigger_ = new Trigger(); @@ -72,8 +92,16 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif +#ifdef USE_MEDIA_PLAYER + media_player::MediaPlayer *media_player_{nullptr}; + bool playing_tts_{false}; +#endif + + std::string conversation_id_{""}; bool running_{false}; + bool continuous_{false}; + bool silence_detection_; }; template class StartAction : public Action, public Parented { @@ -81,9 +109,22 @@ template class StartAction : public Action, public Parent void play(Ts... x) override { this->parent_->request_start(); } }; +template class StartContinuousAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_start(true); } +}; + template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->signal_stop(); } + void play(Ts... x) override { + this->parent_->set_continuous(false); + this->parent_->signal_stop(); + } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c9da07795cd9..068d0157325f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -53,6 +53,9 @@ "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) +WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) +WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) def validate_password(value): @@ -253,6 +256,7 @@ def _validate(config): CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" +CONF_ENABLE_ON_BOOT = "enable_on_boot" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -286,6 +290,7 @@ def _validate(config): "This option has been removed. Please use the [disabled] option under the " "new mdns component instead." ), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } ), _validate, @@ -385,6 +390,8 @@ def add_sta(ap, network): if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) elif CORE.is_esp32 and CORE.using_arduino: @@ -410,3 +417,18 @@ def add_sta(ap, network): @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) async def wifi_connected_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_condition("wifi.enabled", WiFiEnabledCondition, cv.Schema({})) +async def wifi_enabled_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) +async def wifi_enable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) + + +@automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({})) +async def wifi_disable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index db19f9bcc032..ff621291f067 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -36,9 +36,18 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up WiFi..."); + this->wifi_pre_setup_(); + if (this->enable_on_boot_) { + this->start(); + } else { + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + } +} + +void WiFiComponent::start() { + ESP_LOGCONFIG(TAG, "Starting WiFi..."); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); this->last_connected_ = millis(); - this->wifi_pre_setup_(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; @@ -135,6 +144,8 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_OFF: case WIFI_COMPONENT_STATE_AP: break; + case WIFI_COMPONENT_STATE_DISABLED: + return; } if (this->has_ap() && !this->ap_setup_) { @@ -387,6 +398,28 @@ void WiFiComponent::print_connect_params_() { #endif } +void WiFiComponent::enable() { + if (this->state_ != WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Enabling WIFI..."); + this->error_from_callback_ = false; + this->state_ = WIFI_COMPONENT_STATE_OFF; + this->start(); +} + +void WiFiComponent::disable() { + if (this->state_ == WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Disabling WIFI..."); + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + this->wifi_disconnect_(); + this->wifi_mode_(false, false); +} + +bool WiFiComponent::is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; } + void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan..."); @@ -608,7 +641,7 @@ void WiFiComponent::retry_connect() { } bool WiFiComponent::can_proceed() { - if (!this->has_sta()) { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED) { return true; } return this->is_connected(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d42be43b2d36..d39b0629902b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -47,6 +47,8 @@ struct SavedWifiSettings { enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, + /** WiFi is disabled. */ + WIFI_COMPONENT_STATE_DISABLED, /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ WIFI_COMPONENT_STATE_COOLDOWN, /** WiFi is in STA-only mode and currently scanning for APs. */ @@ -198,6 +200,9 @@ class WiFiComponent : public Component { void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void enable(); + void disable(); + bool is_disabled(); void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); @@ -224,6 +229,7 @@ class WiFiComponent : public Component { // (In most use cases you won't need these) /// Setup WiFi interface. void setup() override; + void start(); void dump_config() override; /// WIFI setup_priority. float get_setup_priority() const override; @@ -282,6 +288,8 @@ class WiFiComponent : public Component { int8_t wifi_rssi(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + protected: static std::string format_mac_addr(const uint8_t mac[6]); void setup_ap_config_(); @@ -359,18 +367,30 @@ class WiFiComponent : public Component { bool btm_{false}; bool rrm_{false}; #endif + bool enable_on_boot_; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override; + bool check(Ts... x) override { return global_wifi_component->is_connected(); } }; -template bool WiFiConnectedCondition::check(Ts... x) { - return global_wifi_component->is_connected(); -} +template class WiFiEnabledCondition : public Condition { + public: + bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->disable(); } +}; } // namespace wifi } // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index cbc8f428f5dd..470f8a46e5f8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -399,6 +399,7 @@ CONF_MDNS = "mdns" CONF_MEASUREMENT_DURATION = "measurement_duration" CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" +CONF_MEDIA_PLAYER = "media_player" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" diff --git a/requirements.txt b/requirements.txt index 2b5b2c7e1fc6..b994de1932f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,11 +7,11 @@ tzlocal==5.0.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.7 # When updating platformio, also update Dockerfile -esptool==4.5.1 +esptool==4.6 click==8.1.3 esphome-dashboard==20230516.0 -aioesphomeapi==13.7.5 -zeroconf==0.62.0 +aioesphomeapi==14.0.0 +zeroconf==0.63.0 # 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/requirements_optional.txt b/requirements_optional.txt index 2c734301099d..df6b3b387e7a 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,2 +1,3 @@ pillow>4.0.0 +cairosvg>=2.2.0 cryptography>=2.0.0,<4 diff --git a/requirements_test.txt b/requirements_test.txt index 42aba7ddcf75..c099250bd71f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==7.3.1 -pytest-cov==4.0.0 +pytest-cov==4.1.0 pytest-mock==3.10.0 pytest-asyncio==0.21.0 asyncmock==0.4.2 diff --git a/tests/test2.yaml b/tests/test2.yaml index 8f5633684837..0dae0b257066 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -680,6 +680,13 @@ image: type: RGB565 use_transparency: no + - id: mdi_alert + file: mdi:alert-circle-outline + resize: 50x50 + - id: another_alert_icon + file: mdi:alert-outline + type: BINARY + cap1188: id: cap1188_component address: 0x29 diff --git a/tests/test6.yaml b/tests/test6.yaml index 6d956aa9c8b5..6224563a77e7 100644 --- a/tests/test6.yaml +++ b/tests/test6.yaml @@ -37,24 +37,24 @@ switch: output: pin_4 id: pin_4_switch -light: - - platform: rp2040_pio_led_strip - id: led_strip - pin: GPIO13 - num_leds: 60 - pio: 0 - rgb_order: GRB - chipset: WS2812 - - platform: rp2040_pio_led_strip - id: led_strip_custom_timings - pin: GPIO13 - num_leds: 60 - pio: 1 - rgb_order: GRB - bit0_high: .1us - bit0_low: 1.2us - bit1_high: .69us - bit1_low: .4us +#light: +# - platform: rp2040_pio_led_strip +# id: led_strip +# pin: GPIO13 +# num_leds: 60 +# pio: 0 +# rgb_order: GRB +# chipset: WS2812 +# - platform: rp2040_pio_led_strip +# id: led_strip_custom_timings +# pin: GPIO13 +# num_leds: 60 +# pio: 1 +# rgb_order: GRB +# bit0_high: .1us +# bit0_low: 1.2us +# bit1_high: .69us +# bit1_low: .4us sensor: