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

Merge branch 'dev' into integration

This commit is contained in:
J. Nick Koston
2025-07-25 08:42:21 -10:00
112 changed files with 1396 additions and 1107 deletions

View File

@@ -89,9 +89,9 @@ def choose_prompt(options, purpose: str = None):
def choose_upload_log_host(
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
):
options = []
for port in get_serial_ports():
options.append((f"{port.path} ({port.description})", port.path))
options = [
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
if default == "SERIAL":
return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
@@ -119,9 +119,7 @@ def mqtt_logging_enabled(mqtt_config):
return False
if CONF_TOPIC not in log_topic:
return False
if log_topic.get(CONF_LEVEL, None) == "NONE":
return False
return True
return log_topic.get(CONF_LEVEL, None) != "NONE"
def get_port_type(port):

View File

@@ -14,6 +14,8 @@ with warnings.catch_warnings():
from aioesphomeapi import APIClient, parse_log_message
from aioesphomeapi.log_runner import async_run
import contextlib
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE
@@ -66,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
def run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command."""
try:
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, address))
except KeyboardInterrupt:
pass

View File

@@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id):
async def autorepeat_filter_to_code(config, filter_id):
timings = []
if len(config) > 0:
for conf in config:
timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]))
timings.extend(
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
for conf in config
)
else:
timings.append(
(
@@ -573,16 +575,15 @@ async def setup_binary_sensor_core_(var, config):
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_MULTI_CLICK, []):
timings = []
for tim in conf[CONF_TIMING]:
timings.append(
cg.StructInitializer(
MultiClickTriggerEvent,
("state", tim[CONF_STATE]),
("min_length", tim[CONF_MIN_LENGTH]),
("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
)
timings = [
cg.StructInitializer(
MultiClickTriggerEvent,
("state", tim[CONF_STATE]),
("min_length", tim[CONF_MIN_LENGTH]),
("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
)
for tim in conf[CONF_TIMING]
]
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
if CONF_INVALID_COOLDOWN in conf:
cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))

View File

@@ -22,9 +22,8 @@ def validate_id(config):
if CONF_CAN_ID in config:
can_id = config[CONF_CAN_ID]
id_ext = config[CONF_USE_EXTENDED_ID]
if not id_ext:
if can_id > 0x7FF:
raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
if not id_ext and can_id > 0x7FF:
raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
return config

View File

@@ -74,8 +74,7 @@ def range_segment_list(input):
if isinstance(input, list):
for list_item in input:
if isinstance(list_item, list):
for item in list_item:
flat_list.append(item)
flat_list.extend(list_item)
else:
flat_list.append(list_item)
else:

View File

@@ -973,14 +973,16 @@ def _write_idf_component_yml():
# Called by writer.py
def copy_files():
if CORE.using_arduino:
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if (
CORE.using_arduino
and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]
):
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if CORE.using_esp_idf:
_write_sdkconfig()
_write_idf_component_yml()
@@ -1000,7 +1002,7 @@ def copy_files():
__version__,
)
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items():
for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values():
if file[KEY_PATH].startswith("http"):
import requests

View File

@@ -140,20 +140,22 @@ VALUE_TYPES = {
def validate_char_on_write(char_config):
if CONF_ON_WRITE in char_config:
if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set"
)
if (
CONF_ON_WRITE in char_config
and not char_config[CONF_WRITE]
and not char_config[CONF_WRITE_NO_RESPONSE]
):
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set"
)
return char_config
def validate_descriptor(desc_config):
if CONF_ON_WRITE in desc_config:
if not desc_config[CONF_WRITE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set"
)
if CONF_ON_WRITE in desc_config and not desc_config[CONF_WRITE]:
raise cv.Invalid(
f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set"
)
if CONF_MAX_LENGTH not in desc_config:
value = desc_config[CONF_VALUE][CONF_DATA]
if cg.is_template(value):

View File

@@ -310,9 +310,7 @@ async def to_code(config):
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = []
for it in conf[CONF_MAC_ADDRESS]:
addr_list.append(it.as_hex)
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):

View File

@@ -294,9 +294,8 @@ async def to_code(config):
)
)
if get_esp32_variant() == VARIANT_ESP32:
if CONF_IIR_FILTER in config:
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config:
cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER]))
if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3:
if CONF_FILTER_MODE in config:

View File

@@ -245,7 +245,7 @@ async def to_code(config):
if ver <= cv.Version(2, 3, 0):
# No ld script support
ld_script = None
if ver <= cv.Version(2, 4, 2):
elif ver <= cv.Version(2, 4, 2):
# Old ld script path
ld_script = ld_scripts[0]
else:

View File

@@ -73,8 +73,7 @@ def ota_esphome_final_validate(config):
else:
new_ota_conf.append(ota_conf)
for port_conf in merged_ota_esphome_configs_by_port.values():
new_ota_conf.append(port_conf)
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
full_conf[CONF_OTA] = new_ota_conf
fv.full_config.set(full_conf)

View File

@@ -112,7 +112,7 @@ def _is_framework_spi_polling_mode_supported():
return True
if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1):
return True
if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4):
if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103
return True
return False
if CORE.using_arduino:

View File

@@ -1,5 +1,97 @@
from esphome.automation import Trigger, build_automation, validate_automation
import esphome.codegen as cg
from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_TRIGGER_ID,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE
from esphome.final_validate import full_config
CODEOWNERS = ["@anatoly-savchenkov"]
factory_reset_ns = cg.esphome_ns.namespace("factory_reset")
FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component)
FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component)
CONF_MAX_DELAY = "max_delay"
CONF_RESETS_REQUIRED = "resets_required"
CONF_ON_INCREMENT = "on_increment"
def _validate(config):
if CONF_RESETS_REQUIRED in config:
return cv.only_on(
[
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
)(config)
if CONF_ON_INCREMENT in config:
raise cv.Invalid(
f"'{CONF_ON_INCREMENT}' requires a value for '{CONF_RESETS_REQUIRED}'"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(FactoryResetComponent),
cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=cv.TimePeriod(milliseconds=1000)),
),
cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int,
cv.Optional(CONF_ON_INCREMENT): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger),
}
),
}
).extend(cv.COMPONENT_SCHEMA),
_validate,
)
def _final_validate(config):
if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config:
fconfig = full_config.get()
if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]):
raise cv.Invalid(
"'resets_required' needs 'restore_from_flash' to be enabled in the 'esp8266' configuration"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
if reset_count := config.get(CONF_RESETS_REQUIRED):
var = cg.new_Pvariable(
config[CONF_ID],
reset_count,
config[CONF_MAX_DELAY].total_milliseconds,
)
await cg.register_component(var, config)
for conf in config.get(CONF_ON_INCREMENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await build_automation(
trigger,
[
(cg.uint8, "x"),
(cg.uint8, "target"),
],
conf,
)

View File

@@ -0,0 +1,76 @@
#include "factory_reset.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
#if !defined(USE_RP2040) && !defined(USE_HOST)
namespace esphome {
namespace factory_reset {
static const char *const TAG = "factory_reset";
static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE;
static bool was_power_cycled() {
#ifdef USE_ESP32
return esp_reset_reason() == ESP_RST_POWERON;
#endif
#ifdef USE_ESP8266
auto reset_reason = EspClass::getResetReason();
return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0;
#endif
#ifdef USE_LIBRETINY
auto reason = lt_get_reboot_reason();
return reason == REBOOT_REASON_POWER || reason == REBOOT_REASON_HARDWARE;
#endif
}
void FactoryResetComponent::dump_config() {
uint8_t count = 0;
this->flash_.load(&count);
ESP_LOGCONFIG(TAG, "Factory Reset by Reset:");
ESP_LOGCONFIG(TAG,
" Max interval between resets %" PRIu32 " seconds\n"
" Current count: %u\n"
" Factory reset after %u resets",
this->max_interval_ / 1000, count, this->required_count_);
}
void FactoryResetComponent::save_(uint8_t count) {
this->flash_.save(&count);
global_preferences->sync();
this->defer([count, this] { this->increment_callback_.call(count, this->required_count_); });
}
void FactoryResetComponent::setup() {
this->flash_ = global_preferences->make_preference<uint8_t>(POWER_CYCLES_KEY, true);
if (was_power_cycled()) {
uint8_t count = 0;
this->flash_.load(&count);
// this is a power on reset or external system reset
count++;
if (count == this->required_count_) {
ESP_LOGW(TAG, "Reset count reached, factory resetting");
global_preferences->reset();
// delay to allow log to be sent
delay(100); // NOLINT
App.safe_reboot(); // should not return
}
this->save_(count);
ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count);
this->set_timeout(this->max_interval_, [this]() {
ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000);
this->save_(0); // reset count
});
} else {
this->save_(0); // reset count if not a power cycle
}
}
} // namespace factory_reset
} // namespace esphome
#endif // !defined(USE_RP2040) && !defined(USE_HOST)

View File

@@ -0,0 +1,43 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/preferences.h"
#if !defined(USE_RP2040) && !defined(USE_HOST)
#ifdef USE_ESP32
#include <esp_system.h>
#endif
namespace esphome {
namespace factory_reset {
class FactoryResetComponent : public Component {
public:
FactoryResetComponent(uint8_t required_count, uint32_t max_interval)
: required_count_(required_count), max_interval_(max_interval) {}
void dump_config() override;
void setup() override;
void add_increment_callback(std::function<void(uint8_t, uint8_t)> &&callback) {
this->increment_callback_.add(std::move(callback));
}
protected:
~FactoryResetComponent() = default;
void save_(uint8_t count);
ESPPreferenceObject flash_{}; // saves the number of fast power cycles
uint8_t required_count_; // The number of boot attempts before fast boot is enabled
uint32_t max_interval_; // max interval between power cycles
CallbackManager<void(uint8_t, uint8_t)> increment_callback_{};
};
class FastBootTrigger : public Trigger<uint8_t, uint8_t> {
public:
explicit FastBootTrigger(FactoryResetComponent *parent) {
parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); });
}
};
} // namespace factory_reset
} // namespace esphome
#endif // !defined(USE_RP2040) && !defined(USE_HOST)

View File

@@ -55,9 +55,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await fastled_base.new_fastled_light(config)
rgb_order = cg.RawExpression(
config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB"
)
rgb_order = cg.RawExpression(config.get(CONF_RGB_ORDER, "RGB"))
data_rate = None
if CONF_DATA_RATE in config:

View File

@@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.polling_component_schema("20s"))
.extend(uart.UART_DEVICE_SCHEMA),
cv.only_with_arduino,
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True)
@@ -123,4 +122,9 @@ async def to_code(config):
cg.add(var.set_hdop_sensor(sens))
# https://platformio.org/lib/show/1655/TinyGPSPlus
cg.add_library("mikalhart/TinyGPSPlus", "1.1.0")
# Using fork of TinyGPSPlus patched to build on ESP-IDF
cg.add_library(
"TinyGPSPlus",
None,
"https://github.com/esphome/TinyGPSPlus.git#v1.1.0",
)

View File

@@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "gps.h"
#include "esphome/core/log.h"
@@ -22,73 +20,76 @@ void GPS::dump_config() {
}
void GPS::update() {
if (this->latitude_sensor_ != nullptr)
if (this->latitude_sensor_ != nullptr) {
this->latitude_sensor_->publish_state(this->latitude_);
}
if (this->longitude_sensor_ != nullptr)
if (this->longitude_sensor_ != nullptr) {
this->longitude_sensor_->publish_state(this->longitude_);
}
if (this->speed_sensor_ != nullptr)
if (this->speed_sensor_ != nullptr) {
this->speed_sensor_->publish_state(this->speed_);
}
if (this->course_sensor_ != nullptr)
if (this->course_sensor_ != nullptr) {
this->course_sensor_->publish_state(this->course_);
}
if (this->altitude_sensor_ != nullptr)
if (this->altitude_sensor_ != nullptr) {
this->altitude_sensor_->publish_state(this->altitude_);
}
if (this->satellites_sensor_ != nullptr)
if (this->satellites_sensor_ != nullptr) {
this->satellites_sensor_->publish_state(this->satellites_);
}
if (this->hdop_sensor_ != nullptr)
if (this->hdop_sensor_ != nullptr) {
this->hdop_sensor_->publish_state(this->hdop_);
}
}
void GPS::loop() {
while (this->available() > 0 && !this->has_time_) {
if (this->tiny_gps_.encode(this->read())) {
if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = this->tiny_gps_.location.lng();
if (!this->tiny_gps_.encode(this->read())) {
return;
}
if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = this->tiny_gps_.location.lng();
ESP_LOGV(TAG, "Latitude, Longitude: %.6f°, %.6f°", this->latitude_, this->longitude_);
}
ESP_LOGD(TAG, "Location:");
ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_);
ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_);
}
if (this->tiny_gps_.speed.isUpdated()) {
this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGV(TAG, "Speed: %.3f km/h", this->speed_);
}
if (this->tiny_gps_.speed.isUpdated()) {
this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_);
}
if (this->tiny_gps_.course.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg();
ESP_LOGV(TAG, "Course: %.2f°", this->course_);
}
if (this->tiny_gps_.course.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg();
ESP_LOGD(TAG, "Course: %.2f °", this->course_);
}
if (this->tiny_gps_.altitude.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGV(TAG, "Altitude: %.2f m", this->altitude_);
}
if (this->tiny_gps_.altitude.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_);
}
if (this->tiny_gps_.satellites.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGV(TAG, "Satellites: %d", this->satellites_);
}
if (this->tiny_gps_.satellites.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGD(TAG, "Satellites: %d", this->satellites_);
}
if (this->tiny_gps_.hdop.isUpdated()) {
this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGV(TAG, "HDOP: %.3f", this->hdop_);
}
if (this->tiny_gps_.hdop.isUpdated()) {
this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
}
for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_);
}
for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_);
}
}
}
} // namespace gps
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,10 +1,8 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include <TinyGPSPlus.h>
#include <vector>
@@ -53,8 +51,9 @@ class GPS : public PollingComponent, public uart::UARTDevice {
float speed_{NAN};
float course_{NAN};
float altitude_{NAN};
uint16_t satellites_{0};
float hdop_{NAN};
uint16_t satellites_{0};
bool has_time_{false};
sensor::Sensor *latitude_sensor_{nullptr};
sensor::Sensor *longitude_sensor_{nullptr};
@@ -64,12 +63,9 @@ class GPS : public PollingComponent, public uart::UARTDevice {
sensor::Sensor *satellites_sensor_{nullptr};
sensor::Sensor *hdop_sensor_{nullptr};
bool has_time_{false};
TinyGPSPlus tiny_gps_;
std::vector<GPSListener *> listeners_{};
};
} // namespace gps
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,5 +1,3 @@
#ifdef USE_ARDUINO
#include "gps_time.h"
#include "esphome/core/log.h"
@@ -9,12 +7,10 @@ namespace gps {
static const char *const TAG = "gps.time";
void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid())
return;
if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated())
return;
if (tiny_gps.date.year() < 2019)
if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid() || !tiny_gps.time.isUpdated() ||
!tiny_gps.date.isUpdated() || tiny_gps.date.year() < 2025) {
return;
}
ESPTime val{};
val.year = tiny_gps.date.year();
@@ -34,5 +30,3 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) {
} // namespace gps
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,10 +1,8 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h"
#include "esphome/components/gps/gps.h"
#include "esphome/components/time/real_time_clock.h"
#include "esphome/core/component.h"
namespace esphome {
namespace gps {
@@ -13,8 +11,9 @@ class GPSTime : public time::RealTimeClock, public GPSListener {
public:
void update() override { this->from_tiny_gps_(this->get_tiny_gps()); };
void on_update(TinyGPSPlus &tiny_gps) override {
if (!this->has_time_)
if (!this->has_time_) {
this->from_tiny_gps_(tiny_gps);
}
}
protected:
@@ -24,5 +23,3 @@ class GPSTime : public time::RealTimeClock, public GPSListener {
} // namespace gps
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -116,7 +116,7 @@ GRAPH_SCHEMA = cv.Schema(
def _relocate_fields_to_subfolder(config, subfolder, subschema):
fields = [k.schema for k in subschema.schema.keys()]
fields = [k.schema for k in subschema.schema]
fields.remove(CONF_ID)
if subfolder in config:
# Ensure no ambiguous fields in base of config

View File

@@ -70,9 +70,8 @@ def validate_url(value):
def validate_ssl_verification(config):
error_message = ""
if CORE.is_esp32:
if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
error_message = "ESPHome supports certificate verification only via ESP-IDF"
if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]:
error_message = "ESPHome supports certificate verification only via ESP-IDF"
if CORE.is_rp2040 and config[CONF_VERIFY_SSL]:
error_message = "ESPHome does not support certificate verification on RP2040"

View File

@@ -66,11 +66,10 @@ PROTOCOL_NAMES = {
def _validate(config):
for conf, models in SUPPORTED_OPTIONS.items():
if conf in config:
if config[CONF_MODEL] not in models:
raise cv.Invalid(
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
)
if conf in config and config[CONF_MODEL] not in models:
raise cv.Invalid(
f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}"
)
return config

View File

@@ -243,10 +243,7 @@ def _final_validate(_):
def use_legacy():
if CORE.using_esp_idf:
if not _use_legacy_driver:
return False
return True
return not (CORE.using_esp_idf and not _use_legacy_driver)
FINAL_VALIDATE_SCHEMA = _final_validate

View File

@@ -44,9 +44,8 @@ PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
def _validate_esp32_variant(config):
variant = esp32.get_esp32_variant()
if config[CONF_ADC_TYPE] == "external":
if config[CONF_PDM]:
if variant not in PDM_VARIANTS:
raise cv.Invalid(f"{variant} does not support PDM")
if config[CONF_PDM] and variant not in PDM_VARIANTS:
raise cv.Invalid(f"{variant} does not support PDM")
return config
if config[CONF_ADC_TYPE] == "internal":
if variant not in INTERNAL_ADC_VARIANTS:
@@ -122,9 +121,8 @@ CONFIG_SCHEMA = cv.All(
def _final_validate(config):
if not use_legacy():
if config[CONF_ADC_TYPE] == "internal":
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
if not use_legacy() and config[CONF_ADC_TYPE] == "internal":
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.")
FINAL_VALIDATE_SCHEMA = _final_validate

View File

@@ -138,9 +138,10 @@ def _validate(config):
]:
raise cv.Invalid("Selected model can't run on ESP8266.")
if model == "CUSTOM":
if CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config:
raise cv.Invalid("CUSTOM model requires init_sequence and dimensions")
if model == "CUSTOM" and (
CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config
):
raise cv.Invalid("CUSTOM model requires init_sequence and dimensions")
return config

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import contextlib
import hashlib
import io
import logging
@@ -174,9 +175,8 @@ class ImageGrayscale(ImageEncoder):
b = 1
if self.invert_alpha:
b ^= 0xFF
if self.transparency == CONF_ALPHA_CHANNEL:
if a != 0xFF:
b = a
if self.transparency == CONF_ALPHA_CHANNEL and a != 0xFF:
b = a
self.data[self.index] = b
self.index += 1
@@ -672,10 +672,8 @@ async def write_image(config, all_frames=False):
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
try:
with contextlib.suppress(AttributeError):
frame_count = image.n_frames
except AttributeError:
pass
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)

View File

@@ -27,14 +27,13 @@ def validate_logger(config):
logger_conf = fv.full_config.get()[CONF_LOGGER]
if logger_conf[CONF_BAUD_RATE] == 0:
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
if CORE.using_esp_idf:
if (
logger_conf[CONF_HARDWARE_UART] == USB_CDC
and get_esp32_variant() == VARIANT_ESP32S3
):
raise cv.Invalid(
"improv_serial does not support the selected logger hardware_uart"
)
if CORE.using_esp_idf and (
logger_conf[CONF_HARDWARE_UART] == USB_CDC
and get_esp32_variant() == VARIANT_ESP32S3
):
raise cv.Invalid(
"improv_serial does not support the selected logger hardware_uart"
)
return config

View File

@@ -78,11 +78,8 @@ def validate_model_config(config):
model = config[CONF_MODEL]
for key in config:
if key in SENSOR_MODEL_OPTIONS:
if model not in SENSOR_MODEL_OPTIONS[key]:
raise cv.Invalid(
f"Device model '{model}' does not support '{key}' sensor"
)
if key in SENSOR_MODEL_OPTIONS and model not in SENSOR_MODEL_OPTIONS[key]:
raise cv.Invalid(f"Device model '{model}' does not support '{key}' sensor")
tempco = config[CONF_TEMPERATURE_COEFFICIENT]
if tempco > 0 and model not in ["INA228", "INA229"]:

View File

@@ -41,9 +41,7 @@ CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await lcd_base.setup_lcd_display(var, config)
pins_ = []
for conf in config[CONF_DATA_PINS]:
pins_.append(await cg.gpio_pin_expression(conf))
pins_ = [await cg.gpio_pin_expression(conf) for conf in config[CONF_DATA_PINS]]
cg.add(var.set_data_pins(*pins_))
enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN])
cg.add(var.set_enable_pin(enable))

View File

@@ -56,7 +56,8 @@ async def to_code(config):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(ld2450_component.set_mac_text_sensor(sens))
for n in range(MAX_TARGETS):
if direction_conf := config.get(f"target_{n + 1}"):
if direction_config := direction_conf.get(CONF_DIRECTION):
sens = await text_sensor.new_text_sensor(direction_config)
cg.add(ld2450_component.set_direction_text_sensor(n, sens))
if (direction_conf := config.get(f"target_{n + 1}")) and (
direction_config := direction_conf.get(CONF_DIRECTION)
):
sens = await text_sensor.new_text_sensor(direction_config)
cg.add(ld2450_component.set_direction_text_sensor(n, sens))

View File

@@ -291,31 +291,30 @@ async def random_effect_to_code(config, effect_id):
)
async def strobe_effect_to_code(config, effect_id):
var = cg.new_Pvariable(effect_id, config[CONF_NAME])
colors = []
for color in config.get(CONF_COLORS, []):
colors.append(
cg.StructInitializer(
StrobeLightEffectColor,
(
"color",
LightColorValues(
color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
color[CONF_STATE],
color[CONF_BRIGHTNESS],
color[CONF_COLOR_BRIGHTNESS],
color[CONF_RED],
color[CONF_GREEN],
color[CONF_BLUE],
color[CONF_WHITE],
color.get(CONF_COLOR_TEMPERATURE, 0.0),
color[CONF_COLD_WHITE],
color[CONF_WARM_WHITE],
),
colors = [
cg.StructInitializer(
StrobeLightEffectColor,
(
"color",
LightColorValues(
color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
color[CONF_STATE],
color[CONF_BRIGHTNESS],
color[CONF_COLOR_BRIGHTNESS],
color[CONF_RED],
color[CONF_GREEN],
color[CONF_BLUE],
color[CONF_WHITE],
color.get(CONF_COLOR_TEMPERATURE, 0.0),
color[CONF_COLD_WHITE],
color[CONF_WARM_WHITE],
),
("duration", color[CONF_DURATION]),
("transition_length", color[CONF_TRANSITION_LENGTH]),
)
),
("duration", color[CONF_DURATION]),
("transition_length", color[CONF_TRANSITION_LENGTH]),
)
for color in config.get(CONF_COLORS, [])
]
cg.add(var.set_colors(colors))
return var
@@ -404,20 +403,19 @@ async def addressable_color_wipe_effect_to_code(config, effect_id):
var = cg.new_Pvariable(effect_id, config[CONF_NAME])
cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL]))
cg.add(var.set_reverse(config[CONF_REVERSE]))
colors = []
for color in config.get(CONF_COLORS, []):
colors.append(
cg.StructInitializer(
AddressableColorWipeEffectColor,
("r", int(round(color[CONF_RED] * 255))),
("g", int(round(color[CONF_GREEN] * 255))),
("b", int(round(color[CONF_BLUE] * 255))),
("w", int(round(color[CONF_WHITE] * 255))),
("random", color[CONF_RANDOM]),
("num_leds", color[CONF_NUM_LEDS]),
("gradient", color[CONF_GRADIENT]),
)
colors = [
cg.StructInitializer(
AddressableColorWipeEffectColor,
("r", int(round(color[CONF_RED] * 255))),
("g", int(round(color[CONF_GREEN] * 255))),
("b", int(round(color[CONF_BLUE] * 255))),
("w", int(round(color[CONF_WHITE] * 255))),
("random", color[CONF_RANDOM]),
("num_leds", color[CONF_NUM_LEDS]),
("gradient", color[CONF_GRADIENT]),
)
for color in config.get(CONF_COLORS, [])
]
cg.add(var.set_colors(colors))
return var
@@ -526,7 +524,7 @@ def validate_effects(allowed_effects):
errors = []
names = set()
for i, x in enumerate(value):
key = next(it for it in x.keys())
key = next(it for it in x)
if key not in allowed_effects:
errors.append(
cv.Invalid(

View File

@@ -346,14 +346,13 @@ async def to_code(config):
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
if CORE.using_arduino:
if config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1")
if CORE.is_esp32 and get_esp32_variant() in (
VARIANT_ESP32C3,
VARIANT_ESP32C6,
):
cg.add_build_flag("-DARDUINO_USB_MODE=1")
if CORE.using_arduino and config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1")
if CORE.is_esp32 and get_esp32_variant() in (
VARIANT_ESP32C3,
VARIANT_ESP32C6,
):
cg.add_build_flag("-DARDUINO_USB_MODE=1")
if CORE.using_esp_idf:
if config[CONF_HARDWARE_UART] == USB_CDC:

View File

@@ -201,9 +201,8 @@ def final_validation(configs):
multi_conf_validate(configs)
global_config = full_config.get()
for config in configs:
if pages := config.get(CONF_PAGES):
if all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
for display_id in config[df.CONF_DISPLAYS]:
path = global_config.get_path_for_id(display_id)[:-1]
display = global_config.get_config_for_path(path)

View File

@@ -28,9 +28,10 @@ CONF_HAS_PULLDOWNS = "has_pulldowns"
def check_keys(obj):
if CONF_KEYS in obj:
if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]):
raise cv.Invalid("The number of key codes must equal the number of buttons")
if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(
obj[CONF_COLUMNS]
):
raise cv.Invalid("The number of key codes must equal the number of buttons")
return obj

View File

@@ -124,11 +124,10 @@ async def to_code(config):
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
if task_stack_in_psram:
if config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])

View File

@@ -63,11 +63,13 @@ def _validate(config):
raise cv.Invalid(
f"{axis}: {CONF_RESOLUTION} cannot be {res} with {CONF_TEMPERATURE_COMPENSATION} enabled"
)
if config[CONF_HALLCONF] == 0xC:
if (config[CONF_OVERSAMPLING], config[CONF_FILTER]) in [(0, 0), (1, 0), (0, 1)]:
raise cv.Invalid(
f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}"
)
if config[CONF_HALLCONF] == 0xC and (
config[CONF_OVERSAMPLING],
config[CONF_FILTER],
) in [(0, 0), (1, 0), (0, 1)]:
raise cv.Invalid(
f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}"
)
return config

View File

@@ -56,12 +56,13 @@ def _final_validate(config):
for binary_sensor in binary_sensors:
if binary_sensor.get(CONF_MPR121_ID) == config[CONF_ID]:
max_touch_channel = max(max_touch_channel, binary_sensor[CONF_CHANNEL])
if max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL):
if max_touch_channel != max_touch_channel_in_config:
raise cv.Invalid(
"Max touch channel must equal the highest binary sensor channel or be removed for auto calculation",
path=[CONF_MAX_TOUCH_CHANNEL],
)
if (
max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL)
) and max_touch_channel != max_touch_channel_in_config:
raise cv.Invalid(
"Max touch channel must equal the highest binary sensor channel or be removed for auto calculation",
path=[CONF_MAX_TOUCH_CHANNEL],
)
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
this_config = fconf.get_config_for_path(path)
this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel

View File

@@ -27,7 +27,7 @@ void MQTTButtonComponent::setup() {
}
void MQTTButtonComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Button '%s': ", this->button_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true);
LOG_MQTT_COMPONENT(false, true);
}
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -25,9 +25,9 @@ async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable:
await cg.register_component(var, config)
input.generate_setters(var, config)
if (initial_value := config.get(CONF_INITIAL_VALUE, None)) is not None:
if (initial_value := config.get(CONF_INITIAL_VALUE)) is not None:
cg.add(var.set_initial_value(initial_value))
if (restore_value := config.get(CONF_RESTORE_VALUE, None)) is not None:
if (restore_value := config.get(CONF_RESTORE_VALUE)) is not None:
cg.add(var.set_restore_value(restore_value))
return var

View File

@@ -79,9 +79,8 @@ def set_sdkconfig_options(config):
"CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()
)
if force_dataset := config.get(CONF_FORCE_DATASET):
if force_dataset:
cg.add_define("USE_OPENTHREAD_FORCE_DATASET")
if config.get(CONF_FORCE_DATASET):
cg.add_define("USE_OPENTHREAD_FORCE_DATASET")
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True)
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True)

View File

@@ -89,9 +89,10 @@ def validate_(config):
raise cv.Invalid("No sensors or binary sensors to encrypt")
elif config[CONF_ROLLING_CODE_ENABLE]:
raise cv.Invalid("Rolling code requires an encryption key")
if config[CONF_PING_PONG_ENABLE]:
if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()):
raise cv.Invalid("Ping-pong requires at least one encrypted provider")
if config[CONF_PING_PONG_ENABLE] and not any(
CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()
):
raise cv.Invalid("Ping-pong requires at least one encrypted provider")
return config

View File

@@ -273,7 +273,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend(
async def to_code(config):
paren = await cg.get_variable(config[CONF_PIPSOLAR_ID])
for type, _ in TYPES.items():
for type in TYPES:
if type in config:
conf = config[type]
sens = await sensor.new_sensor(conf)

View File

@@ -49,12 +49,15 @@ def validate_internal_filter(value):
[CONF_USE_PCNT],
)
if CORE.is_esp32 and use_pcnt:
if value.get(CONF_INTERNAL_FILTER).total_microseconds > 13:
raise cv.Invalid(
"Maximum internal filter value when using ESP32 hardware PCNT is 13us",
[CONF_INTERNAL_FILTER],
)
if (
CORE.is_esp32
and use_pcnt
and value.get(CONF_INTERNAL_FILTER).total_microseconds > 13
):
raise cv.Invalid(
"Maximum internal filter value when using ESP32 hardware PCNT is 13us",
[CONF_INTERNAL_FILTER],
)
return value

View File

@@ -73,9 +73,8 @@ def map_sequence(value):
def _validate(config):
chip = DriverChip.chips[config[CONF_MODEL]]
if not chip.initsequence:
if CONF_INIT_SEQUENCE not in config:
raise cv.Invalid(f"{chip.name} model requires init_sequence")
if not chip.initsequence and CONF_INIT_SEQUENCE not in config:
raise cv.Invalid(f"{chip.name} model requires init_sequence")
return config

View File

@@ -24,9 +24,8 @@ QwiicPIRComponent = qwiic_pir_ns.class_(
def validate_no_debounce_unless_native(config):
if CONF_DEBOUNCE in config:
if config[CONF_DEBOUNCE_MODE] != "NATIVE":
raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE")
if CONF_DEBOUNCE in config and config[CONF_DEBOUNCE_MODE] != "NATIVE":
raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE")
return config

View File

@@ -1,4 +1,5 @@
#include "rc522.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Based on:
@@ -13,30 +14,6 @@ static const char *const TAG = "rc522";
static const uint8_t RESET_COUNT = 5;
std::string format_buffer(uint8_t *b, uint8_t len) {
char buf[32];
int offset = 0;
for (uint8_t i = 0; i < len; i++) {
const char *format = "%02X";
if (i + 1 < len)
format = "%02X-";
offset += sprintf(buf + offset, format, b[i]);
}
return std::string(buf);
}
std::string format_uid(std::vector<uint8_t> &uid) {
char buf[32];
int offset = 0;
for (size_t i = 0; i < uid.size(); i++) {
const char *format = "%02X";
if (i + 1 < uid.size())
format = "%02X-";
offset += sprintf(buf + offset, format, uid[i]);
}
return std::string(buf);
}
void RC522::setup() {
state_ = STATE_SETUP;
// Pull device out of power down / reset state.
@@ -215,7 +192,7 @@ void RC522::loop() {
ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status);
} else {
ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_,
format_buffer(buffer_, 9).c_str());
format_hex_pretty(buffer_, back_length_, '-', false).c_str());
}
state_ = STATE_DONE;
@@ -239,7 +216,7 @@ void RC522::loop() {
std::vector<uint8_t> rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_);
uid_idx_ = 0;
// ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str());
// ESP_LOGD(TAG, "Processing '%s'", format_hex_pretty(rfid_uid, '-', false).c_str());
pcd_antenna_off_();
state_ = STATE_INIT; // scan again on next update
bool report = true;
@@ -260,13 +237,13 @@ void RC522::loop() {
trigger->process(rfid_uid);
if (report) {
ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str());
ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty(rfid_uid, '-', false).c_str());
}
break;
}
case STATE_DONE: {
if (!this->current_uid_.empty()) {
ESP_LOGV(TAG, "Tag '%s' removed", format_uid(this->current_uid_).c_str());
ESP_LOGV(TAG, "Tag '%s' removed", format_hex_pretty(this->current_uid_, '-', false).c_str());
for (auto *trigger : this->triggers_ontagremoved_)
trigger->process(this->current_uid_);
}
@@ -361,7 +338,7 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to
* @return STATUS_OK on success, STATUS_??? otherwise.
*/
void RC522::pcd_transceive_data_(uint8_t send_len) {
ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str());
ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty(buffer_, send_len, '-', false).c_str());
delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands
send_len_ = send_len;
// Prepare values for BitFramingReg
@@ -435,7 +412,8 @@ RC522::StatusCode RC522::await_transceive_() {
error_reg_value); // TODO: is this always due to collissions?
return STATUS_ERROR;
}
ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str());
ESP_LOGV(TAG, "received %d bytes: %s", back_length_,
format_hex_pretty(buffer_ + send_len_, back_length_, '-', false).c_str());
return STATUS_OK;
}
@@ -499,7 +477,7 @@ bool RC522BinarySensor::process(std::vector<uint8_t> &data) {
this->found_ = result;
return result;
}
void RC522Trigger::process(std::vector<uint8_t> &data) { this->trigger(format_uid(data)); }
void RC522Trigger::process(std::vector<uint8_t> &data) { this->trigger(format_hex_pretty(data, '-', false)); }
} // namespace rc522
} // namespace esphome

View File

@@ -1062,12 +1062,11 @@ def validate_raw_alternating(value):
last_negative = None
for i, val in enumerate(value):
this_negative = val < 0
if i != 0:
if this_negative == last_negative:
raise cv.Invalid(
f"Values must alternate between being positive and negative, please see index {i} and {i + 1}",
[i],
)
if i != 0 and this_negative == last_negative:
raise cv.Invalid(
f"Values must alternate between being positive and negative, please see index {i} and {i + 1}",
[i],
)
last_negative = this_negative
return value

View File

@@ -90,11 +90,10 @@ async def to_code(config):
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
if task_stack_in_psram:
if config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -140,7 +140,6 @@ async def to_code(config):
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED]))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY]))
index = 0
dpins = []
if CONF_RED in config[CONF_DATA_PINS]:
red_pins = config[CONF_DATA_PINS][CONF_RED]
@@ -158,10 +157,9 @@ async def to_code(config):
dpins = dpins[8:16] + dpins[0:8]
else:
dpins = config[CONF_DATA_PINS]
for pin in dpins:
for index, pin in enumerate(dpins):
data_pin = await cg.gpio_pin_expression(pin)
cg.add(var.add_data_pin(data_pin, index))
index += 1
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = await cg.gpio_pin_expression(enable_pin)

View File

@@ -12,7 +12,7 @@ from esphome.const import (
UNIT_DECIBEL,
)
AUTOLOAD = ["audio"]
AUTO_LOAD = ["audio"]
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["microphone"]

View File

@@ -204,13 +204,14 @@ def _validate_pipeline(config):
def _validate_repeated_speaker(config):
if (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) and (
media_config := config.get(CONF_MEDIA_PIPELINE)
if (
(announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE))
and (media_config := config.get(CONF_MEDIA_PIPELINE))
and announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]
):
if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]:
raise cv.Invalid(
"The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers."
)
raise cv.Invalid(
"The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers."
)
return config

View File

@@ -115,9 +115,7 @@ def get_target_platform():
def get_target_variant():
return (
CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else ""
)
return CORE.data[KEY_ESP32].get(KEY_VARIANT, "")
# Get a list of available hardware interfaces based on target and variant.
@@ -213,9 +211,7 @@ def validate_hw_pins(spi, index=-1):
return False
if sdo_pin_no not in pin_set[CONF_MOSI_PIN]:
return False
if sdi_pin_no not in pin_set[CONF_MISO_PIN]:
return False
return True
return sdi_pin_no in pin_set[CONF_MISO_PIN]
return False

View File

@@ -130,11 +130,11 @@ def validate_sprinkler(config):
if (
CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller
and CONF_VALVE_OPEN_DELAY not in sprinkler_controller
and sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]
):
if sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]:
raise cv.Invalid(
f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled"
)
raise cv.Invalid(
f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled"
)
if (
CONF_REPEAT in sprinkler_controller

View File

@@ -42,14 +42,15 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_")
def _validate(value):
model = value[CONF_MODEL]
if model not in ("SSD1305_128X32", "SSD1305_128X64"):
# Contrast is default value (1.0) while brightness is not
# Indicates user is using old `brightness` option
if value[CONF_BRIGHTNESS] != 1.0 and value[CONF_CONTRAST] == 1.0:
raise cv.Invalid(
"SSD1306/SH1106 no longer accepts brightness option, "
'please use "contrast" instead.'
)
if (
model not in ("SSD1305_128X32", "SSD1305_128X64")
and value[CONF_BRIGHTNESS] != 1.0
and value[CONF_CONTRAST] == 1.0
):
raise cv.Invalid(
"SSD1306/SH1106 no longer accepts brightness option, "
'please use "contrast" instead.'
)
return value

View File

@@ -189,7 +189,6 @@ async def to_code(config):
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED]))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY]))
index = 0
dpins = []
if CONF_RED in config[CONF_DATA_PINS]:
red_pins = config[CONF_DATA_PINS][CONF_RED]
@@ -207,10 +206,9 @@ async def to_code(config):
dpins = dpins[8:16] + dpins[0:8]
else:
dpins = config[CONF_DATA_PINS]
for pin in dpins:
for index, pin in enumerate(dpins):
data_pin = await cg.gpio_pin_expression(pin)
cg.add(var.add_data_pin(data_pin, index))
index += 1
if dc_pin := config.get(CONF_DC_PIN):
dc = await cg.gpio_pin_expression(dc_pin)

View File

@@ -49,15 +49,14 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing):
try:
# Invoke the jinja engine to evaluate the expression.
value, err = jinja.expand(value)
if err is not None:
if not ignore_missing and "password" not in path:
_LOGGER.warning(
"Found '%s' (see %s) which looks like an expression,"
" but could not resolve all the variables: %s",
value,
"->".join(str(x) for x in path),
err.message,
)
if err is not None and not ignore_missing and "password" not in path:
_LOGGER.warning(
"Found '%s' (see %s) which looks like an expression,"
" but could not resolve all the variables: %s",
value,
"->".join(str(x) for x in path),
err.message,
)
except (
TemplateError,
TemplateRuntimeError,

View File

@@ -1,3 +1,4 @@
import contextlib
import re
from esphome import automation
@@ -41,12 +42,10 @@ ELEVATION_MAP = {
def elevation(value):
if isinstance(value, str):
try:
with contextlib.suppress(cv.Invalid):
value = ELEVATION_MAP[
cv.one_of(*ELEVATION_MAP, lower=True, space="_")(value)
]
except cv.Invalid:
pass
value = cv.angle(value)
return cv.float_range(min=-180, max=180)(value)

View File

@@ -41,11 +41,13 @@ SX1509KeyTrigger = sx1509_ns.class_(
def check_keys(config):
if CONF_KEYS in config:
if len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]:
raise cv.Invalid(
"The number of key codes must equal the number of rows * columns"
)
if (
CONF_KEYS in config
and len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]
):
raise cv.Invalid(
"The number of key codes must equal the number of rows * columns"
)
return config

View File

@@ -477,11 +477,11 @@ def validate_thermostat(config):
if (
CONF_ON_BOOT_RESTORE_FROM in config
and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET
and CONF_DEFAULT_PRESET not in config
):
if CONF_DEFAULT_PRESET not in config:
raise cv.Invalid(
f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode"
)
raise cv.Invalid(
f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode"
)
if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config:
raise cv.Invalid(

View File

@@ -236,7 +236,7 @@ def validate_time_at(value):
def validate_cron_keys(value):
if CONF_CRON in value:
for key in value.keys():
for key in value:
if key in CRON_KEYS:
raise cv.Invalid(f"Cannot use option {key} when cron: is specified.")
if CONF_AT in value:
@@ -246,7 +246,7 @@ def validate_cron_keys(value):
value.update(cron_)
return value
if CONF_AT in value:
for key in value.keys():
for key in value:
if key in CRON_KEYS:
raise cv.Invalid(f"Cannot use option {key} when at: is specified.")
at_ = value[CONF_AT]

View File

@@ -46,16 +46,15 @@ TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component)
def validate_temperature_multipliers(value):
if CONF_TEMPERATURE_MULTIPLIER in value:
if (
CONF_CURRENT_TEMPERATURE_MULTIPLIER in value
or CONF_TARGET_TEMPERATURE_MULTIPLIER in value
):
raise cv.Invalid(
f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as "
f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and "
f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}"
)
if CONF_TEMPERATURE_MULTIPLIER in value and (
CONF_CURRENT_TEMPERATURE_MULTIPLIER in value
or CONF_TARGET_TEMPERATURE_MULTIPLIER in value
):
raise cv.Invalid(
f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as "
f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and "
f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}"
)
if (
CONF_CURRENT_TEMPERATURE_MULTIPLIER in value
and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value

View File

@@ -34,12 +34,14 @@ def validate_min_max(config):
min_value = config[CONF_MIN_VALUE]
if max_value <= min_value:
raise cv.Invalid("max_value must be greater than min_value")
if hidden_config := config.get(CONF_DATAPOINT_HIDDEN):
if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None:
if (initial_value > max_value) or (initial_value < min_value):
raise cv.Invalid(
f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}"
)
if (
(hidden_config := config.get(CONF_DATAPOINT_HIDDEN))
and (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None
and ((initial_value > max_value) or (initial_value < min_value))
):
raise cv.Invalid(
f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}"
)
return config

View File

@@ -442,9 +442,7 @@ async def to_code(config):
if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None)
elif CORE.is_esp32 and CORE.using_arduino:
cg.add_library("WiFi", None)
elif CORE.is_rp2040:
elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040:
cg.add_library("WiFi", None)
if CORE.is_esp32 and CORE.using_esp_idf:

View File

@@ -198,10 +198,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
self.output_paths.remove((path, domain))
def is_in_error_path(self, path: ConfigPath) -> bool:
for err in self.errors:
if _path_begins_with(err.path, path):
return True
return False
return any(_path_begins_with(err.path, path) for err in self.errors)
def set_by_path(self, path, value):
conf = self
@@ -224,7 +221,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
for index, path_item in enumerate(path):
try:
if path_item in data:
key_data = [x for x in data.keys() if x == path_item][0]
key_data = [x for x in data if x == path_item][0]
if isinstance(key_data, ESPHomeDataBase):
doc_range = key_data.esp_range
if get_key and index == len(path) - 1:
@@ -1081,7 +1078,7 @@ def dump_dict(
ret += "{}"
multiline = False
for k in conf.keys():
for k in conf:
path_ = path + [k]
error = config.get_error_for_path(path_)
if error is not None:
@@ -1097,10 +1094,7 @@ def dump_dict(
msg = f"\n{indent(msg)}"
if inf is not None:
if m:
msg = f" {inf}{msg}"
else:
msg = f"{msg} {inf}"
msg = f" {inf}{msg}" if m else f"{msg} {inf}"
ret += f"{st + msg}\n"
elif isinstance(conf, str):
if is_secret(conf):

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from contextlib import contextmanager
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from datetime import datetime
from ipaddress import (
@@ -2113,10 +2113,8 @@ def require_esphome_version(year, month, patch):
@contextmanager
def suppress_invalid():
try:
with suppress(vol.Invalid):
yield
except vol.Invalid:
pass
GIT_SCHEMA = Schema(

View File

@@ -317,7 +317,7 @@ def preload_core_config(config, result) -> str:
target_platforms = []
for domain, _ in config.items():
for domain in config:
if domain.startswith("."):
continue
if _is_target_platform(domain):

View File

@@ -65,7 +65,7 @@ static void validate_static_string(const char *name) {
// Common implementation for both timeout and interval
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
const void *name_ptr, uint32_t delay, std::function<void()> func) {
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry) {
// Get the name as const char*
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
@@ -130,6 +130,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
#endif /* ESPHOME_DEBUG_SCHEDULER */
LockGuard guard{this->lock_};
// For retries, check if there's a cancelled timeout first
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr) ||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr))) {
// Skip scheduling - the retry was cancelled
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr);
#endif
return;
}
// If name is provided, do atomic cancel-and-add
// Cancel existing items
this->cancel_item_locked_(component, name_cstr, type);
@@ -178,12 +190,14 @@ struct RetryArgs {
Scheduler *scheduler;
};
static void retry_handler(const std::shared_ptr<RetryArgs> &args) {
void retry_handler(const std::shared_ptr<RetryArgs> &args) {
RetryResult const retry_result = args->func(--args->retry_countdown);
if (retry_result == RetryResult::DONE || args->retry_countdown <= 0)
return;
// second execution of `func` happens after `initial_wait_time`
args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); });
args->scheduler->set_timer_common_(
args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval,
[args]() { retry_handler(args); }, true);
// backoff_increase_factor applied to third & later executions
args->current_interval *= args->backoff_increase_factor;
}

View File

@@ -15,8 +15,15 @@
namespace esphome {
class Component;
struct RetryArgs;
// Forward declaration of retry_handler - needs to be non-static for friend declaration
void retry_handler(const std::shared_ptr<RetryArgs> &args);
class Scheduler {
// Allow retry_handler to access protected members
friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
public:
// Public API - accepts std::string for backward compatibility
void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> func);
@@ -147,7 +154,7 @@ class Scheduler {
// Common implementation for both timeout and interval
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
uint32_t delay, std::function<void()> func);
uint32_t delay, std::function<void()> func, bool is_retry = false);
uint64_t millis_64_(uint32_t now);
// Cleanup logically deleted items from the scheduler
@@ -170,8 +177,8 @@ class Scheduler {
// Helper function to check if item matches criteria for cancellation
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
SchedulerItem::Type type) {
if (item->component != component || item->type != type || item->remove) {
SchedulerItem::Type type, bool skip_removed = true) const {
if (item->component != component || item->type != type || (skip_removed && item->remove)) {
return false;
}
const char *item_name = item->get_name();
@@ -197,6 +204,18 @@ class Scheduler {
return item->remove || (item->component != nullptr && item->component->is_failed());
}
// Template helper to check if any item in a container matches our criteria
template<typename Container>
bool has_cancelled_timeout_in_container_(const Container &container, Component *component,
const char *name_cstr) const {
for (const auto &item : container) {
if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, false)) {
return true;
}
}
return false;
}
Mutex lock_;
std::vector<std::unique_ptr<SchedulerItem>> items_;
std::vector<std::unique_ptr<SchedulerItem>> to_add_;

View File

@@ -1037,10 +1037,7 @@ class MockObjClass(MockObj):
def inherits_from(self, other: "MockObjClass") -> bool:
if str(self) == str(other):
return True
for parent in self._parents:
if str(parent) == str(other):
return True
return False
return any(str(parent) == str(other) for parent in self._parents)
def template(self, *args: SafeExpType) -> "MockObjClass":
if len(args) != 1 or not isinstance(args[0], TemplateArguments):

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
from asyncio import events
from concurrent.futures import ThreadPoolExecutor
import contextlib
import logging
import os
import socket
@@ -125,10 +126,8 @@ def start_dashboard(args) -> None:
asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose))
try:
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_start(args))
except KeyboardInterrupt:
pass
async def async_start(args) -> None:

View File

@@ -144,7 +144,7 @@ def websocket_class(cls):
if not hasattr(cls, "_message_handlers"):
cls._message_handlers = {}
for _, method in cls.__dict__.items():
for method in cls.__dict__.values():
if hasattr(method, "_message_handler"):
cls._message_handlers[method._message_handler] = method

View File

@@ -88,10 +88,7 @@ def recv_decode(sock, amount, decode=True):
def receive_exactly(sock, amount, msg, expect, decode=True):
if decode:
data = []
else:
data = b""
data = [] if decode else b""
try:
data += recv_decode(sock, 1, decode=decode)

View File

@@ -96,9 +96,7 @@ def cpp_string_escape(string, encoding="utf-8"):
def _should_escape(byte: int) -> bool:
if not 32 <= byte < 127:
return True
if byte in (ord("\\"), ord('"')):
return True
return False
return byte in (ord("\\"), ord('"'))
if isinstance(string, str):
string = string.encode(encoding)

View File

@@ -61,7 +61,7 @@ class ESPHomeLogFormatter(logging.Formatter):
}.get(record.levelname, "")
message = f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}"
if CORE.dashboard:
try:
try: # noqa: SIM105
message = message.replace("\033", "\\033")
except UnicodeEncodeError:
pass

View File

@@ -1,3 +1,4 @@
import contextlib
from datetime import datetime
import hashlib
import json
@@ -52,10 +53,8 @@ def initialize(
client = prepare(
config, subscriptions, on_message, on_connect, username, password, client_id
)
try:
with contextlib.suppress(KeyboardInterrupt):
client.loop_forever()
except KeyboardInterrupt:
pass
return 0

View File

@@ -141,9 +141,11 @@ def _load_idedata(config):
temp_idedata = Path(CORE.relative_internal_path("idedata", f"{CORE.name}.json"))
changed = False
if not platformio_ini.is_file() or not temp_idedata.is_file():
changed = True
elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime:
if (
not platformio_ini.is_file()
or not temp_idedata.is_file()
or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime
):
changed = True
if not changed:

View File

@@ -59,7 +59,7 @@ def safe_print(message="", end="\n"):
from esphome.core import CORE
if CORE.dashboard:
try:
try: # noqa: SIM105
message = message.replace("\033", "\\033")
except UnicodeEncodeError:
pass

View File

@@ -116,10 +116,7 @@ def wizard_file(**kwargs):
kwargs["fallback_name"] = ap_name
kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12))
if kwargs.get("friendly_name"):
base = BASE_CONFIG_FRIENDLY
else:
base = BASE_CONFIG
base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG
config = base.format(**kwargs)

View File

@@ -86,21 +86,17 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
if old.src_version != new.src_version:
return True
if old.build_path != new.build_path:
return True
return False
return old.build_path != new.build_path
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
if (
old.loaded_integrations != new.loaded_integrations
or old.loaded_platforms != new.loaded_platforms
):
if new.core_platform == PLATFORM_ESP32:
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
) and new.core_platform == PLATFORM_ESP32:
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return new.framework == FRAMEWORK_ESP_IDF
return False

View File

@@ -56,9 +56,12 @@ class ESPHomeDataBase:
def from_node(self, node):
# pylint: disable=attribute-defined-outside-init
self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark)
if isinstance(node, yaml.ScalarNode):
if node.style is not None and node.style in "|>":
self._content_offset = 1
if (
isinstance(node, yaml.ScalarNode)
and node.style is not None
and node.style in "|>"
):
self._content_offset = 1
def from_database(self, database):
# pylint: disable=attribute-defined-outside-init