mirror of
https://github.com/esphome/esphome.git
synced 2025-01-06 04:41:49 +01:00
implementation of time based cover with tilt
This commit is contained in:
parent
8d6ffb9169
commit
12dd2cbcd5
4 changed files with 457 additions and 0 deletions
0
esphome/components/time_based_tilt/__init__.py
Normal file
0
esphome/components/time_based_tilt/__init__.py
Normal file
70
esphome/components/time_based_tilt/cover.py
Normal file
70
esphome/components/time_based_tilt/cover.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import cover
|
||||
from esphome.const import (
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CLOSE_DURATION,
|
||||
CONF_ID,
|
||||
CONF_OPEN_ACTION,
|
||||
CONF_OPEN_DURATION,
|
||||
CONF_STOP_ACTION,
|
||||
CONF_ASSUMED_STATE,
|
||||
)
|
||||
|
||||
time_based_tilt_ns = cg.esphome_ns.namespace("time_based_tilt")
|
||||
TimeBasedTiltCover = time_based_tilt_ns.class_("TimeBasedTiltCover", cover.Cover, cg.Component)
|
||||
|
||||
CONF_TILT_OPEN_DURATION = "tilt_open_duration"
|
||||
CONF_TILT_CLOSE_DURATION = "tilt_close_duration"
|
||||
CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time"
|
||||
CONF_RECALIBRATION_TIME = "recalibration_time"
|
||||
CONF_INERTIA_OPEN_TIME = "inertia_open_time"
|
||||
CONF_INERTIA_CLOSE_TIME = "inertia_close_time"
|
||||
|
||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TimeBasedTiltCover),
|
||||
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_TILT_OPEN_DURATION, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_TILT_CLOSE_DURATION, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_INTERLOCK_WAIT_TIME, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_RECALIBRATION_TIME, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_INERTIA_OPEN_TIME, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_INERTIA_CLOSE_TIME, default="0ms"): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await cover.register_cover(var, config)
|
||||
|
||||
await automation.build_automation(
|
||||
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
|
||||
)
|
||||
|
||||
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
|
||||
await automation.build_automation(
|
||||
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
|
||||
)
|
||||
|
||||
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
|
||||
await automation.build_automation(
|
||||
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
|
||||
)
|
||||
|
||||
cg.add(var.set_tilt_open_duration(config[CONF_TILT_OPEN_DURATION]))
|
||||
cg.add(var.set_tilt_close_duration(config[CONF_TILT_CLOSE_DURATION]))
|
||||
cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME]))
|
||||
cg.add(var.set_recalibration_time(config[CONF_RECALIBRATION_TIME]))
|
||||
cg.add(var.set_inertia_close_time(config[CONF_INERTIA_CLOSE_TIME]))
|
||||
cg.add(var.set_inertia_open_time(config[CONF_INERTIA_OPEN_TIME]))
|
||||
cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
|
311
esphome/components/time_based_tilt/time_based_tilt_cover.cpp
Normal file
311
esphome/components/time_based_tilt/time_based_tilt_cover.cpp
Normal file
|
@ -0,0 +1,311 @@
|
|||
#include "time_based_tilt_cover.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace time_based_tilt {
|
||||
|
||||
static const char *const TAG = "time_based_tilt.cover";
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void TimeBasedTiltCover::dump_config() {
|
||||
LOG_COVER("", "Time Based Tilt Cover", this);
|
||||
ESP_LOGCONFIG(TAG, " Open Duration: %.3fs", this->open_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Close Duration: %.3fs", this->close_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Tilt Close Duration: %.3fs", this->tilt_close_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Tilt Open Duration: %.3fs", this->tilt_open_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Interlock wait time: %.3fs", this->interlock_wait_time_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Inertia close time: %.3fs", this->inertia_close_time_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Inertia open time: %.3fs", this->inertia_open_time_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Recalibration time: %.3fs", this->recalibration_time_ / 1e3f);
|
||||
}
|
||||
void TimeBasedTiltCover::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(this);
|
||||
} else {
|
||||
this->position = 0.5f;
|
||||
this->tilt = 0.5f;
|
||||
}
|
||||
}
|
||||
bool TimeBasedTiltCover::is_at_target_position_() const {
|
||||
switch (this->current_operation) {
|
||||
case COVER_OPERATION_OPENING:
|
||||
return this->position >= this->target_position_;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return this->position <= this->target_position_;
|
||||
case COVER_OPERATION_IDLE:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool TimeBasedTiltCover::is_at_target_tilt_() const {
|
||||
switch (this->current_operation) {
|
||||
case COVER_OPERATION_OPENING:
|
||||
return this->tilt >= this->target_tilt_;
|
||||
case COVER_OPERATION_CLOSING:
|
||||
return this->tilt <= this->target_tilt_;
|
||||
case COVER_OPERATION_IDLE:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void TimeBasedTiltCover::loop() {
|
||||
|
||||
if ( this->fsm_state_ == STATE_IDLE && this->target_position_ == TARGET_NONE && this->target_tilt_ == TARGET_NONE ) return;
|
||||
|
||||
const uint32_t now = millis();
|
||||
|
||||
if ( this->fsm_state_ == STATE_CALIBRATING ) {
|
||||
if ( now - this->last_recompute_time_ >= this->recalibration_time_ ) {
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this->fsm_state_ == STATE_STOPPING )
|
||||
{
|
||||
this->stop_trigger_->trigger();
|
||||
this->interlockedTime = millis();
|
||||
this->interlocked_direction = this->current_operation == COVER_OPERATION_CLOSING ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
|
||||
this->fsm_state_ = STATE_IDLE;
|
||||
this->last_operation_ = this->current_operation;
|
||||
this->current_operation = COVER_OPERATION_IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this->fsm_state_ == STATE_IDLE && (this->target_position_ != TARGET_NONE || this->target_tilt_ != TARGET_NONE) )
|
||||
{
|
||||
|
||||
if (this->target_position_ != TARGET_NONE ) {
|
||||
this->current_operation = this->compute_direction(this->target_position_,this->position);
|
||||
} else {
|
||||
this->current_operation = this->compute_direction(this->target_tilt_,this->tilt);
|
||||
}
|
||||
|
||||
if ( this->current_operation == this->interlocked_direction
|
||||
&& now - this->interlockedTime < this->interlock_wait_time_ ) return;
|
||||
|
||||
Trigger<> *trig = this->current_operation == COVER_OPERATION_CLOSING ? this->close_trigger_ : this->open_trigger_;
|
||||
|
||||
trig->trigger();
|
||||
this->last_recompute_time_ = now;
|
||||
|
||||
this->fsm_state_ = STATE_MOVING;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( this->fsm_state_ == STATE_MOVING )
|
||||
{
|
||||
|
||||
auto travel_time = now - this->last_recompute_time_;
|
||||
this->last_recompute_time_ = now;
|
||||
|
||||
float dir_factor = this->current_operation == COVER_OPERATION_CLOSING ? -1.0 : 1.0 ;
|
||||
auto inertia_time = this->current_operation == COVER_OPERATION_CLOSING ? this->inertia_close_time_ : this->inertia_open_time_ ;
|
||||
|
||||
if ( inertia_time > 0 && this->inertia * dir_factor < 0.5f ) // inertia before movement
|
||||
{
|
||||
auto inertia_step = dir_factor * travel_time / inertia_time;
|
||||
this->inertia += inertia_step;
|
||||
auto rest = this->inertia - clamp( this->inertia , -0.5f, 0.5f);
|
||||
this->inertia = clamp( this->inertia, -0.5f, 0.5f);
|
||||
|
||||
if ( ! rest ) return; // the movement has not yet actually started
|
||||
travel_time = dir_factor * rest * inertia_time; // actual movement time taking inertia into account
|
||||
}
|
||||
|
||||
auto tilt_time = this->current_operation == COVER_OPERATION_CLOSING ? this->tilt_close_duration_ : this->tilt_open_duration_;
|
||||
|
||||
if ( tilt_time > 0 && ( this->tilt - 0.5f ) * dir_factor < 0.5f ) // tilting before movement
|
||||
{
|
||||
auto tilt_step = dir_factor * travel_time / tilt_time;
|
||||
this->tilt += tilt_step;
|
||||
auto rest = this->tilt - 0.5f - clamp( this->tilt - 0.5f , -0.5f, 0.5f);
|
||||
this->tilt = clamp( this->tilt , 0.0f, 1.0f);
|
||||
|
||||
if (this->target_position_ == TARGET_NONE && this->is_at_target_tilt_()){ // only tilting w/o position change
|
||||
this->last_recompute_time_ = now;
|
||||
this->target_tilt_ = TARGET_NONE;
|
||||
this->publish_state();
|
||||
this->last_publish_time_= now;
|
||||
|
||||
if ( ( this->position == 0.0f && this->tilt == 0.0f ) || ( this->position == 1.0f && this->tilt == 1.0f ) ) // start recalibration
|
||||
{
|
||||
this->fsm_state_ = STATE_CALIBRATING;
|
||||
} else {
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
|
||||
return; // only tilting w/o position change so no need to recompute position
|
||||
}
|
||||
|
||||
if ( now - this->last_publish_time_ > 300 ){
|
||||
this->publish_state(false);
|
||||
this->last_publish_time_= now;
|
||||
}
|
||||
|
||||
if ( ! rest ) return; // the movement has not yet actually started
|
||||
|
||||
travel_time = dir_factor * rest * tilt_time; // actual movement time taking tilt into account
|
||||
}
|
||||
|
||||
auto move_time = this->current_operation == COVER_OPERATION_CLOSING ? this->close_duration_ : this->open_duration_ ;
|
||||
|
||||
if ( move_time > 0 && ( this->position - 0.5f ) * dir_factor < 0.5f )
|
||||
{
|
||||
auto move_step = dir_factor * travel_time / move_time;
|
||||
this->position += move_step;
|
||||
this->position = clamp( this->position , 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (this->is_at_target_position_()){
|
||||
this->last_recompute_time_ = now;
|
||||
this->target_position_ = TARGET_NONE;
|
||||
this->publish_state();
|
||||
this->last_publish_time_= now;
|
||||
|
||||
if ( ( this->position == 0.0f && this->tilt == 0.0f ) || ( this->position == 1.0f && this->tilt == 1.0f ) )
|
||||
{
|
||||
this->fsm_state_ = STATE_CALIBRATING;
|
||||
} else {
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
|
||||
if ( now - this->last_publish_time_ > 1000 ){
|
||||
this->publish_state(false);
|
||||
this->last_publish_time_= now;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
float TimeBasedTiltCover::get_setup_priority() const { return setup_priority::DATA; }
|
||||
CoverTraits TimeBasedTiltCover::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_position(true);
|
||||
traits.set_supports_tilt(this->tilt_close_duration_ !=0 && this->tilt_open_duration_ !=0);
|
||||
traits.set_supports_toggle(true);
|
||||
traits.set_is_assumed_state(this->assumed_state_);
|
||||
return traits;
|
||||
}
|
||||
void TimeBasedTiltCover::control(const CoverCall &call) {
|
||||
if (call.get_stop()) {
|
||||
this->target_position_ = TARGET_NONE;
|
||||
this->target_tilt_= TARGET_NONE;
|
||||
if (this->fsm_state_ == STATE_MOVING){
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (call.get_position().has_value() && call.get_tilt().has_value() ) {
|
||||
auto pos = *call.get_position();
|
||||
auto til = *call.get_tilt();
|
||||
|
||||
if (this->round_position(pos) == this->round_position(this->position))
|
||||
pos = TARGET_NONE;
|
||||
if (this->round_position(til) == this->round_position(this->tilt))
|
||||
til = TARGET_NONE;
|
||||
|
||||
this->target_position_ = pos;
|
||||
this->target_tilt_ = til;
|
||||
|
||||
if (this->fsm_state_ == STATE_MOVING){ //TODO: check direction also on tilting
|
||||
auto direction = COVER_OPERATION_IDLE;
|
||||
if ( this->target_position_ != TARGET_NONE && this->target_position_ != this->position) {
|
||||
direction = this->compute_direction(this->target_position_ , this->position );
|
||||
} else if ( this->target_tilt_ != TARGET_NONE && this->target_tilt_ != this->tilt){
|
||||
direction = this->compute_direction( this->target_tilt_ , this->tilt );
|
||||
}
|
||||
|
||||
if (direction != this->current_operation )
|
||||
{
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
} else if (call.get_position().has_value()) {
|
||||
auto pos = *call.get_position();
|
||||
|
||||
if ( pos == 0.0f && this->position == 0.0f && this->tilt != 0.0f ){
|
||||
pos = TARGET_NONE;
|
||||
this->target_tilt_ = 0.0f;
|
||||
} else if ( pos == 1.0f && this->position == 1.0f && this->tilt != 1.0f ){
|
||||
pos = TARGET_NONE;
|
||||
this->target_tilt_ = 1.0f;
|
||||
} else if ( this->round_position(pos) == this->round_position(this->position) ){
|
||||
pos = TARGET_NONE;
|
||||
}
|
||||
|
||||
this->target_position_ = pos;
|
||||
|
||||
if (this->fsm_state_ == STATE_MOVING){
|
||||
auto direction = COVER_OPERATION_IDLE;
|
||||
if ( this->target_position_ != TARGET_NONE && this->target_position_ != this->position) {
|
||||
direction = this->compute_direction(this->target_position_ , this->position );
|
||||
this->target_tilt_ = TARGET_NONE; // unset previous target tilt
|
||||
} else if ( this->target_tilt_ != TARGET_NONE && this->target_tilt_ != this->tilt){
|
||||
direction = this->compute_direction( this->target_tilt_ , this->tilt );
|
||||
}
|
||||
|
||||
if (direction != this->current_operation )
|
||||
{
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
} else if (call.get_tilt().has_value()) {
|
||||
auto til = *call.get_tilt();
|
||||
if ( this->round_position(til) == this->round_position(this->tilt) )
|
||||
{
|
||||
til = TARGET_NONE;
|
||||
}
|
||||
|
||||
this->target_tilt_ = til;
|
||||
|
||||
if (this->fsm_state_ == STATE_MOVING){
|
||||
auto direction = COVER_OPERATION_IDLE;
|
||||
if ( this->target_tilt_ != TARGET_NONE && this->target_tilt_ != this->tilt){
|
||||
direction = this->compute_direction( this->target_tilt_ , this->tilt );
|
||||
this->target_position_ = TARGET_NONE;
|
||||
}
|
||||
|
||||
if (direction != this->current_operation )
|
||||
{
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (call.get_toggle().has_value()) {
|
||||
if (this->current_operation != COVER_OPERATION_IDLE) {
|
||||
this->publish_state();
|
||||
this->fsm_state_ = STATE_STOPPING;
|
||||
this->target_position_ = TARGET_NONE;
|
||||
this->target_tilt_ = TARGET_NONE;
|
||||
} else {
|
||||
if ( this->position == COVER_CLOSED && this->tilt == COVER_CLOSED ) {
|
||||
this->target_position_ = COVER_OPEN;
|
||||
} else if ( this->position == COVER_OPEN && this->tilt == COVER_OPEN ) {
|
||||
this->target_position_ = COVER_CLOSED;
|
||||
} else if ( this->last_operation_ == COVER_OPERATION_CLOSING ) {
|
||||
if (this->position != COVER_OPEN) {
|
||||
this->target_position_ = COVER_OPEN;
|
||||
} else {
|
||||
this->target_tilt_ = COVER_OPEN;
|
||||
}
|
||||
} else {
|
||||
if (this->position != COVER_CLOSED) {
|
||||
this->target_position_ = COVER_CLOSED;
|
||||
} else {
|
||||
this->target_tilt_ = COVER_CLOSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace time_based_tilt
|
||||
} // namespace esphome
|
76
esphome/components/time_based_tilt/time_based_tilt_cover.h
Normal file
76
esphome/components/time_based_tilt/time_based_tilt_cover.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace time_based_tilt {
|
||||
|
||||
class TimeBasedTiltCover : public cover::Cover, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
|
||||
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
|
||||
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
|
||||
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
|
||||
void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; }
|
||||
void set_tilt_open_duration(uint32_t tilt_open_duration) { this->tilt_open_duration_ = tilt_open_duration; }
|
||||
void set_tilt_close_duration(uint32_t tilt_close_duration) { this->tilt_close_duration_ = tilt_close_duration; }
|
||||
void set_interlock_wait_time(uint32_t interlock_wait_time) { this->interlock_wait_time_ = interlock_wait_time; }
|
||||
void set_recalibration_time(uint32_t recalibration_time) { this->recalibration_time_ = recalibration_time; }
|
||||
void set_inertia_open_time(uint32_t inertia_time) { this->inertia_open_time_ = inertia_time; }
|
||||
void set_inertia_close_time(uint32_t inertia_time) { this->inertia_close_time_ = inertia_time; }
|
||||
cover::CoverOperation compute_direction(float target, float current){ return target < current ? cover::COVER_OPERATION_CLOSING : cover::COVER_OPERATION_OPENING ; };
|
||||
float round_position(float pos) { return round(100 * pos)/100; };
|
||||
cover::CoverTraits get_traits() override;
|
||||
void set_assumed_state(bool value) { this->assumed_state_ = value; }
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
bool is_at_target_position_() const;
|
||||
bool is_at_target_tilt_() const;
|
||||
|
||||
Trigger<> *open_trigger_{new Trigger<>()};
|
||||
Trigger<> *close_trigger_{new Trigger<>()};
|
||||
Trigger<> *stop_trigger_{new Trigger<>()};
|
||||
|
||||
uint32_t open_duration_;
|
||||
uint32_t close_duration_;
|
||||
|
||||
uint32_t tilt_close_duration_;
|
||||
uint32_t tilt_open_duration_;
|
||||
|
||||
uint32_t interlock_wait_time_;
|
||||
uint32_t recalibration_time_;
|
||||
uint32_t inertia_open_time_;
|
||||
uint32_t inertia_close_time_;
|
||||
|
||||
const float TARGET_NONE{-1};
|
||||
enum State : uint8_t {
|
||||
STATE_IDLE,
|
||||
STATE_MOVING,
|
||||
STATE_STOPPING,
|
||||
STATE_CALIBRATING
|
||||
};
|
||||
|
||||
Trigger<> *prev_command_trigger_{nullptr};
|
||||
uint32_t last_recompute_time_{0};
|
||||
uint32_t last_publish_time_{0};
|
||||
float target_position_{TARGET_NONE};
|
||||
float target_tilt_{TARGET_NONE};
|
||||
float inertia{0.0f};
|
||||
bool has_built_in_endstop_{false};
|
||||
bool assumed_state_{false};
|
||||
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
|
||||
State fsm_state_{STATE_IDLE};
|
||||
cover::CoverOperation interlocked_direction{cover::COVER_OPERATION_IDLE};
|
||||
uint32_t interlockedTime{0};
|
||||
};
|
||||
|
||||
} // namespace time_based_tilt
|
||||
} // namespace esphome
|
Loading…
Reference in a new issue