Rewrite blinds in external component

This commit is contained in:
2025-05-03 19:09:54 +02:00
parent aef0f374b9
commit 27e8bf4781
9 changed files with 329 additions and 228 deletions
View File
+20
View File
@@ -0,0 +1,20 @@
#include <ELECHOUSE_CC1101_SRC_DRV.h>
#ifndef cc1101_h
#define cc1101_h
static bool cc1101_initialized = false;
static void cc1101_Init() {
if (!cc1101_initialized) {
ELECHOUSE_cc1101.addSpiPin(14, 16, 15, 13, 0);
ELECHOUSE_cc1101.setModul(0);
ELECHOUSE_cc1101.Init();
ELECHOUSE_cc1101.setPA(7);
ELECHOUSE_cc1101.setMHZ(433.34);
ELECHOUSE_cc1101.SetTx();
}
cc1101_initialized = true;
}
#endif
+33
View File
@@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import cover, mqtt
from esphome.const import CONF_ID
DEPENDENCIES = ["mqtt"]
AUTO_LOAD = ["mqtt"]
somfy_ns = cg.esphome_ns.namespace("somfy")
SomfyRTSCover = somfy_ns.class_("SomfyRTSCover", cover.Cover, cg.Component)
CONF_RF_PIN = "rf_pin"
CONF_REMOTE_ID_BASE = "remote_id_base"
CONF_REMOTE_ID_OFFSET = "remote_id_offset"
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SomfyRTSCover),
cv.Required(CONF_RF_PIN): cv.uint8_t,
cv.Required(CONF_REMOTE_ID_BASE): cv.uint32_t,
cv.Required(CONF_REMOTE_ID_OFFSET): cv.uint32_t,
}
).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)
cg.add(var.set_rf_pin(config[CONF_RF_PIN]))
cg.add(var.set_remote_id(
config[CONF_REMOTE_ID_BASE] + config[CONF_REMOTE_ID_OFFSET]
))
+185
View File
@@ -0,0 +1,185 @@
#include "somfy.h"
#include "cc1101.h"
#include "esphome/components/mqtt/mqtt_client.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace somfy {
static const char *const TAG = "somfy";
static const uint8_t CMD_STOP = 0x01;
static const uint8_t CMD_UP = 0x02;
static const uint8_t CMD_DOWN = 0x04;
static const uint8_t CMD_PROG = 0x08;
static const int RF_SYMBOL = 640;
void SomfyRTSCover::dump_config() {
LOG_COVER("", "Somfy RTS Cover", this);
ESP_LOGCONFIG(TAG, " RF Pin: %d", this->rf_pin_);
ESP_LOGCONFIG(TAG, " Remote ID: %d", this->remote_id_);
if (this->rolling_code_) {
ESP_LOGCONFIG(TAG, " Rolling Code: %d", this->rolling_code_);
} else {
ESP_LOGCONFIG(TAG, " Rolling Code: N/A");
}
}
cover::CoverTraits SomfyRTSCover::get_traits() {
auto traits = cover::CoverTraits();
traits.set_is_assumed_state(true);
traits.set_supports_position(false);
traits.set_supports_tilt(false);
traits.set_supports_toggle(false);
return traits;
}
void SomfyRTSCover::setup() {
pinMode(rf_pin_, OUTPUT);
digitalWrite(rf_pin_, LOW);
cc1101_Init();
rolling_code_pref_ =
global_preferences->make_preference<int>(this->get_object_id_hash());
if (mqtt::global_mqtt_client != nullptr) {
this->mqtt_topic_prefix_ =
mqtt::global_mqtt_client->get_topic_prefix() + "/rolling_code/";
}
if (this->rolling_code_pref_.load(&this->rolling_code_)) {
ESP_LOGI(TAG, "Restored rolling code for remote #%d from flash: %d",
remote_id_, rolling_code_);
} else if (mqtt::global_mqtt_client != nullptr) {
// Either a new version was flashed, flash was corrupt, or this firmware
// was flashed to a different ESP chip. Either way, try to restore from
// MQTT.
mqtt::global_mqtt_client->subscribe(
mqtt_topic_prefix_ + std::to_string(remote_id_),
[this](const std::string &topic, const std::string &payload) {
if (rolling_code_ == 0) {
ESP_LOGI(TAG, "Received rolling code for remote #%d from MQTT: %s",
remote_id_, payload.c_str());
this->rolling_code_ = atoi(payload.c_str());
this->rolling_code_pref_.save(&this->rolling_code_);
}
},
/*qos=*/1 /* (at least once) */);
} else {
ESP_LOGE(TAG, "No rolling code in flash, and MQTT client unavailable");
rolling_code_ = 0;
}
}
void SomfyRTSCover::control(const cover::CoverCall &call) {
if (call.get_position().has_value()) {
float position = *call.get_position();
send(position > 0.5 ? CMD_UP : CMD_DOWN);
this->position = position;
this->publish_state();
} else if (call.get_stop()) {
send(CMD_STOP);
}
}
void SomfyRTSCover::send(uint8_t command) {
if (rolling_code_ == 0) {
ESP_LOGE(TAG,
"Remote #%d was requested to run command %d, but has not "
"yet received its rolling code. Refusing to proceed, to "
"avoid crippling MQTT rolling code storage",
remote_id_, command);
return;
}
ESP_LOGI(TAG, "Remote #%d sending command %d with rolling code %d",
remote_id_, command, rolling_code_);
uint8_t frame[] = {
0xA7, // Encryption key.
static_cast<uint8_t>(command << 4), // 4 lsb will be checksum.
static_cast<uint8_t>(rolling_code_ >> 8),
static_cast<uint8_t>(rolling_code_),
static_cast<uint8_t>(remote_id_ >> 16),
static_cast<uint8_t>(remote_id_ >> 8),
static_cast<uint8_t>(remote_id_),
};
++rolling_code_;
// Checksum calculation: a XOR of all the nibbles.
uint8_t checksum = 0;
for (uint8_t i = 0; i < 7; i++) {
checksum = checksum ^ frame[i] ^ (frame[i] >> 4);
}
checksum &= 0b1111; // We keep the last 4 bits only.
frame[1] |= checksum;
// Obfuscation: a XOR of all the bytes.
for (uint8_t i = 1; i < 7; i++) {
frame[i] ^= frame[i - 1];
}
// Wake-up pulse & silence.
digitalWrite(rf_pin_, HIGH);
delayMicroseconds(9415);
digitalWrite(rf_pin_, LOW);
delayMicroseconds(89565);
for (int repeat = 0; repeat < 3; ++repeat) {
// Hardware sync.
uint8_t sync = repeat == 0 ? 2 : 7;
for (uint8_t i = 0; i < sync; i++) {
digitalWrite(rf_pin_, HIGH);
delayMicroseconds(4 * RF_SYMBOL);
digitalWrite(rf_pin_, LOW);
delayMicroseconds(4 * RF_SYMBOL);
}
// Software sync.
digitalWrite(rf_pin_, HIGH);
delayMicroseconds(4550);
digitalWrite(rf_pin_, LOW);
delayMicroseconds(RF_SYMBOL);
// Data: bits are sent one by one.
for (uint8_t octet = 0; octet < 7; ++octet) {
// Starting with MSB.
for (signed char bit = 7; bit >= 0; --bit) {
uint8_t value = (frame[octet] >> bit) & 1;
if (value == 1) {
digitalWrite(rf_pin_, LOW);
delayMicroseconds(RF_SYMBOL);
digitalWrite(rf_pin_, HIGH);
delayMicroseconds(RF_SYMBOL);
} else {
digitalWrite(rf_pin_, HIGH);
delayMicroseconds(RF_SYMBOL);
digitalWrite(rf_pin_, LOW);
delayMicroseconds(RF_SYMBOL);
}
}
}
digitalWrite(rf_pin_, LOW);
// Inter-frame silence.
delayMicroseconds(30415);
}
this->rolling_code_pref_.save(&rolling_code_);
if (mqtt::global_mqtt_client != nullptr) {
// Publish the new rolling code at the end to ensure fast reaction time
// when the component is called. This method may be synchronous and
// introduces an unpredictable delay.
mqtt::global_mqtt_client->publish(
mqtt_topic_prefix_ + std::to_string(remote_id_),
std::to_string(rolling_code_),
/*qos=*/1, /*retain=*/true);
}
}
} // namespace somfy
} // namespace esphome
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <string>
#include "esphome/components/cover/cover.h"
#include "esphome/components/mqtt/mqtt_component.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace somfy {
class SomfyRTSCover : public cover::Cover, public Component {
public:
void set_rf_pin(uint8_t rf_pin) { this->rf_pin_ = rf_pin; }
void set_remote_id(uint32_t remote_id) { this->remote_id_ = remote_id; }
cover::CoverTraits get_traits() override;
void setup() override;
void dump_config() override;
void control(const cover::CoverCall &call) override;
protected:
/* Wake up the blinds motor controller and send the command.
* Overall, with repeats and syncs, takes about 510ms.
*
* mqtt.publish is synchronous unless idf_send_async is set, and may
* introduce an unknown delay into the method execution timeline.
*/
void send(uint8_t command);
uint8_t rf_pin_;
uint32_t remote_id_;
uint32_t rolling_code_;
std::string mqtt_topic_prefix_;
ESPPreferenceObject rolling_code_pref_;
};
} // namespace somfy
} // namespace esphome