Skip to content

Commit 7025b5d

Browse files
committed
Port Raex blinds to a proper ESPHome Component
1 parent 61ca6fb commit 7025b5d

8 files changed

Lines changed: 332 additions & 50 deletions

File tree

‎home-assistant/martin-pl/blinds.config.yaml‎

Lines changed: 0 additions & 37 deletions
This file was deleted.

‎home-assistant/martin-pl/esphome/.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88
**/src/
99
**/platformio.ini
1010
/secrets.yaml
11-
*/*
11+
**/__pycache__/**
12+
archive/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.const import CONF_ID
4+
5+
CONF_RAEX_PARENT = "raex_parent"
6+
CONF_REMOTE_ID = "remote_id"
7+
CONF_CHANNEL_ID = "channel_id"
8+
AUTO_LOAD = ["cover", "button"]
9+
DEPENDENCIES = ["api"]
10+
11+
# Define namespace and component
12+
raex_blind_tx_ns = cg.esphome_ns.namespace('raex_blind_tx')
13+
RaexBlindTX = raex_blind_tx_ns.class_('RaexBlindTX', cg.Component)
14+
15+
CONFIG_SCHEMA = cv.Schema({
16+
cv.GenerateID(): cv.declare_id(RaexBlindTX),
17+
}).extend(cv.COMPONENT_SCHEMA)
18+
19+
async def to_code(config):
20+
var = cg.new_Pvariable(config[CONF_ID])
21+
await cg.register_component(var, config)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.components import button
4+
from esphome.const import (
5+
CONF_ID,
6+
CONF_ENTITY_CATEGORY,
7+
ENTITY_CATEGORY_CONFIG,
8+
)
9+
from . import raex_blind_tx_ns, RaexBlindTX, CONF_RAEX_PARENT, CONF_REMOTE_ID, CONF_CHANNEL_ID
10+
11+
DEPENDENCIES = ["raex_blind_tx"]
12+
13+
RaexBlindPairButton = raex_blind_tx_ns.class_("RaexBlindPairButton", button.Button, cg.Component)
14+
15+
CONFIG_SCHEMA = button.button_schema(RaexBlindPairButton).extend({
16+
cv.GenerateID(CONF_RAEX_PARENT): cv.use_id(RaexBlindTX),
17+
cv.Required(CONF_REMOTE_ID): cv.int_range(min=0, max=65535),
18+
cv.Required(CONF_CHANNEL_ID): cv.int_range(min=0, max=255),
19+
cv.Optional(CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG): cv.entity_category,
20+
}).extend(cv.COMPONENT_SCHEMA)
21+
22+
async def to_code(config):
23+
var = cg.new_Pvariable(config[CONF_ID])
24+
await cg.register_component(var, config)
25+
await button.register_button(var, config)
26+
27+
parent = await cg.get_variable(config[CONF_RAEX_PARENT])
28+
cg.add(var.set_parent(parent))
29+
cg.add(var.set_remote_id(config[CONF_REMOTE_ID]))
30+
cg.add(var.set_channel_id(config[CONF_CHANNEL_ID]))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import esphome.codegen as cg
2+
import esphome.config_validation as cv
3+
from esphome.components import cover
4+
from esphome.const import CONF_ID
5+
from . import raex_blind_tx_ns, RaexBlindTX, CONF_RAEX_PARENT, CONF_REMOTE_ID, CONF_CHANNEL_ID
6+
7+
DEPENDENCIES = ["raex_blind_tx"]
8+
9+
RaexBlindCover = raex_blind_tx_ns.class_("RaexBlindCover", cover.Cover, cg.Component)
10+
11+
CONFIG_SCHEMA = cover.cover_schema(RaexBlindCover).extend({
12+
# cv.GenerateID(): cv.declare_id(RaexBlindCover),
13+
cv.GenerateID(CONF_RAEX_PARENT): cv.use_id(RaexBlindTX),
14+
cv.Required(CONF_REMOTE_ID): cv.int_range(min=0, max=65535),
15+
cv.Required(CONF_CHANNEL_ID): cv.int_range(min=0, max=255),
16+
}).extend(cv.COMPONENT_SCHEMA)
17+
18+
async def to_code(config):
19+
var = cg.new_Pvariable(config[CONF_ID])
20+
await cg.register_component(var, config)
21+
await cover.register_cover(var, config)
22+
23+
parent = await cg.get_variable(config[CONF_RAEX_PARENT])
24+
cg.add(var.set_parent(parent))
25+
cg.add(var.set_remote_id(config[CONF_REMOTE_ID]))
26+
cg.add(var.set_channel_id(config[CONF_CHANNEL_ID]))

home-assistant/martin-pl/esphome/raex_blind_tx.h renamed to home-assistant/martin-pl/esphome/custom_components/raex_blind_tx/raex_blind_tx.h

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
#pragma once
2+
13
#include "esphome.h"
4+
#include "esphome/core/component.h"
5+
#include "esphome/core/log.h"
6+
#include "esphome/components/cover/cover.h"
7+
#include "esphome/components/button/button.h"
8+
#include "esphome/components/api/custom_api_device.h"
29

310
// Goals
411
// - Eventual consistency; retry over a longer period of time.
@@ -15,7 +22,10 @@
1522
#define CLOCK_WIDTH 360
1623
#define LOCKOUT_DELAY_MS 200
1724

18-
static const char *TAG = "raex_blind_tx";
25+
namespace esphome {
26+
namespace raex_blind_tx {
27+
28+
static const char *const TAG = "raex_blind_tx";
1929

2030
enum _raex_action {
2131
TX_RAEX_ACTION_UP = 254,
@@ -63,7 +73,7 @@ RaexMessage::RaexMessage(
6373
action_id(action_id) {};
6474

6575

66-
class RaexBlindTransmitComponent : public Component, public CustomAPIDevice {
76+
class RaexBlindTX : public Component, public api::CustomAPIDevice {
6777
private:
6878
std::map<uint32_t, RaexMessage*> pending_messages;
6979
int lockout_until = 0;
@@ -75,9 +85,9 @@ class RaexBlindTransmitComponent : public Component, public CustomAPIDevice {
7585
// think of it as the setup() call in Arduino
7686
pinMode(3, OUTPUT);
7787

78-
register_service(&RaexBlindTransmitComponent::transmit, "transmit",
88+
register_service(&RaexBlindTX::transmit, "transmit",
7989
{"remote_id", "channel_id", "action"});
80-
register_service(&RaexBlindTransmitComponent::transmit_custom, "transmit_custom",
90+
register_service(&RaexBlindTX::transmit_custom, "transmit_custom",
8191
{"remote_id", "channel_id", "action", "blocks", "retries"});
8292
}
8393

@@ -256,3 +266,126 @@ class RaexBlindTransmitComponent : public Component, public CustomAPIDevice {
256266
}
257267
};
258268

269+
class RaexBlindCover : public cover::Cover, public Component {
270+
public:
271+
void setup() override {
272+
// Load saved state from preferences
273+
float saved_position;
274+
if (this->pref_position_.load(&saved_position)) {
275+
this->position = saved_position;
276+
} else {
277+
// Default to open if no saved state
278+
this->position = cover::COVER_OPEN;
279+
}
280+
281+
uint8_t saved_operation;
282+
if (this->pref_operation_.load(&saved_operation)) {
283+
this->current_operation = static_cast<cover::CoverOperation>(saved_operation);
284+
} else {
285+
this->current_operation = cover::COVER_OPERATION_IDLE;
286+
}
287+
288+
this->publish_state();
289+
}
290+
291+
void loop() override {
292+
// Check if we need to update state after movement
293+
if (this->state_change_time != 0) {
294+
const uint32_t now = millis();
295+
if (now - this->state_change_time >= 5000) { // 5 second delay
296+
this->state_change_time = 0;
297+
298+
if (this->current_operation == cover::COVER_OPERATION_OPENING) {
299+
this->current_operation = cover::COVER_OPERATION_IDLE;
300+
this->position = cover::COVER_OPEN;
301+
} else if (this->current_operation == cover::COVER_OPERATION_CLOSING) {
302+
this->current_operation = cover::COVER_OPERATION_IDLE;
303+
this->position = cover::COVER_CLOSED;
304+
}
305+
306+
// Save state when it changes
307+
this->save_state_();
308+
this->publish_state();
309+
}
310+
}
311+
}
312+
313+
void set_parent(RaexBlindTX *parent) { parent_ = parent; }
314+
void set_remote_id(uint16_t remote_id) {
315+
remote_id_ = remote_id;
316+
// Initialize preferences with unique keys based on remote_id and channel_id
317+
this->pref_position_ = global_preferences->make_preference<float>(remote_id_ << 8 | channel_id_);
318+
this->pref_operation_ = global_preferences->make_preference<uint8_t>((remote_id_ << 8 | channel_id_) + 1);
319+
}
320+
void set_channel_id(uint8_t channel_id) {
321+
channel_id_ = channel_id;
322+
// Re-initialize preferences with updated IDs
323+
this->pref_position_ = global_preferences->make_preference<float>(remote_id_ << 8 | channel_id_);
324+
this->pref_operation_ = global_preferences->make_preference<uint8_t>((remote_id_ << 8 | channel_id_) + 1);
325+
}
326+
327+
cover::CoverTraits get_traits() override {
328+
auto traits = cover::CoverTraits();
329+
traits.set_is_assumed_state(true);
330+
traits.set_supports_position(false);
331+
traits.set_supports_tilt(false);
332+
traits.set_supports_stop(true); // Enable stop button
333+
return traits;
334+
}
335+
336+
protected:
337+
void control(const cover::CoverCall &call) override {
338+
if (call.get_stop()) {
339+
// Handle stop command
340+
this->parent_->transmit(remote_id_, channel_id_, "STOP");
341+
this->current_operation = cover::COVER_OPERATION_IDLE;
342+
this->state_change_time = 0;
343+
this->save_state_();
344+
this->publish_state();
345+
} else if (call.get_position().has_value()) {
346+
auto pos = *call.get_position();
347+
if (pos == cover::COVER_OPEN) {
348+
this->parent_->transmit(remote_id_, channel_id_, "OPEN");
349+
this->current_operation = cover::COVER_OPERATION_OPENING;
350+
this->state_change_time = millis();
351+
} else if (pos == cover::COVER_CLOSED) {
352+
this->parent_->transmit(remote_id_, channel_id_, "CLOSE");
353+
this->current_operation = cover::COVER_OPERATION_CLOSING;
354+
this->state_change_time = millis();
355+
}
356+
this->publish_state();
357+
}
358+
}
359+
360+
void save_state_() {
361+
this->pref_position_.save(&this->position);
362+
uint8_t operation = static_cast<uint8_t>(this->current_operation);
363+
this->pref_operation_.save(&operation);
364+
}
365+
366+
RaexBlindTX *parent_;
367+
uint16_t remote_id_{0};
368+
uint8_t channel_id_{0};
369+
uint32_t state_change_time{0}; // Tracks when state changes for delayed updates
370+
ESPPreferenceObject pref_position_;
371+
ESPPreferenceObject pref_operation_;
372+
};
373+
374+
class RaexBlindPairButton : public button::Button, public Component {
375+
public:
376+
void set_parent(RaexBlindTX *parent) { parent_ = parent; }
377+
void set_remote_id(uint16_t remote_id) { remote_id_ = remote_id; }
378+
void set_channel_id(uint8_t channel_id) { channel_id_ = channel_id; }
379+
380+
protected:
381+
void press_action() override {
382+
this->parent_->transmit(remote_id_, channel_id_, "PAIR");
383+
}
384+
385+
RaexBlindTX *parent_;
386+
uint16_t remote_id_{0};
387+
uint8_t channel_id_{0};
388+
};
389+
390+
} // namespace raex_blind_tx
391+
} // namespace esphome
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Template for a blind and its pairing button
2+
# Required variables:
3+
# - name: The name of the blind (e.g. "Bedroom Screen")
4+
# - id: The ID prefix to use (e.g. "bedroomScreen")
5+
# - remote_id: The remote ID for this blind
6+
# - channel_id: The channel ID for this blind
7+
# Note: The ID will be automatically derived from the package name
8+
api:
9+
custom_services: true
10+
11+
cover:
12+
- platform: raex_blind_tx
13+
name: "${name}"
14+
device_class: blind
15+
raex_parent: raex_tx
16+
remote_id: ${remote_id}
17+
channel_id: ${channel_id}
18+
device_id: ${device_id}
19+
20+
button:
21+
- platform: raex_blind_tx
22+
name: "${name} Pairing"
23+
entity_category: "config"
24+
raex_parent: raex_tx
25+
remote_id: ${remote_id}
26+
channel_id: ${channel_id}
27+
device_id: ${device_id}

0 commit comments

Comments
 (0)