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
+1
View File
@@ -0,0 +1 @@
BasedOnStyle: Google
+36 -36
View File
@@ -5,11 +5,7 @@ esphome:
project: project:
name: dasfoo.blinds name: dasfoo.blinds
version: "2.0" version: "3.0"
includes:
- blinds/somfy.h
- blinds/cc1101.h
libraries: libraries:
- SPI - SPI
@@ -18,52 +14,56 @@ esphome:
packages: packages:
device_base: !include templates/esp32-poe.yaml device_base: !include templates/esp32-poe.yaml
globals: external_components:
- id: somfy_remote_id_base # Path is relative to the main YAML file, not this device_base template!
type: int - source: components
# 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
cover: cover:
- platform: custom
lambda: |-
auto rfPin = 4;
std::vector<esphome::cover::Cover*> blinds;
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: "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
- name: "Bedroom" - name: "Bedroom"
remote_id_offset: 1
<<: *somfy_cover
- name: "Living Room side" - name: "Living Room side"
remote_id_offset: 2
<<: *somfy_cover
- name: "Wardrobe" - name: "Wardrobe"
remote_id_offset: 3
<<: *somfy_cover
- name: "Child West door" - name: "Child West door"
remote_id_offset: 4
<<: *somfy_cover
- name: "Child West window" - name: "Child West window"
remote_id_offset: 5
<<: *somfy_cover
- name: "Hallway Stairs window" - name: "Hallway Stairs window"
remote_id_offset: 6
<<: *somfy_cover
- name: "Kitchen window" - name: "Kitchen window"
remote_id_offset: 7
<<: *somfy_cover
- name: "Kitchen door" - name: "Kitchen door"
remote_id_offset: 8
<<: *somfy_cover
- name: "Living Room door" - name: "Living Room door"
remote_id_offset: 9
<<: *somfy_cover
- name: "Living Room window" - name: "Living Room window"
remote_id_offset: 10
<<: *somfy_cover
- name: "Child East window" - name: "Child East window"
remote_id_offset: 11
<<: *somfy_cover
- name: "Child East door" - name: "Child East door"
remote_id_offset: 12
<<: *somfy_cover
- platform: template - platform: template
name: "Wintergarten" name: "Wintergarten"
-10
View File
@@ -1,10 +0,0 @@
#include <ELECHOUSE_CC1101_SRC_DRV.h>
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();
}
-170
View File
@@ -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<uint8_t>(command << 4), // 4 lsb will be checksum.
static_cast<uint8_t>(*rollingCode >> 8),
static_cast<uint8_t>(*rollingCode),
static_cast<uint8_t>(remoteId >> 16),
static_cast<uint8_t>(remoteId >> 8),
static_cast<uint8_t>(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);
}
};
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