diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f6cb8ad --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/blinds.yaml b/blinds.yaml index 2ad0777..d8e4e45 100644 --- a/blinds.yaml +++ b/blinds.yaml @@ -5,11 +5,7 @@ esphome: project: name: dasfoo.blinds - version: "2.0" - - includes: - - blinds/somfy.h - - blinds/cc1101.h + version: "3.0" libraries: - SPI @@ -18,52 +14,56 @@ esphome: packages: device_base: !include templates/esp32-poe.yaml -globals: - - id: somfy_remote_id_base - type: int - # Can be retrieved from MQTT server. Take the key with lowest integer value - # from /rolling_code/ path. - initial_value: !secret somfy_remote_id_base - - id: somfy_rolling_codes - type: 'int[13]' - initial_value: '{0,0,0,0,0,0,0,0,0,0,0,0,0}' - restore_value: yes +external_components: + # Path is relative to the main YAML file, not this device_base template! + - source: components cover: - - platform: custom - lambda: |- - auto rfPin = 4; - std::vector blinds; + - name: "Office window" + remote_id_offset: 0 + <<: &somfy_cover + platform: somfy + rf_pin: 4 + # Can be retrieved from MQTT server. Take the key with lowest integer value + # from /$board/rolling_code/ path. + remote_id_base: !secret somfy_remote_id_base - for (int i = 0; i < 13; ++i) { - auto cover = new SomfyRTS( - rfPin, - id(somfy_remote_id_base) + i, - id(mqtt_homeassistant), - &(id(somfy_rolling_codes)[i])); - blinds.push_back(cover); - App.register_component(cover); - } - - // SomfyRTS ctor will set correct pin modes, now we can initialize cc1101. - cc1101_Init(); - - return blinds; - - covers: - - name: "Office window" - - name: "Bedroom" - - name: "Living Room side" - - name: "Wardrobe" - - name: "Child West door" - - name: "Child West window" - - name: "Hallway Stairs window" - - name: "Kitchen window" - - name: "Kitchen door" - - name: "Living Room door" - - name: "Living Room window" - - name: "Child East window" - - name: "Child East door" + - name: "Bedroom" + remote_id_offset: 1 + <<: *somfy_cover + - name: "Living Room side" + remote_id_offset: 2 + <<: *somfy_cover + - name: "Wardrobe" + remote_id_offset: 3 + <<: *somfy_cover + - name: "Child West door" + remote_id_offset: 4 + <<: *somfy_cover + - name: "Child West window" + remote_id_offset: 5 + <<: *somfy_cover + - name: "Hallway Stairs window" + remote_id_offset: 6 + <<: *somfy_cover + - name: "Kitchen window" + remote_id_offset: 7 + <<: *somfy_cover + - name: "Kitchen door" + remote_id_offset: 8 + <<: *somfy_cover + - name: "Living Room door" + remote_id_offset: 9 + <<: *somfy_cover + - name: "Living Room window" + remote_id_offset: 10 + <<: *somfy_cover + - name: "Child East window" + remote_id_offset: 11 + <<: *somfy_cover + - name: "Child East door" + remote_id_offset: 12 + <<: *somfy_cover - platform: template name: "Wintergarten" diff --git a/blinds/cc1101.h b/blinds/cc1101.h deleted file mode 100644 index 648a13e..0000000 --- a/blinds/cc1101.h +++ /dev/null @@ -1,10 +0,0 @@ -#include - -void cc1101_Init() { - 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(); -} diff --git a/blinds/somfy.h b/blinds/somfy.h deleted file mode 100644 index 6a4dfdf..0000000 --- a/blinds/somfy.h +++ /dev/null @@ -1,170 +0,0 @@ -#include "esphome.h" - -namespace { - static const char* const TAG = __FILE__; - - 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; -} - -class SomfyRTS : public Component, public Cover { - public: - SomfyRTS(int rfPin, int remoteId, esphome::mqtt::MQTTClientComponent* mqtt, int *rollingCode) { - this->rfPin = rfPin; - this->remoteId = remoteId; - this->mqtt = mqtt; - this->mqttTopicPrefix = mqtt->get_topic_prefix() + "/rolling_code/"; - this->rollingCode = rollingCode; - } - - void setup() override { - pinMode(rfPin, OUTPUT); - digitalWrite(rfPin, LOW); - - if (*rollingCode == 0) { - // 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->subscribe(mqttTopicPrefix + std::to_string(remoteId), - [=](const std::string &topic, const std::string &payload) { - if (*rollingCode == 0) { - ESP_LOGI(TAG, "Received rolling code for remote #%d from MQTT: %s", - remoteId, payload.c_str()); - *this->rollingCode = atoi(payload.c_str()); - } - }, /*qos=*/1 /* (at least once) */); - } else { - ESP_LOGI(TAG, "Restored rolling code for remote #%d from flash: %d", - remoteId, *rollingCode); - } - } - - CoverTraits get_traits() override { - auto traits = 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 control(const CoverCall &call) override { - 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); - } - } - - private: - int rfPin; - int remoteId; - int *rollingCode; - esphome::mqtt::MQTTClientComponent* mqtt; - std::string mqttTopicPrefix; - - /* 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) { - if (*rollingCode == 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", - remoteId, command); - return; - } - - ESP_LOGI(TAG, "Remote #%d sending command %d with rolling code %d", - remoteId, command, *rollingCode); - - uint8_t frame[] = { - 0xA7, // Encryption key. - static_cast(command << 4), // 4 lsb will be checksum. - - static_cast(*rollingCode >> 8), - static_cast(*rollingCode), - - static_cast(remoteId >> 16), - static_cast(remoteId >> 8), - static_cast(remoteId), - }; - - ++*rollingCode; - - // 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(rfPin, HIGH); - delayMicroseconds(9415); - digitalWrite(rfPin, 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(rfPin, HIGH); - delayMicroseconds(4*RF_SYMBOL); - digitalWrite(rfPin, LOW); - delayMicroseconds(4*RF_SYMBOL); - } - - // Software sync. - digitalWrite(rfPin, HIGH); - delayMicroseconds(4550); - digitalWrite(rfPin, 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(rfPin, LOW); - delayMicroseconds(RF_SYMBOL); - digitalWrite(rfPin, HIGH); - delayMicroseconds(RF_SYMBOL); - } else { - digitalWrite(rfPin, HIGH); - delayMicroseconds(RF_SYMBOL); - digitalWrite(rfPin, LOW); - delayMicroseconds(RF_SYMBOL); - } - } - } - - digitalWrite(rfPin, LOW); - // Inter-frame silence. - delayMicroseconds(30415); - } - - // 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->publish(mqttTopicPrefix + std::to_string(remoteId), - std::to_string(*rollingCode), /*qos=*/1, /*retain=*/true); - } -}; diff --git a/components/somfy/__init__.py b/components/somfy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/somfy/cc1101.h b/components/somfy/cc1101.h new file mode 100644 index 0000000..8d8c410 --- /dev/null +++ b/components/somfy/cc1101.h @@ -0,0 +1,20 @@ +#include + +#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 diff --git a/components/somfy/cover.py b/components/somfy/cover.py new file mode 100644 index 0000000..efc7acb --- /dev/null +++ b/components/somfy/cover.py @@ -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] + )) diff --git a/components/somfy/somfy.cpp b/components/somfy/somfy.cpp new file mode 100644 index 0000000..63f1ef7 --- /dev/null +++ b/components/somfy/somfy.cpp @@ -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(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(command << 4), // 4 lsb will be checksum. + static_cast(rolling_code_ >> 8), + static_cast(rolling_code_), + static_cast(remote_id_ >> 16), + static_cast(remote_id_ >> 8), + static_cast(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 diff --git a/components/somfy/somfy.h b/components/somfy/somfy.h new file mode 100644 index 0000000..93b1cdf --- /dev/null +++ b/components/somfy/somfy.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#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