1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-02 16:11:53 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Jesse Hills
53fda0e96d Merge pull request #8358 from esphome/bump-2025.2.2
2025.2.2
2025-03-03 21:23:20 +13:00
Jesse Hills
7b8e68c73a Bump version to 2025.2.2 2025-03-03 17:15:40 +13:00
Jesse Hills
db666e44a7 [ltr390] Move calculation to allow dynamic setting of gain and resolution (#8343) 2025-03-03 17:15:40 +13:00
J. Nick Koston
903d033e0f Bump aioesphomeapi to 29.3.2 (#8353) 2025-03-03 17:15:40 +13:00
Kevin Ahrendt
19d938ce48 [audio] Determine http timeout based on duration since last successful read (#8341) 2025-03-03 17:15:40 +13:00
J. Nick Koston
653318479a Fix end_of_scan_ not being called while disconnecting (#8328) 2025-03-03 17:15:40 +13:00
Jesse Hills
c13174c318 Merge pull request #8326 from esphome/bump-2025.2.1
2025.2.1
2025-02-26 11:01:14 +13:00
Jesse Hills
8fa157581e Bump version to 2025.2.1 2025-02-26 09:49:22 +13:00
Keith Burzinski
7114d6bdd1 [esp32_touch] Fix variants, add tests for variants (#8320) 2025-02-26 09:49:22 +13:00
J. Nick Koston
eca0c21966 Fix bluetooth race when disconnect called while still connecting (#8297) 2025-02-26 09:49:22 +13:00
esphomebot
20c9c410af Update webserver local assets to 20250224-195901 (#8312) 2025-02-26 09:49:22 +13:00
J. Nick Koston
79af437f48 Fix BLE max notifications with ESP-IDF 5.x (#8301) 2025-02-26 09:49:22 +13:00
J. Nick Koston
6e27003787 Bump aioesphomeapi to 29.1.1 (#8274) 2025-02-26 09:49:22 +13:00
Jesse Hills
7c9726859f Merge pull request #8275 from esphome/bump-2025.2.0
2025.2.0
2025-02-19 22:19:09 +13:00
Jesse Hills
ba79e2d7b1 Bump version to 2025.2.0 2025-02-19 13:40:15 +13:00
Jesse Hills
ae65f76dfe Merge pull request #8272 from esphome/bump-2025.2.0b6
2025.2.0b6
2025-02-19 10:44:08 +13:00
Jesse Hills
4d380214df Bump version to 2025.2.0b6 2025-02-19 09:22:52 +13:00
J. Nick Koston
c5ebf7683e Bump zeroconf to 0.145.1 (#8267) 2025-02-19 09:22:52 +13:00
G-Two
a973adda67 Increase default repeat delay for Toto remote transmitter protocol (#8265) 2025-02-19 09:22:52 +13:00
J. Nick Koston
d9b419eaf5 Bump openssh-client to 1:9.2p1-2+deb12u4 to fix docker builds (#8269) 2025-02-19 09:22:52 +13:00
Jesse Hills
2bc9782ce7 Merge pull request #8264 from esphome/bump-2025.2.0b5
2025.2.0b5
2025-02-18 14:13:33 +13:00
Jesse Hills
6583e17810 Bump version to 2025.2.0b5 2025-02-18 13:39:42 +13:00
J. Nick Koston
64c8bcef2e Bump aioesphomeapi to 29.1.0 (#8105) 2025-02-18 13:39:42 +13:00
J. Nick Koston
f9da8dbfb8 Replace glyphsets with esphome_glyphsets (#8261) 2025-02-18 13:39:42 +13:00
Jesse Hills
26d25464da Merge pull request #8259 from esphome/bump-2025.2.0b4
2025.2.0b4
2025-02-17 13:09:45 +13:00
Jesse Hills
78b55e22ee Bump version to 2025.2.0b4 2025-02-17 12:14:06 +13:00
Ali Jafri
9ee5227fe0 DHT platform now supports modules with inbuilt external resistor (#8257) 2025-02-17 12:14:06 +13:00
J. Nick Koston
e89603fe3b Bump zeroconf to 0.144.3 (#8253) 2025-02-17 12:14:06 +13:00
Djordje Mandic
c0804d665d [scd30] Increase minimal CONF_UPDATE_INTERVAL from 1 to 2 seconds (#8256) 2025-02-17 12:14:05 +13:00
Samuel Sieb
a67b85eabf don't crash on null pages (#8254)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-02-17 12:14:05 +13:00
30 changed files with 4832 additions and 4669 deletions

View File

@@ -35,7 +35,7 @@ RUN \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u8 \
openssh-client=1:9.2p1-2+deb12u3 \
openssh-client=1:9.2p1-2+deb12u4 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \

View File

@@ -1,12 +1,11 @@
from __future__ import annotations
import asyncio
import logging
from datetime import datetime
from typing import Any
import logging
from typing import TYPE_CHECKING, Any
from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
@@ -14,6 +13,12 @@ from esphome.core import CORE
from . import CONF_ENCRYPTION
if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import (
SubscribeLogsResponse, # pylint: disable=no-name-in-module
)
_LOGGER = logging.getLogger(__name__)

View File

@@ -15,6 +15,8 @@ namespace audio {
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
// The number of times the http read times out with no data before throwing an error
static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
@@ -97,7 +99,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
client_config.user_data = this;
client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
client_config.keep_alive_enable = true;
client_config.timeout_ms = 5000; // Shouldn't trigger watchdog resets if caller runs in a task
client_config.timeout_ms = CONNECTION_TIMEOUT_MS; // Shouldn't trigger watchdog resets if caller runs in a task
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (uri.find("https:") != std::string::npos) {
@@ -189,7 +191,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
file_type = this->audio_file_type_;
}
this->no_data_read_count_ = 0;
this->last_data_read_ms_ = millis();
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(this->buffer_size_);
if (this->output_transfer_buffer_ == nullptr) {
@@ -271,8 +273,7 @@ AudioReaderState AudioReader::http_read_() {
if (received_len > 0) {
this->output_transfer_buffer_->increase_buffer_length(received_len);
this->no_data_read_count_ = 0;
this->last_data_read_ms_ = millis();
} else if (received_len < 0) {
// HTTP read error
this->cleanup_connection_();
@@ -280,12 +281,11 @@ AudioReaderState AudioReader::http_read_() {
} else {
if (bytes_to_read > 0) {
// Read timed out
++this->no_data_read_count_;
if (this->no_data_read_count_ >= ERROR_COUNT_NO_DATA_READ_TIMEOUT) {
// Timed out with no data read too many times, so the http read has failed
if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
}
}

View File

@@ -71,7 +71,7 @@ class AudioReader {
void cleanup_connection_();
size_t buffer_size_;
uint32_t no_data_read_count_;
uint32_t last_data_read_ms_;
esp_http_client_handle_t client_{nullptr};

View File

@@ -23,6 +23,7 @@ void DHT::dump_config() {
} else {
ESP_LOGCONFIG(TAG, " Model: DHT22 (or equivalent)");
}
ESP_LOGCONFIG(TAG, " Internal Pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this);
@@ -101,7 +102,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else {
delayMicroseconds(800);
}
this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->pin_->pin_mode(this->pin_->get_flags());
{
InterruptLock lock;

View File

@@ -34,7 +34,7 @@ DHT = dht_ns.class_("DHT", cg.PollingComponent)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DHT),
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,

View File

@@ -815,8 +815,20 @@ void Display::test_card() {
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() { this->next_->show(); }
void DisplayPage::show_prev() { this->prev_->show(); }
void DisplayPage::show_next() {
if (this->next_ == nullptr) {
ESP_LOGE(TAG, "no next page");
return;
}
this->next_->show();
}
void DisplayPage::show_prev() {
if (this->prev_ == nullptr) {
ESP_LOGE(TAG, "no previous page");
return;
}
this->prev_->show();
}
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }

View File

@@ -123,12 +123,49 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING)
if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_,
this->address_str_.c_str());
return;
ESP_LOGI(TAG, "[%d] [%s] Disconnecting.", this->connection_index_, this->address_str_.c_str());
}
if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_,
this->address_str_.c_str());
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_,
this->address_str_.c_str());
this->want_disconnect_ = true;
return;
}
this->unconditional_disconnect();
}
void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_,
this->address_str_.c_str());
return;
}
if (this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_,
this->address_str_.c_str());
return;
}
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
//
// This is a fatal error, but we can't do anything about it
// and it likely means the BLE stack is in a bad state.
//
// In the future we might consider App.reboot() here since
// the BLE stack is in an indeterminate state.
//
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
err);
}
@@ -184,12 +221,38 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->log_event_("ESP_GATTC_OPEN_EVT");
this->conn_id_ = param->open.conn_id;
this->service_count_ = 0;
if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
if (this->state_ == espbt::ClientState::CONNECTED) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_,
this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_,
this->address_str_.c_str(), param->open.status);
} else {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
}
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
param->open.status);
this->set_state(espbt::ClientState::IDLE);
break;
}
if (this->want_disconnect_) {
// Disconnect was requested after connecting started,
// but before the connection was established. Now that we have
// this->conn_id_ set, we can disconnect it.
this->unconditional_disconnect();
this->conn_id_ = UNSET_CONN_ID;
break;
}
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
@@ -241,6 +304,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->log_event_("ESP_GATTC_CLOSE_EVT");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
break;
}
case ESP_GATTC_SEARCH_RES_EVT: {

View File

@@ -21,6 +21,8 @@ namespace esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker;
static const int UNSET_CONN_ID = 0xFFFF;
class BLEClientBase : public espbt::ESPBTClient, public Component {
public:
void setup() override;
@@ -37,6 +39,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void connect() override;
esp_err_t pair();
void disconnect() override;
void unconditional_disconnect();
void release_services();
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
@@ -94,7 +97,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
int gattc_if_;
esp_bd_addr_t remote_bda_;
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
uint16_t conn_id_{0xFFFF};
uint16_t conn_id_{UNSET_CONN_ID};
uint64_t address_{0};
bool auto_connect_{false};
std::string address_str_{};

View File

@@ -238,6 +238,12 @@ async def to_code(config):
else:
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
# CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
# max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
# is enough in 4.x
# https://github.com/esphome/issues/issues/6808
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 0, 0):
add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -176,9 +176,9 @@ void ESP32BLETracker::loop() {
https://github.com/espressif/esp-idf/issues/6688
*/
if (!connecting && !disconnecting && xSemaphoreTake(this->scan_end_lock_, 0L)) {
if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) {
if (this->scan_continuous_) {
if (!promote_to_connecting && !this->scan_start_failed_ && !this->scan_set_param_failed_) {
if (!disconnecting && !promote_to_connecting && !this->scan_start_failed_ && !this->scan_set_param_failed_) {
this->start_scan_(false);
} else {
// We didn't start the scan, so we need to release the lock

View File

@@ -173,12 +173,22 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual void set_state(ClientState st) { this->state_ = st; }
virtual void set_state(ClientState st) {
this->state_ = st;
if (st == ClientState::IDLE) {
this->want_disconnect_ = false;
}
}
ClientState state() const { return state_; }
int app_id;
protected:
ClientState state_{ClientState::INIT};
// want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false};
};
class ESP32BLETracker : public Component,

View File

@@ -52,7 +52,7 @@ void ESP32TouchComponent::setup() {
}
#endif
#if ESP_IDF_VERSION_MAJOR >= 5
#if ESP_IDF_VERSION_MAJOR >= 5 && defined(USE_ESP32_VARIANT_ESP32)
touch_pad_set_measurement_clock_cycles(this->meas_cycle_);
touch_pad_set_measurement_interval(this->sleep_cycle_);
#else

View File

@@ -5,8 +5,8 @@ import os
from pathlib import Path
import re
import esphome_glyphsets as glyphsets
import freetype
import glyphsets
import requests
from esphome import core, external_files

View File

@@ -1,7 +1,7 @@
#include "ltr390.h"
#include <bitset>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <bitset>
namespace esphome {
namespace ltr390 {
@@ -91,7 +91,12 @@ void LTR390Component::read_uvs_() {
uint32_t uv = *val;
if (this->uvi_sensor_ != nullptr) {
this->uvi_sensor_->publish_state((uv / this->sensitivity_uv_) * this->wfac_);
// Set sensitivity by linearly scaling against known value in the datasheet
float gain_scale_uv = GAINVALUES[this->gain_uv_] / GAIN_MAX;
float intg_scale_uv = (RESOLUTIONVALUE[this->res_uv_] * 100) / INTG_MAX;
float sensitivity_uv = SENSITIVITY_MAX * gain_scale_uv * intg_scale_uv;
this->uvi_sensor_->publish_state((uv / sensitivity_uv) * this->wfac_);
}
if (this->uv_sensor_ != nullptr) {
@@ -166,11 +171,6 @@ void LTR390Component::setup() {
return;
}
// Set sensitivity by linearly scaling against known value in the datasheet
float gain_scale_uv = GAINVALUES[this->gain_uv_] / GAIN_MAX;
float intg_scale_uv = (RESOLUTIONVALUE[this->res_uv_] * 100) / INTG_MAX;
this->sensitivity_uv_ = SENSITIVITY_MAX * gain_scale_uv * intg_scale_uv;
// Set sensor read state
this->reading_ = false;

View File

@@ -77,7 +77,6 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice {
LTR390GAIN gain_uv_;
LTR390RESOLUTION res_als_;
LTR390RESOLUTION res_uv_;
float sensitivity_uv_;
float wfac_;
sensor::Sensor *light_sensor_{nullptr};

View File

@@ -36,7 +36,7 @@ template<typename... Ts> class TotoAction : public RemoteTransmitterActionBase<T
data.rc_code_2 = this->rc_code_2_.value(x...);
data.command = this->command_.value(x...);
this->set_send_times(this->send_times_.value_or(x..., 3));
this->set_send_wait(this->send_wait_.value_or(x..., 32000));
this->set_send_wait(this->send_wait_.value_or(x..., 36000));
TotoProtocol().encode(dst, data);
}
};

View File

@@ -75,7 +75,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
min=core.TimePeriod(seconds=2), max=core.TimePeriod(seconds=1800)
),
),
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.2.0b3"
__version__ = "2025.2.2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -13,11 +13,11 @@ platformio==6.1.16 # When updating platformio, also update Dockerfile
esptool==4.7.0
click==8.1.7
esphome-dashboard==20250212.0
aioesphomeapi==24.6.2
zeroconf==0.144.1
aioesphomeapi==29.3.2
zeroconf==0.145.1
puremagic==1.27
ruamel.yaml==0.18.6 # dashboard_import
glyphsets==1.0.0
esphome-glyphsets==0.1.0
pillow==10.4.0
freetype-py==2.5.1

View File

@@ -0,0 +1,15 @@
esp32_touch:
setup_mode: false
sleep_duration: 27ms
measurement_duration: 8ms
low_voltage_reference: 0.5V
high_voltage_reference: 2.7V
voltage_attenuation: 1.5V
binary_sensor:
- platform: esp32_touch
name: ESP32 Touch Pad
pin: ${pin}
threshold: 1000
on_press:
- logger.log: "I'm touched!"

View File

@@ -10,7 +10,7 @@ esp32_touch:
binary_sensor:
- platform: esp32_touch
name: ESP32 Touch Pad
pin: 27
pin: ${pin}
threshold: 1000
on_press:
- logger.log: "I'm touched!"

View File

@@ -1 +1,4 @@
substitutions:
pin: GPIO27
<<: !include common.yaml

View File

@@ -1 +1,4 @@
substitutions:
pin: GPIO27
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
substitutions:
pin: GPIO12
<<: !include common-variants.yaml

View File

@@ -0,0 +1,4 @@
substitutions:
pin: GPIO12
<<: !include common-variants.yaml

View File

@@ -0,0 +1,4 @@
substitutions:
pin: GPIO12
<<: !include common-variants.yaml

View File

@@ -0,0 +1,4 @@
substitutions:
pin: GPIO12
<<: !include common-variants.yaml