mirror of
https://github.com/esphome/esphome.git
synced 2025-03-29 11:49:00 +01:00
Cover component for Tormatic and Novoferm garage doors (#5933)
This commit is contained in:
parent
23e04e18f8
commit
74a25a7e76
13 changed files with 718 additions and 0 deletions
|
@ -448,6 +448,7 @@ esphome/components/tmp102/* @timsavage
|
|||
esphome/components/tmp1075/* @sybrenstuvel
|
||||
esphome/components/tmp117/* @Azimath
|
||||
esphome/components/tof10120/* @wstrzalka
|
||||
esphome/components/tormatic/* @ti-mo
|
||||
esphome/components/toshiba/* @kbx81
|
||||
esphome/components/touchscreen/* @jesserockz @nielsnl68
|
||||
esphome/components/tsl2591/* @wjcarpenter
|
||||
|
|
1
esphome/components/tormatic/__init__.py
Normal file
1
esphome/components/tormatic/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
CODEOWNERS = ["@ti-mo"]
|
47
esphome/components/tormatic/cover.py
Normal file
47
esphome/components/tormatic/cover.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import cover, uart
|
||||
from esphome.const import (
|
||||
CONF_CLOSE_DURATION,
|
||||
CONF_ID,
|
||||
CONF_OPEN_DURATION,
|
||||
)
|
||||
|
||||
tormatic_ns = cg.esphome_ns.namespace("tormatic")
|
||||
Tormatic = tormatic_ns.class_("Tormatic", cover.Cover, cg.PollingComponent)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.COVER_SCHEMA.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.polling_component_schema("300ms"))
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Tormatic),
|
||||
cv.Optional(
|
||||
CONF_OPEN_DURATION, default="15s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_CLOSE_DURATION, default="22s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"tormatic",
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
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 uart.register_uart_device(var, config)
|
||||
|
||||
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
|
||||
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
|
355
esphome/components/tormatic/tormatic_cover.cpp
Normal file
355
esphome/components/tormatic/tormatic_cover.cpp
Normal file
|
@ -0,0 +1,355 @@
|
|||
#include <vector>
|
||||
|
||||
#include "tormatic_cover.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace esphome {
|
||||
namespace tormatic {
|
||||
|
||||
static const char *const TAG = "tormatic.cover";
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void Tormatic::setup() {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assume gate is closed without preexisting state.
|
||||
this->position = 0.0f;
|
||||
}
|
||||
|
||||
cover::CoverTraits Tormatic::get_traits() {
|
||||
auto traits = CoverTraits();
|
||||
traits.set_supports_stop(true);
|
||||
traits.set_supports_position(true);
|
||||
traits.set_is_assumed_state(false);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void Tormatic::dump_config() {
|
||||
LOG_COVER("", "Tormatic Cover", this);
|
||||
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
|
||||
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
|
||||
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
|
||||
}
|
||||
}
|
||||
|
||||
void Tormatic::update() { this->request_gate_status_(); }
|
||||
|
||||
void Tormatic::loop() {
|
||||
auto o_status = this->read_gate_status_();
|
||||
if (o_status) {
|
||||
auto status = o_status.value();
|
||||
|
||||
this->recalibrate_duration_(status);
|
||||
this->handle_gate_status_(status);
|
||||
}
|
||||
|
||||
this->recompute_position_();
|
||||
this->stop_at_target_();
|
||||
}
|
||||
|
||||
void Tormatic::control(const cover::CoverCall &call) {
|
||||
if (call.get_stop()) {
|
||||
this->send_gate_command_(PAUSED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (call.get_position().has_value()) {
|
||||
auto pos = call.get_position().value();
|
||||
this->control_position_(pos);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the Cover's publish_state with a rate limiter. Publishes if the last
|
||||
// publish was longer than ratelimit milliseconds ago. 0 to disable.
|
||||
void Tormatic::publish_state(bool save, uint32_t ratelimit) {
|
||||
auto now = millis();
|
||||
if ((now - this->last_publish_time_) < ratelimit) {
|
||||
return;
|
||||
}
|
||||
this->last_publish_time_ = now;
|
||||
|
||||
Cover::publish_state(save);
|
||||
};
|
||||
|
||||
// Recalibrate the gate's estimated open or close duration based on the
|
||||
// actual time the operation took.
|
||||
void Tormatic::recalibrate_duration_(GateStatus s) {
|
||||
if (this->current_status_ == s) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = millis();
|
||||
auto old = this->current_status_;
|
||||
|
||||
// Gate paused halfway through opening or closing, invalidate the start time
|
||||
// of the current operation. Close/open durations can only be accurately
|
||||
// calibrated on full open or close cycle due to motor acceleration.
|
||||
if (s == PAUSED) {
|
||||
ESP_LOGD(TAG, "Gate paused, clearing direction start time");
|
||||
this->direction_start_time_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the start time of a state transition if the gate was in the fully
|
||||
// open or closed position before the command.
|
||||
if ((old == CLOSED && s == OPENING) || (old == OPENED && s == CLOSING)) {
|
||||
ESP_LOGD(TAG, "Gate started moving from fully open or closed state");
|
||||
this->direction_start_time_ = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// The gate was resumed from a paused state, don't attempt recalibration.
|
||||
if (this->direction_start_time_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s == OPENED) {
|
||||
this->open_duration_ = now - this->direction_start_time_;
|
||||
ESP_LOGI(TAG, "Recalibrated the gate's open duration to %dms", this->open_duration_);
|
||||
}
|
||||
if (s == CLOSED) {
|
||||
this->close_duration_ = now - this->direction_start_time_;
|
||||
ESP_LOGI(TAG, "Recalibrated the gate's close duration to %dms", this->close_duration_);
|
||||
}
|
||||
|
||||
this->direction_start_time_ = 0;
|
||||
}
|
||||
|
||||
// Set the Cover's internal state based on a status message
|
||||
// received from the unit.
|
||||
void Tormatic::handle_gate_status_(GateStatus s) {
|
||||
if (this->current_status_ == s) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Status changed from %s to %s", gate_status_to_str(this->current_status_), gate_status_to_str(s));
|
||||
|
||||
switch (s) {
|
||||
case OPENED:
|
||||
// The Novoferm 423 doesn't respond to the first 'Close' command after
|
||||
// being opened completely. Sending a pause command after opening fixes
|
||||
// that.
|
||||
this->send_gate_command_(PAUSED);
|
||||
|
||||
this->position = COVER_OPEN;
|
||||
break;
|
||||
case CLOSED:
|
||||
this->position = COVER_CLOSED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->current_status_ = s;
|
||||
this->current_operation = gate_status_to_cover_operation(s);
|
||||
|
||||
this->publish_state(true);
|
||||
|
||||
// This timestamp is used to generate position deltas on every loop() while
|
||||
// the gate is moving. Bump it on each state transition so the first tick
|
||||
// doesn't generate a huge delta.
|
||||
this->last_recompute_time_ = millis();
|
||||
}
|
||||
|
||||
// Recompute the gate's position and publish the results while
|
||||
// the gate is moving. No-op when the gate is idle.
|
||||
void Tormatic::recompute_position_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = millis();
|
||||
uint32_t diff = now - this->last_recompute_time_;
|
||||
|
||||
auto direction = +1.0f;
|
||||
uint32_t duration = this->open_duration_;
|
||||
if (this->current_operation == COVER_OPERATION_CLOSING) {
|
||||
direction = -1.0f;
|
||||
duration = this->close_duration_;
|
||||
}
|
||||
|
||||
auto delta = direction * diff / duration;
|
||||
|
||||
this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
|
||||
|
||||
this->last_recompute_time_ = now;
|
||||
|
||||
this->publish_state(true, 250);
|
||||
}
|
||||
|
||||
// Start moving the gate in the direction of the target position.
|
||||
void Tormatic::control_position_(float target) {
|
||||
if (target == this->position) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target == COVER_OPEN) {
|
||||
ESP_LOGI(TAG, "Fully opening gate");
|
||||
this->send_gate_command_(OPENED);
|
||||
return;
|
||||
}
|
||||
if (target == COVER_CLOSED) {
|
||||
ESP_LOGI(TAG, "Fully closing gate");
|
||||
this->send_gate_command_(CLOSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't set target position when fully opening or closing the gate, the gate
|
||||
// stops automatically when it reaches the configured open/closed positions.
|
||||
this->target_position_ = target;
|
||||
|
||||
if (target > this->position) {
|
||||
ESP_LOGI(TAG, "Opening gate towards %.1f", target);
|
||||
this->send_gate_command_(OPENED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target < this->position) {
|
||||
ESP_LOGI(TAG, "Closing gate towards %.1f", target);
|
||||
this->send_gate_command_(CLOSED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the gate if it is moving at or beyond its target position. Target
|
||||
// position is only set when the gate is requested to move to a halfway
|
||||
// position.
|
||||
void Tormatic::stop_at_target_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE) {
|
||||
return;
|
||||
}
|
||||
if (!this->target_position_) {
|
||||
return;
|
||||
}
|
||||
auto target = this->target_position_.value();
|
||||
|
||||
if (this->current_operation == COVER_OPERATION_OPENING && this->position < target) {
|
||||
return;
|
||||
}
|
||||
if (this->current_operation == COVER_OPERATION_CLOSING && this->position > target) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->send_gate_command_(PAUSED);
|
||||
this->target_position_.reset();
|
||||
}
|
||||
|
||||
// Read a GateStatus from the unit. The unit only sends messages in response to
|
||||
// status requests or commands, so a message needs to be sent first.
|
||||
optional<GateStatus> Tormatic::read_gate_status_() {
|
||||
if (this->available() < sizeof(MessageHeader)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto o_hdr = this->read_data_<MessageHeader>();
|
||||
if (!o_hdr) {
|
||||
ESP_LOGE(TAG, "Timeout reading message header");
|
||||
return {};
|
||||
}
|
||||
auto hdr = o_hdr.value();
|
||||
|
||||
switch (hdr.type) {
|
||||
case STATUS: {
|
||||
if (hdr.payload_size() != sizeof(StatusReply)) {
|
||||
ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(),
|
||||
sizeof(StatusReply));
|
||||
}
|
||||
|
||||
// Read a StatusReply requested by update().
|
||||
auto o_status = this->read_data_<StatusReply>();
|
||||
if (!o_status) {
|
||||
return {};
|
||||
}
|
||||
auto status = o_status.value();
|
||||
|
||||
return status.state;
|
||||
}
|
||||
|
||||
case COMMAND:
|
||||
// Commands initiated by control() are simply echoed back by the unit, but
|
||||
// don't guarantee that the unit's internal state has been transitioned,
|
||||
// nor that the motor started moving. A subsequent status request may
|
||||
// still return the previous state. Discard these messages, don't use them
|
||||
// to drive the Cover state machine.
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown message type, drain the remaining amount of bytes specified in
|
||||
// the header.
|
||||
ESP_LOGE(TAG, "Reading remaining %d payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type);
|
||||
break;
|
||||
}
|
||||
|
||||
// Drain any unhandled payload bytes described by the message header, if any.
|
||||
this->drain_rx_(hdr.payload_size());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Send a message to the unit requesting the gate's status.
|
||||
void Tormatic::request_gate_status_() {
|
||||
ESP_LOGV(TAG, "Requesting gate status");
|
||||
StatusRequest req(GATE);
|
||||
this->send_message_(STATUS, req);
|
||||
}
|
||||
|
||||
// Send a message to the unit issuing a command.
|
||||
void Tormatic::send_gate_command_(GateStatus s) {
|
||||
ESP_LOGI(TAG, "Sending gate command %s", gate_status_to_str(s));
|
||||
CommandRequestReply req(s);
|
||||
this->send_message_(COMMAND, req);
|
||||
}
|
||||
|
||||
template<typename T> void Tormatic::send_message_(MessageType t, T req) {
|
||||
MessageHeader hdr(t, ++this->seq_tx_, sizeof(req));
|
||||
|
||||
auto out = serialize(hdr);
|
||||
auto reqv = serialize(req);
|
||||
out.insert(out.end(), reqv.begin(), reqv.end());
|
||||
|
||||
this->write_array(out);
|
||||
}
|
||||
|
||||
template<typename T> optional<T> Tormatic::read_data_() {
|
||||
T obj;
|
||||
uint32_t start = millis();
|
||||
|
||||
auto ok = this->read_array((uint8_t *) &obj, sizeof(obj));
|
||||
if (!ok) {
|
||||
// Couldn't read object successfully, timeout?
|
||||
return {};
|
||||
}
|
||||
obj.byteswap();
|
||||
|
||||
ESP_LOGV(TAG, "Read %s in %d ms", obj.print().c_str(), millis() - start);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Drain up to n amount of bytes from the uart rx buffer.
|
||||
void Tormatic::drain_rx_(uint16_t n) {
|
||||
uint8_t data;
|
||||
uint16_t count = 0;
|
||||
while (this->available()) {
|
||||
this->read_byte(&data);
|
||||
count++;
|
||||
|
||||
if (n > 0 && count >= n) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tormatic
|
||||
} // namespace esphome
|
60
esphome/components/tormatic/tormatic_cover.h
Normal file
60
esphome/components/tormatic/tormatic_cover.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
#include "tormatic_protocol.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tormatic {
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; };
|
||||
|
||||
void set_open_duration(uint32_t duration) { this->open_duration_ = duration; }
|
||||
void set_close_duration(uint32_t duration) { this->close_duration_ = duration; }
|
||||
|
||||
void publish_state(bool save = true, uint32_t ratelimit = 0);
|
||||
|
||||
cover::CoverTraits get_traits() override;
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
|
||||
void recalibrate_duration_(GateStatus s);
|
||||
void recompute_position_();
|
||||
void control_position_(float target);
|
||||
void stop_at_target_();
|
||||
|
||||
template<typename T> void send_message_(MessageType t, T r);
|
||||
template<typename T> optional<T> read_data_();
|
||||
void drain_rx_(uint16_t n = 0);
|
||||
|
||||
void request_gate_status_();
|
||||
optional<GateStatus> read_gate_status_();
|
||||
|
||||
void send_gate_command_(GateStatus s);
|
||||
void handle_gate_status_(GateStatus s);
|
||||
|
||||
uint32_t seq_tx_{0};
|
||||
|
||||
GateStatus current_status_{PAUSED};
|
||||
|
||||
uint32_t open_duration_{0};
|
||||
uint32_t close_duration_{0};
|
||||
uint32_t last_publish_time_{0};
|
||||
uint32_t last_recompute_time_{0};
|
||||
uint32_t direction_start_time_{0};
|
||||
GateStatus next_command_{OPENED};
|
||||
optional<float> target_position_{};
|
||||
};
|
||||
|
||||
} // namespace tormatic
|
||||
} // namespace esphome
|
211
esphome/components/tormatic/tormatic_protocol.h
Normal file
211
esphome/components/tormatic/tormatic_protocol.h
Normal file
|
@ -0,0 +1,211 @@
|
|||
#pragma once
|
||||
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
/**
|
||||
* This file implements the UART protocol spoken over the on-board Micro-USB
|
||||
* (Type B) connector of Tormatic and Novoferm gates manufactured as of 2016.
|
||||
* All communication is initiated by the component. The unit doesn't send data
|
||||
* without being asked first.
|
||||
*
|
||||
* There are two main message types: status requests and commands.
|
||||
*
|
||||
* Querying the gate's status:
|
||||
*
|
||||
* | sequence | length | type | payload |
|
||||
* | 0xF3 0xCB | 0x00 0x00 0x00 0x06 | 0x01 0x04 | 0x00 0x0A 0x00 0x01 |
|
||||
* | 0xF3 0xCB | 0x00 0x00 0x00 0x05 | 0x01 0x04 | 0x02 0x03 0x00 |
|
||||
*
|
||||
* This request asks for the gate status (0x0A); the only other value observed
|
||||
* in the request was 0x0B, but replies were always zero. Presumably this
|
||||
* queries another sensor on the unit like a safety breaker, but this is not
|
||||
* relevant for an esphome cover component.
|
||||
*
|
||||
* The second byte of the reply is set to 0x03 when the gate is in fully open
|
||||
* position. Other valid values for the second byte are: (0x0) Paused, (0x1)
|
||||
* Closed, (0x2) Ventilating, (0x3) Opened, (0x4) Opening, (0x5) Closing. The
|
||||
* meaning of the other bytes is currently unknown and ignored by the component.
|
||||
*
|
||||
* Controlling the gate:
|
||||
*
|
||||
* | sequence | length | type | payload |
|
||||
* | 0x40 0xFF | 0x00 0x00 0x00 0x06 | 0x01 0x06 | 0x00 0x0A 0x00 0x03 |
|
||||
* | 0x40 0xFF | 0x00 0x00 0x00 0x06 | 0x01 0x06 | 0x00 0x0A 0x00 0x03 |
|
||||
*
|
||||
* The unit acks any commands by echoing back the message in full. However,
|
||||
* this does _not_ mean the gate has started closing. The component only
|
||||
* considers status replies as authoritative and simply fires off commands,
|
||||
* ignoring the echoed messages.
|
||||
*
|
||||
* The payload structure is as follows: [0x00, 0x0A] (gate), followed by
|
||||
* one of the states normally carried in status replies: (0x0) Pause, (0x1)
|
||||
* Close, (0x2) Ventilate (open ~20%), (0x3) Open/high-torque reverse. The
|
||||
* protocol implementation in this file simply reuses the GateStatus enum
|
||||
* for this purpose.
|
||||
*/
|
||||
|
||||
namespace esphome {
|
||||
namespace tormatic {
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
// MessageType is the type of message that follows the MessageHeader.
|
||||
enum MessageType : uint16_t {
|
||||
STATUS = 0x0104,
|
||||
COMMAND = 0x0106,
|
||||
};
|
||||
|
||||
inline const char *message_type_to_str(MessageType t) {
|
||||
switch (t) {
|
||||
case STATUS:
|
||||
return "Status";
|
||||
case COMMAND:
|
||||
return "Command";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// MessageHeader appears at the start of every message, both requests and replies.
|
||||
struct MessageHeader {
|
||||
uint16_t seq;
|
||||
uint32_t len;
|
||||
MessageType type;
|
||||
|
||||
MessageHeader() = default;
|
||||
MessageHeader(MessageType type, uint16_t seq, uint32_t payload_size) {
|
||||
this->type = type;
|
||||
this->seq = seq;
|
||||
// len includes the length of the type field. It was
|
||||
// included in MessageHeader to avoid having to parse
|
||||
// it as part of the payload.
|
||||
this->len = payload_size + sizeof(this->type);
|
||||
}
|
||||
|
||||
std::string print() {
|
||||
return str_sprintf("MessageHeader: seq %d, len %d, type %s", this->seq, this->len, message_type_to_str(this->type));
|
||||
}
|
||||
|
||||
void byteswap() {
|
||||
this->len = convert_big_endian(this->len);
|
||||
this->seq = convert_big_endian(this->seq);
|
||||
this->type = convert_big_endian(this->type);
|
||||
}
|
||||
|
||||
// payload_size returns the amount of payload bytes to be read from the uart
|
||||
// buffer after reading the header.
|
||||
uint32_t payload_size() { return this->len - sizeof(this->type); }
|
||||
} __attribute__((packed));
|
||||
|
||||
// StatusType denotes which 'page' of information needs to be retrieved.
|
||||
// On my Novoferm 423, only the GATE status type returns values, Unknown
|
||||
// only contains zeroes.
|
||||
enum StatusType : uint16_t {
|
||||
GATE = 0x0A,
|
||||
UNKNOWN = 0x0B,
|
||||
};
|
||||
|
||||
// GateStatus defines the current state of the gate, received in a StatusReply
|
||||
// and sent in a Command.
|
||||
enum GateStatus : uint8_t {
|
||||
PAUSED,
|
||||
CLOSED,
|
||||
VENTILATING,
|
||||
OPENED,
|
||||
OPENING,
|
||||
CLOSING,
|
||||
};
|
||||
|
||||
inline CoverOperation gate_status_to_cover_operation(GateStatus s) {
|
||||
switch (s) {
|
||||
case OPENING:
|
||||
return COVER_OPERATION_OPENING;
|
||||
case CLOSING:
|
||||
return COVER_OPERATION_CLOSING;
|
||||
case OPENED:
|
||||
case CLOSED:
|
||||
case PAUSED:
|
||||
case VENTILATING:
|
||||
return COVER_OPERATION_IDLE;
|
||||
}
|
||||
return COVER_OPERATION_IDLE;
|
||||
}
|
||||
|
||||
inline const char *gate_status_to_str(GateStatus s) {
|
||||
switch (s) {
|
||||
case PAUSED:
|
||||
return "Paused";
|
||||
case CLOSED:
|
||||
return "Closed";
|
||||
case VENTILATING:
|
||||
return "Ventilating";
|
||||
case OPENED:
|
||||
return "Opened";
|
||||
case OPENING:
|
||||
return "Opening";
|
||||
case CLOSING:
|
||||
return "Closing";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// A StatusRequest is sent to request the gate's current status.
|
||||
struct StatusRequest {
|
||||
StatusType type;
|
||||
uint16_t trailer = 0x1;
|
||||
|
||||
StatusRequest() = default;
|
||||
StatusRequest(StatusType type) { this->type = type; }
|
||||
|
||||
void byteswap() {
|
||||
this->type = convert_big_endian(this->type);
|
||||
this->trailer = convert_big_endian(this->trailer);
|
||||
}
|
||||
} __attribute__((packed));
|
||||
|
||||
// StatusReply is received from the unit in response to a StatusRequest.
|
||||
struct StatusReply {
|
||||
uint8_t ack = 0x2;
|
||||
GateStatus state;
|
||||
uint8_t trailer = 0x0;
|
||||
|
||||
std::string print() { return str_sprintf("StatusReply: state %s", gate_status_to_str(this->state)); }
|
||||
|
||||
void byteswap(){};
|
||||
} __attribute__((packed));
|
||||
|
||||
// Serialize the given object to a new byte vector.
|
||||
// Invokes the object's byteswap() method.
|
||||
template<typename T> std::vector<uint8_t> serialize(T obj) {
|
||||
obj.byteswap();
|
||||
|
||||
std::vector<uint8_t> out(sizeof(T));
|
||||
memcpy(out.data(), &obj, sizeof(T));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Command tells the gate to start or stop moving.
|
||||
// It is echoed back by the unit on success.
|
||||
struct CommandRequestReply {
|
||||
// The part of the unit to control. For now only the gate is supported.
|
||||
StatusType type = GATE;
|
||||
uint8_t pad = 0x0;
|
||||
// The desired state:
|
||||
// PAUSED = stop
|
||||
// VENTILATING = move to ~20% open
|
||||
// CLOSED = close
|
||||
// OPENED = open/high-torque reverse when closing
|
||||
GateStatus state;
|
||||
|
||||
CommandRequestReply() = default;
|
||||
CommandRequestReply(GateStatus state) { this->state = state; }
|
||||
|
||||
std::string print() { return str_sprintf("CommandRequestReply: state %s", gate_status_to_str(this->state)); }
|
||||
|
||||
void byteswap() { this->type = convert_big_endian(this->type); }
|
||||
} __attribute__((packed));
|
||||
|
||||
} // namespace tormatic
|
||||
} // namespace esphome
|
13
tests/components/tormatic/common.yaml
Normal file
13
tests/components/tormatic/common.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
uart:
|
||||
- id: uart_tormatic
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 9600
|
||||
|
||||
cover:
|
||||
- platform: tormatic
|
||||
uart_id: uart_tormatic
|
||||
id: tormatic_garage_door
|
||||
name: Tormatic Garage Door
|
||||
open_duration: 15s
|
||||
close_duration: 22s
|
5
tests/components/tormatic/test.esp32-ard.yaml
Normal file
5
tests/components/tormatic/test.esp32-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/tormatic/test.esp32-c3-ard.yaml
Normal file
5
tests/components/tormatic/test.esp32-c3-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/tormatic/test.esp32-c3-idf.yaml
Normal file
5
tests/components/tormatic/test.esp32-c3-idf.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/tormatic/test.esp32-idf.yaml
Normal file
5
tests/components/tormatic/test.esp32-idf.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/tormatic/test.esp8266-ard.yaml
Normal file
5
tests/components/tormatic/test.esp8266-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
5
tests/components/tormatic/test.rp2040-ard.yaml
Normal file
5
tests/components/tormatic/test.rp2040-ard.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
<<: !include common.yaml
|
Loading…
Add table
Reference in a new issue