1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 15:41:52 +00:00

Compare commits

...

18 Commits

Author SHA1 Message Date
Jesse Hills
9eb7c26c80 Merge pull request #6472 from esphome/bump-2024.3.2
2024.3.2
2024-04-08 10:40:53 +12:00
Jesse Hills
c029ef5118 Bump version to 2024.3.2 2024-04-04 18:12:28 +13:00
NewoPL
d2b3861465 fix: changing the content source when playing is paused blocks the player (#6454) 2024-04-04 18:12:28 +13:00
Samuel Sieb
87c4ad0256 Add missing ethernet types (#6444) 2024-04-04 18:12:28 +13:00
DAVe3283
4c9bcc71cb Fix logger compile error on ESP32-C6 (#6323) 2024-04-04 18:12:27 +13:00
Jesse Hills
3290ab7f42 Merge pull request #6440 from esphome/bump-2024.3.1
2024.3.1
2024-03-27 14:14:45 +13:00
Jesse Hills
4d30c81b0b Bump version to 2024.3.1 2024-03-27 12:17:31 +13:00
J. Nick Koston
f00d876080 Fix editor live validation (#6431) 2024-03-27 12:17:30 +13:00
Gábor Poczkodi
d304e52940 Don't compile strptime unless its required (#6424) 2024-03-27 12:17:30 +13:00
ebw44
7abb82c1ca microWakeWord: Fix model path joining (#6426) 2024-03-27 12:17:30 +13:00
Clyde Stubbs
37345e11eb AHT10: Fix bug (#6409) 2024-03-27 12:17:30 +13:00
Jesse Hills
ce5a323f91 Merge pull request #6408 from esphome/bump-2024.3.0
2024.3.0
2024-03-20 18:30:49 +13:00
Jesse Hills
9541df9d88 Bump version to 2024.3.0 2024-03-20 17:15:19 +13:00
Jesse Hills
be15122e8b Merge pull request #6407 from esphome/bump-2024.3.0b5
2024.3.0b5
2024-03-20 17:11:24 +13:00
Jesse Hills
6f7273d9cb Bump version to 2024.3.0b5 2024-03-20 16:38:15 +13:00
RFDarter
ccca545862 web_server support for v3 (#6203) 2024-03-20 16:38:15 +13:00
Clyde Stubbs
e27e342927 Show component warnings and errors in the log; (#6400) 2024-03-20 16:38:15 +13:00
Clyde Stubbs
507568db64 AHT10: Use state machine to avoid blocking delay (#6401) 2024-03-20 16:38:15 +13:00
19 changed files with 185 additions and 127 deletions

View File

@@ -36,24 +36,24 @@ static const uint8_t AHT10_INIT_ATTEMPTS = 10;
static const uint8_t AHT10_STATUS_BUSY = 0x80;
void AHT10Component::setup() {
this->read_delay_ = this->humidity_sensor_ != nullptr ? AHT10_HUMIDITY_DELAY : AHT10_DEFAULT_DELAY;
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Reset AHT10 failed!");
}
delay(AHT10_SOFTRESET_DELAY);
const uint8_t *init_cmd;
i2c::ErrorCode error_code = i2c::ERROR_INVALID_ARGUMENT;
switch (this->variant_) {
case AHT10Variant::AHT20:
init_cmd = AHT20_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT20");
error_code = this->write(AHT20_INITIALIZE_CMD, sizeof(AHT20_INITIALIZE_CMD));
break;
case AHT10Variant::AHT10:
default:
init_cmd = AHT10_INITIALIZE_CMD;
ESP_LOGCONFIG(TAG, "Setting up AHT10");
error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
break;
}
if (this->write(init_cmd, sizeof(init_cmd)) != i2c::ERROR_OK) {
if (error_code != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->mark_failed();
return;
@@ -83,74 +83,78 @@ void AHT10Component::setup() {
ESP_LOGV(TAG, "AHT10 initialization");
}
void AHT10Component::update() {
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
return;
}
uint8_t data[6];
uint8_t delay_ms = AHT10_DEFAULT_DELAY;
if (this->humidity_sensor_ != nullptr)
delay_ms = AHT10_HUMIDITY_DELAY;
bool success = false;
for (int i = 0; i < AHT10_ATTEMPTS; ++i) {
ESP_LOGVV(TAG, "Attempt %d at %6" PRIu32, i, millis());
delay(delay_ms);
if (this->read(data, 6) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
continue;
}
if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy
ESP_LOGD(TAG, "AHT10 is busy, waiting...");
} else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
break;
} else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
return;
}
}
} else {
// data is valid, we can break the loop
ESP_LOGVV(TAG, "Answer at %6" PRIu32, millis());
success = true;
break;
}
}
if (!success || (data[0] & 0x80) == 0x80) {
void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0;
ESP_LOGE(TAG, "Measurements reading timed-out!");
this->status_set_warning();
this->status_set_error();
return;
}
this->read_count_++;
this->set_timeout(this->read_delay_, [this]() { this->read_data_(); });
}
void AHT10Component::read_data_() {
uint8_t data[6];
ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
if (this->read(data, 6) != i2c::ERROR_OK) {
ESP_LOGD(TAG, "Communication with AHT10 failed, waiting...");
this->restart_read_();
return;
}
if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy
ESP_LOGD(TAG, "AHT10 is busy, waiting...");
this->restart_read_();
return;
}
if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
// Unrealistic humidity (0x0)
if (this->humidity_sensor_ == nullptr) {
ESP_LOGV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
} else {
ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
}
this->restart_read_();
return;
}
}
ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_));
uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4;
float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
float humidity;
if (raw_humidity == 0) { // unrealistic value
humidity = NAN;
} else {
humidity = (float) raw_humidity * 100.0f / 1048576.0f;
}
if (this->temperature_sensor_ != nullptr) {
float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
float humidity;
if (raw_humidity == 0) { // unrealistic value
humidity = NAN;
} else {
humidity = (float) raw_humidity * 100.0f / 1048576.0f;
}
if (std::isnan(humidity)) {
ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum");
}
this->humidity_sensor_->publish_state(humidity);
}
this->status_clear_warning();
this->read_count_ = 0;
}
void AHT10Component::update() {
if (this->read_count_ != 0)
return;
this->start_time_ = millis();
if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Communication with AHT10 failed!");
this->status_set_warning();
return;
}
this->restart_read_();
}
float AHT10Component::get_setup_priority() const { return setup_priority::DATA; }

View File

@@ -26,6 +26,11 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
unsigned read_count_{};
unsigned read_delay_{};
void read_data_();
void restart_read_();
uint32_t start_time_{};
};
} // namespace aht10

View File

@@ -155,6 +155,8 @@ CONFIG_SCHEMA = cv.All(
"DP83848": RMII_SCHEMA,
"IP101": RMII_SCHEMA,
"JL1101": RMII_SCHEMA,
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
},
upper=True,

View File

@@ -12,12 +12,12 @@ static const char *const TAG = "audio";
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
if (call.get_media_url().has_value()) {
this->current_url_ = call.get_media_url();
if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && this->audio_ != nullptr) {
if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) {
if (this->audio_->isRunning()) {
this->audio_->stopSong();
}
this->audio_->connecttohost(this->current_url_.value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->start();
}

View File

@@ -129,7 +129,7 @@ void Logger::pre_setup() {
this->uart_num_ = UART_NUM_2;
break;
#endif
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
this->uart_num_ = -1;
break;

View File

@@ -287,7 +287,7 @@ def _load_model_data(manifest_path: Path):
except cv.Invalid as e:
raise EsphomeError(f"Invalid manifest file: {e}") from e
model_path = urljoin(str(manifest_path), manifest[CONF_MODEL])
model_path = manifest_path.parent / manifest[CONF_MODEL]
with open(model_path, "rb") as f:
model = f.read()

View File

@@ -44,6 +44,11 @@ def default_url(config):
config[CONF_CSS_URL] = ""
if not (CONF_JS_URL in config):
config[CONF_JS_URL] = "https://oi.esphome.io/v2/www.js"
if config[CONF_VERSION] == 3:
if not (CONF_CSS_URL in config):
config[CONF_CSS_URL] = ""
if not (CONF_JS_URL in config):
config[CONF_JS_URL] = "https://oi.esphome.io/v3/www.js"
return config
@@ -64,7 +69,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, int=True),
cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, 3, int=True),
cv.Optional(CONF_CSS_URL): cv.string,
cv.Optional(CONF_CSS_INCLUDE): cv.file_,
cv.Optional(CONF_JS_URL): cv.string,
@@ -152,7 +157,7 @@ async def to_code(config):
cg.add_define("USE_WEBSERVER")
cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT])
cg.add_define("USE_WEBSERVER_VERSION", version)
if version == 2:
if version >= 2:
# Don't compress the index HTML as the data sizes are almost the same.
add_resource_as_progmem("INDEX_HTML", build_index_html(config), compress=False)
else:

View File

@@ -358,7 +358,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("</article></body></html>"));
request->send(stream);
}
#elif USE_WEBSERVER_VERSION == 2
#elif USE_WEBSERVER_VERSION >= 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);

View File

@@ -13,7 +13,7 @@
#include <freertos/semphr.h>
#endif
#if USE_WEBSERVER_VERSION == 2
#if USE_WEBSERVER_VERSION >= 2
extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE;
#endif

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
import abc
import functools
import heapq
import logging
import re
from typing import Optional, Union
from typing import Union, Any
from contextlib import contextmanager
import contextvars
@@ -76,7 +77,7 @@ def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
@functools.total_ordering
class _ValidationStepTask:
def __init__(self, priority: float, id_number: int, step: "ConfigValidationStep"):
def __init__(self, priority: float, id_number: int, step: ConfigValidationStep):
self.priority = priority
self.id_number = id_number
self.step = step
@@ -130,7 +131,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
)
self.errors.append(error)
def add_validation_step(self, step: "ConfigValidationStep"):
def add_validation_step(self, step: ConfigValidationStep):
id_num = self._validation_tasks_id
self._validation_tasks_id += 1
heapq.heappush(
@@ -172,7 +173,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
conf = conf[key]
conf[path[-1]] = value
def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]:
def get_error_for_path(self, path: ConfigPath) -> vol.Invalid | None:
for err in self.errors:
if self.get_deepest_path(err.path) == path:
self.errors.remove(err)
@@ -181,7 +182,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
def get_deepest_document_range_for_path(
self, path: ConfigPath, get_key: bool = False
) -> Optional[ESPHomeDataBase]:
) -> ESPHomeDataBase | None:
data = self
doc_range = None
for index, path_item in enumerate(path):
@@ -733,7 +734,9 @@ class PinUseValidationCheck(ConfigValidationStep):
pins.PIN_SCHEMA_REGISTRY.final_validate(result)
def validate_config(config, command_line_substitutions) -> Config:
def validate_config(
config: dict[str, Any], command_line_substitutions: dict[str, Any]
) -> Config:
result = Config()
loader.clear_component_meta_finders()
@@ -897,24 +900,23 @@ class InvalidYAMLError(EsphomeError):
self.base_exc = base_exc
def _load_config(command_line_substitutions):
def _load_config(command_line_substitutions: dict[str, Any]) -> Config:
"""Load the configuration file."""
try:
config = yaml_util.load_yaml(CORE.config_path)
except EsphomeError as e:
raise InvalidYAMLError(e) from e
try:
result = validate_config(config, command_line_substitutions)
return validate_config(config, command_line_substitutions)
except EsphomeError:
raise
except Exception:
_LOGGER.error("Unexpected exception while reading configuration:")
raise
return result
def load_config(command_line_substitutions):
def load_config(command_line_substitutions: dict[str, Any]) -> Config:
try:
return _load_config(command_line_substitutions)
except vol.Invalid as err:

View File

@@ -1,9 +1,4 @@
import json
import os
from esphome.const import CONF_ID
from esphome.core import CORE
from esphome.helpers import read_file
class Extend:
@@ -38,25 +33,6 @@ class Remove:
return isinstance(b, Remove) and self.value == b.value
def read_config_file(path: str) -> str:
if CORE.vscode and (
not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
):
print(
json.dumps(
{
"type": "read_file",
"path": path,
}
)
)
data = json.loads(input())
assert data["type"] == "file_response"
return data["content"]
return read_file(path)
def merge_config(full_old, full_new):
def merge(old, new):
if isinstance(new, dict):

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2024.3.0b4"
__version__ = "2024.3.2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -141,18 +141,35 @@ bool Component::is_ready() {
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
}
bool Component::can_proceed() { return true; }
bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; }
void Component::status_set_warning() {
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
void Component::status_set_warning(const char *message) {
// Don't spam the log. This risks missing different warning messages though.
if ((this->component_state_ & STATUS_LED_WARNING) != 0)
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(this->get_component_source(), "Warning set: %s", message);
}
void Component::status_set_error() {
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(this->get_component_source(), "Error set: %s", message);
}
void Component::status_clear_warning() {
if ((this->component_state_ & STATUS_LED_WARNING) == 0)
return;
this->component_state_ &= ~STATUS_LED_WARNING;
ESP_LOGW(this->get_component_source(), "Warning cleared");
}
void Component::status_clear_error() {
if ((this->component_state_ & STATUS_LED_ERROR) == 0)
return;
this->component_state_ &= ~STATUS_LED_ERROR;
ESP_LOGE(this->get_component_source(), "Error cleared");
}
void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; }
void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; }
void Component::status_momentary_warning(const std::string &name, uint32_t length) {
this->status_set_warning();
this->set_timeout(name, length, [this]() { this->status_clear_warning(); });

View File

@@ -124,13 +124,13 @@ class Component {
virtual bool can_proceed();
bool status_has_warning();
bool status_has_warning() const;
bool status_has_error();
bool status_has_error() const;
void status_set_warning();
void status_set_warning(const char *message = "unspecified");
void status_set_error();
void status_set_error(const char *message = "unspecified");
void status_clear_warning();

View File

@@ -1,4 +1,6 @@
#ifdef USE_DATETIME
#include <regex>
#endif
#include "helpers.h"
#include "time.h" // NOLINT
@@ -64,6 +66,8 @@ std::string ESPTime::strftime(const std::string &format) {
return timestr;
}
#ifdef USE_DATETIME
bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
// clang-format off
std::regex dt_regex(R"(^
@@ -102,6 +106,8 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
return true;
}
#endif
void ESPTime::increment_second() {
this->timestamp++;
if (!increment_time_value(this->second, 0, 60))

View File

@@ -67,6 +67,8 @@ struct ESPTime {
this->day_of_year < 367 && this->month > 0 && this->month < 13;
}
#ifdef USE_DATETIME
/** Convert a string to ESPTime struct as specified by the format argument.
* @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00.
* @param esp_time an instance of a ESPTime struct
@@ -74,6 +76,8 @@ struct ESPTime {
*/
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time);
#endif
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);

View File

@@ -1,20 +1,22 @@
from __future__ import annotations
import json
import os
from io import StringIO
from typing import Any
from typing import Optional
from esphome.config import load_config, _format_vol_invalid, Config
from esphome.yaml_util import parse_yaml
from esphome.config import validate_config, _format_vol_invalid, Config
from esphome.core import CORE, DocumentRange
import esphome.config_validation as cv
def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]:
def _get_invalid_range(res: Config, invalid: cv.Invalid) -> DocumentRange | None:
return res.get_deepest_document_range_for_path(
invalid.path, invalid.error_message == "extra keys not allowed"
)
def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]:
def _dump_range(range: DocumentRange | None) -> dict | None:
if range is None:
return None
return {
@@ -56,6 +58,25 @@ class VSCodeResult:
)
def _read_file_content_from_json_on_stdin() -> str:
"""Read the content of a json encoded file from stdin."""
data = json.loads(input())
assert data["type"] == "file_response"
return data["content"]
def _print_file_read_event(path: str) -> None:
"""Print a file read event."""
print(
json.dumps(
{
"type": "read_file",
"path": path,
}
)
)
def read_config(args):
while True:
CORE.reset()
@@ -68,9 +89,17 @@ def read_config(args):
CORE.config_path = os.path.join(args.configuration, f)
else:
CORE.config_path = data["file"]
file_name = CORE.config_path
_print_file_read_event(file_name)
raw_yaml = _read_file_content_from_json_on_stdin()
command_line_substitutions: dict[str, Any] = (
dict(args.substitution) if args.substitution else {}
)
vs = VSCodeResult()
try:
res = load_config(dict(args.substitution) if args.substitution else {})
config = parse_yaml(file_name, StringIO(raw_yaml))
res = validate_config(config, command_line_substitutions)
except Exception as err: # pylint: disable=broad-except
vs.add_yaml_error(str(err))
else:

View File

@@ -417,20 +417,25 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any:
return _load_yaml_internal(fname)
def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
"""Parse a YAML file."""
try:
return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle)
except EsphomeError:
# Loading failed, so we now load with the Python loader which has more
# readable exceptions
# Rewind the stream so we can try again
file_handle.seek(0, 0)
return _load_yaml_internal_with_type(
ESPHomePurePythonLoader, file_name, file_handle
)
def _load_yaml_internal(fname: str) -> Any:
"""Load a YAML file."""
try:
with open(fname, encoding="utf-8") as f_handle:
try:
return _load_yaml_internal_with_type(ESPHomeLoader, fname, f_handle)
except EsphomeError:
# Loading failed, so we now load with the Python loader which has more
# readable exceptions
# Rewind the stream so we can try again
f_handle.seek(0, 0)
return _load_yaml_internal_with_type(
ESPHomePurePythonLoader, fname, f_handle
)
return parse_yaml(fname, f_handle)
except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err

View File

@@ -23,6 +23,9 @@ logger:
api:
reboot_timeout: 10min
web_server:
version: 3
time:
- platform: sntp