mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Cover component for Tormatic and Novoferm garage doors (#5933)
This commit is contained in:
		| @@ -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 | ||||
		Reference in New Issue
	
	Block a user