Files
esphome/components/somfy/somfy.cpp
T
2026-07-12 12:56:02 +00:00

223 lines
6.5 KiB
C++

#include "somfy.h"
#include "esphome/components/mqtt/mqtt_client.h"
#include "esphome/components/remote_transmitter/remote_transmitter.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;
namespace {
static const int RF_SYMBOL = 640;
struct SomfyRTSProtocolData {
uint8_t command;
uint32_t rolling_code;
uint32_t remote_id;
void dump() const {
ESP_LOGD(TAG, "Somfy RTS command:");
ESP_LOGD(TAG, " Remote ID: %d", remote_id);
ESP_LOGD(TAG, " Rolling Code: %d", rolling_code);
ESP_LOGD(TAG, " Command: %d", command);
}
};
class SomfyRTSProtocol
: public remote_base::RemoteProtocol<SomfyRTSProtocolData> {
public:
virtual void encode(remote_base::RemoteTransmitData* dst,
const ProtocolData& data) override {
/*
* 1. Build a frame.
*/
uint8_t frame[] = {
0xA7, // Encryption key.
static_cast<uint8_t>(data.command << 4), // 4 lsb will be checksum.
static_cast<uint8_t>(data.rolling_code >> 8),
static_cast<uint8_t>(data.rolling_code),
static_cast<uint8_t>(data.remote_id >> 16),
static_cast<uint8_t>(data.remote_id >> 8),
static_cast<uint8_t>(data.remote_id),
};
// 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];
}
/*
* 2. Encode the frame.
*/
dst->reset();
dst->reserve(400);
dst->set_carrier_frequency(433340);
// Wake-up pulse & silence.
dst->item(9415, 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++) {
dst->item(4 * RF_SYMBOL, 4 * RF_SYMBOL);
}
// Software sync.
dst->item(4550, 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) {
dst->space(RF_SYMBOL);
dst->mark(RF_SYMBOL);
} else {
dst->item(RF_SYMBOL, RF_SYMBOL);
}
}
}
// Inter-frame silence.
dst->space(30415);
}
}
virtual void dump(const ProtocolData& data) override { data.dump(); }
virtual optional<ProtocolData> decode(
remote_base::RemoteReceiveData src) override {
// Not implemented.
return nullopt;
}
};
} // namespace
void SomfyRTSCover::dump_config() {
LOG_COVER("", "Somfy RTS Cover", this);
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");
}
ESP_LOGCONFIG(TAG, " Rolling Code MQTT prefix: %s",
this->mqtt_topic_prefix_.c_str());
}
cover::CoverTraits SomfyRTSCover::get_traits() {
auto traits = cover::CoverTraits();
traits.set_is_assumed_state(true);
traits.set_supports_stop(true);
traits.set_supports_position(false);
traits.set_supports_tilt(false);
traits.set_supports_toggle(false);
return traits;
}
void SomfyRTSCover::setup() {
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 {
this->rolling_code_ = 0;
this->mark_failed(LOG_STR("Missing rolling_code"));
ESP_LOGE(TAG, "No rolling code in flash, and MQTT client unavailable");
}
}
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 (transmitter_ == nullptr) {
ESP_LOGE(TAG, "RF code transmitter is not set");
return;
}
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;
}
SomfyRTSProtocolData data{
.command = command,
.rolling_code = rolling_code_,
.remote_id = remote_id_,
};
data.dump();
transmitter_->transmit<SomfyRTSProtocol>(data);
++rolling_code_;
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