This commit is contained in:
klaudiusz223 2024-05-02 08:36:54 +02:00 committed by GitHub
commit 5211d5db42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 497 additions and 0 deletions

View File

@ -368,6 +368,7 @@ esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter
esphome/components/time_based_tilt/* @klaudiusz223
esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12

View File

@ -0,0 +1,85 @@
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,
)
CODEOWNERS = ["@klaudiusz223"]
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.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]))

View File

@ -0,0 +1,321 @@
#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";
const float TimeBasedTiltCover::TARGET_NONE = -1;
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() {
if (this->tilt_close_duration_ == 0 || this->tilt_open_duration_ == 0) {
this->tilt_close_duration_ = 0;
this->tilt_open_duration_ = 0;
}
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();
// recalibrating in extreme postions
if (this->fsm_state_ == STATE_CALIBRATING) {
if (now - this->last_recompute_time_ >= this->recalibration_time_) {
this->fsm_state_ = STATE_STOPPING;
}
return;
}
// STOPPING - determining the direction of the last movement and the stopping time. Needed to support interlocking
if (this->fsm_state_ == STATE_STOPPING) {
this->stop_trigger_->trigger();
if (this->current_operation != COVER_OPERATION_IDLE) {
this->interlocked_time_ = millis();
this->interlocked_direction_ =
this->current_operation == COVER_OPERATION_CLOSING ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
} else {
this->interlocked_direction_ = COVER_OPERATION_IDLE;
}
this->fsm_state_ = STATE_IDLE;
this->last_operation_ = this->current_operation;
this->current_operation = COVER_OPERATION_IDLE;
this->publish_state();
return;
}
// if the cover is not moving, check whether the new targets are set and if they are, compute move direction
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);
}
// interlocking support
if (this->current_operation == this->interlocked_direction_ &&
now - this->interlocked_time_ < 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;
}
// moving state
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->last_publish_time_ = now;
// If the cover is in extreme positions, run recalibration
if (this->recalibration_time_ > 0 &&
(((this->position == COVER_CLOSED && (tilt_time == 0 || this->tilt == COVER_CLOSED)) ||
(this->position == COVER_OPEN && (tilt_time == 0 || this->tilt == COVER_OPEN))))) {
this->fsm_state_ = STATE_CALIBRATING;
this->publish_state(false);
} 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_ > ((tilt_time / 5) > 1000 ? 1000 : (tilt_time / 5))) {
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->last_publish_time_ = now;
// If the cover is in extreme positions, run recalibration
if (this->recalibration_time_ > 0 &&
(((this->position == COVER_CLOSED && (tilt_time == 0 || this->tilt == COVER_CLOSED)) ||
(this->position == COVER_OPEN && (tilt_time == 0 || this->tilt == COVER_OPEN))))) {
this->fsm_state_ = STATE_CALIBRATING;
this->publish_state(false);
} 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_supports_stop(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;
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) {
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 == COVER_CLOSED && this->position == COVER_CLOSED && this->tilt != COVER_CLOSED) {
pos = TARGET_NONE;
this->target_tilt_ = COVER_CLOSED;
} else if (pos == COVER_OPEN && this->position == COVER_OPEN && this->tilt != COVER_OPEN) {
pos = TARGET_NONE;
this->target_tilt_ = COVER_OPEN;
} 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->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

View File

@ -0,0 +1,72 @@
#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 static float TARGET_NONE;
enum State : uint8_t { STATE_IDLE, STATE_MOVING, STATE_STOPPING, STATE_CALIBRATING };
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 interlocked_time_{0};
};
} // namespace time_based_tilt
} // namespace esphome

View File

@ -418,6 +418,7 @@ binary_sensor:
- cover.toggle: time_based_cover
- cover.toggle: endstop_cover
- cover.toggle: current_based_cover
- cover.toggle: time_based_tilt_cover
globals:
- id: my_global_string
@ -610,6 +611,23 @@ cover:
close_action:
- switch.turn_on: gpio_switch2
close_duration: 4.5min
- platform: time_based_tilt
name: Time Based Cover with Tilt
id: time_based_tilt_cover
stop_action:
- switch.turn_on: gpio_switch1
open_action:
- switch.turn_on: gpio_switch1
open_duration: 5min
close_action:
- switch.turn_on: gpio_switch2
close_duration: 4.5min
inertia_open_time: 300 ms
tilt_open_duration: 930 ms
inertia_close_time: 250 ms
tilt_close_duration: 900 ms
interlock_wait_time: 500ms
recalibration_time: 2500ms
- platform: current_based
name: Current Based Cover
id: current_based_cover