#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 { public: virtual void encode(remote_base::RemoteTransmitData* dst, const ProtocolData& data) override { /* * 1. Build a frame. */ uint8_t frame[] = { 0xA7, // Encryption key. static_cast(data.command << 4), // 4 lsb will be checksum. static_cast(data.rolling_code >> 8), static_cast(data.rolling_code), static_cast(data.remote_id >> 16), static_cast(data.remote_id >> 8), static_cast(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 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(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(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