Files
esphome/components/ratgdo/ratgdo.h
T

323 lines
10 KiB
C++

/************************************
* Rage
* Against
* The
* Garage
* Door
* Opener
*
* Copyright (C) 2022 Paul Wieland
*
* GNU GENERAL PUBLIC LICENSE
************************************/
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include <type_traits>
#include <utility>
#include "callbacks.h"
#include "observable.h"
#include "ratgdo_state.h"
namespace esphome::ratgdo {
namespace secplus2 { class Secplus2; }
class RATGDOComponent;
typedef Parented<RATGDOComponent> RATGDOClient;
const float DOOR_POSITION_UNKNOWN = -1.0;
const float DOOR_DELTA_UNKNOWN = -2.0;
class RATGDOComponent : public Component {
public:
RATGDOComponent()
{
}
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void init_protocol();
float start_opening { -1 };
single_observable<float> opening_duration { 0 };
float start_closing { -1 };
single_observable<float> closing_duration { 0 };
observable<int16_t, RATGDO_MAX_DISTANCE_SUBSCRIBERS> last_distance_measurement { 0 };
single_observable<uint16_t> openings { 0 }; // number of times the door has been opened
observable<DoorState, RATGDO_MAX_DOOR_STATE_SUBSCRIBERS> door_state { DoorState::UNKNOWN };
observable<float, RATGDO_MAX_DOOR_STATE_SUBSCRIBERS> door_position { DOOR_POSITION_UNKNOWN };
unsigned long door_start_moving { 0 };
float door_start_position { DOOR_POSITION_UNKNOWN };
float door_move_delta { DOOR_DELTA_UNKNOWN };
uint16_t position_sync_remaining_ { 0 };
float target_position_ { DOOR_POSITION_UNKNOWN };
DoorAction target_direction_ { DoorAction::UNKNOWN };
single_observable<LightState> light_state { LightState::UNKNOWN };
single_observable<LockState> lock_state { LockState::UNKNOWN };
OnceCallbacks<void(DoorState)> on_door_state_;
single_observable<bool> synced { false };
single_observable<optional<bool>> sync_failed { nullopt };
void set_output_gdo_pin(InternalGPIOPin* pin) { this->output_gdo_pin_ = pin; }
void set_input_gdo_pin(InternalGPIOPin* pin) { this->input_gdo_pin_ = pin; }
void received(const DoorState door_state);
void received(const LightState light_state);
void received(const LockState lock_state);
void received(const LightAction light_action);
void received(const Openings openings);
// door
void door_toggle();
void door_open();
void door_close();
void door_stop();
void door_action(DoorAction action);
void smart_door_action(DoorAction target_direction);
void door_move_to_position(float position);
void set_door_position(float door_position) { this->door_position = door_position; }
void set_opening_duration(float duration);
void set_closing_duration(float duration);
void schedule_door_position_sync(float update_period = 500);
void door_position_update();
void cancel_position_sync_callbacks();
void set_distance_measurement(int16_t distance);
// light
void light_on();
void light_off();
LightState get_light_state() const;
// lock
void lock();
void unlock();
// button functionality
void query_status();
void query_openings();
void sync();
using Component::set_timeout;
void set_door_state_expiry();
void cancel_door_state_expiry();
// Register a one-shot door state callback with automatic expiry.
//
// Handles secplus1's nested callback chains where opening from
// STOPPED requires multiple state transitions:
//
// on_door_state(outer_cb) // wait for CLOSING
// → set_door_state_expiry() // expiry A
// → [door reports CLOSING]
// → outer_cb fires, calls:
// toggle_door()
// on_door_state(inner_cb) // wait for STOPPED
// → set_door_state_expiry() // expiry B (replaces A)
// → [door reports STOPPED]
// → inner_cb fires
// toggle_door() // door now opening
// count()==0 → cancel expiry B
//
// The user callback runs BEFORE the expiry check because it may
// re-arm the chain by calling on_door_state() again. If it does,
// the new call sets expiry B which replaces expiry A (same timeout
// ID = replace, not add). We only cancel expiry when count()==0,
// meaning no new callback was queued — otherwise we'd cancel
// expiry B here and leave the inner callback without protection.
template <typename F>
void on_door_state(F&& callback)
{
using Cb = std::decay_t<F>;
this->on_door_state_([this, cb = Cb(std::forward<F>(callback))](DoorState s) {
cb(s);
if (!this->on_door_state_.count()) {
this->cancel_door_state_expiry();
}
});
this->set_door_state_expiry();
}
// children subscriptions — type-safe templates (no std::function)
// Callbacks must be trivially copyable and fit in Callback storage
// (3 * sizeof(void*)), e.g. [this] or [this, f] lambdas.
// Enforced at compile time by Callback::create().
template <typename F>
void subscribe_opening_duration(F&& f);
template <typename F>
void subscribe_closing_duration(F&& f);
template <typename F>
void subscribe_openings(F&& f);
template <typename F>
void subscribe_door_state(F&& f);
template <typename F>
void subscribe_light_state(F&& f);
template <typename F>
void subscribe_lock_state(F&& f);
template <typename F>
void subscribe_sync_failed(F&& f);
template <typename F>
void subscribe_distance_measurement(F&& f);
protected:
// Pointers first (4-byte aligned)
secplus2::Secplus2* protocol_;
InternalGPIOPin* output_gdo_pin_;
InternalGPIOPin* input_gdo_pin_;
// Bool members packed into bitfield
struct {
uint8_t reserved : 8; // Reserved for future use
} flags_ { 0 };
// Subscriber counters for defer name allocation
uint8_t door_state_sub_num_ { 0 };
uint8_t distance_sub_num_ { 0 };
}; // RATGDOComponent
void log_subscriber_overflow(const LogString* observable_name, uint32_t max);
inline uint32_t get_scheduler_id(uint32_t base, uint32_t count, uint8_t& counter, const LogString* observable_name)
{
if (count == 0) {
log_subscriber_overflow(observable_name, count);
return base;
}
if (counter >= count) {
log_subscriber_overflow(observable_name, count);
return base + count - 1; // reuse last ID to avoid collision with first subscriber
}
return base + counter++;
}
// Scheduler IDs using uint32_t ranges to avoid heap allocations
// Bases are auto-generated from counts to prevent ID conflicts
namespace scheduler_ids {
inline constexpr uint32_t INTERVAL_POSITION_SYNC = 0;
// Multi-subscriber ranges — counts derived from codegen defines
inline constexpr uint32_t DEFER_DOOR_STATE_COUNT = RATGDO_MAX_DOOR_STATE_SUBSCRIBERS;
inline constexpr uint32_t DEFER_DOOR_STATE_BASE = INTERVAL_POSITION_SYNC + 1;
inline constexpr uint32_t DEFER_DISTANCE_COUNT = RATGDO_MAX_DISTANCE_SUBSCRIBERS;
inline constexpr uint32_t DEFER_DISTANCE_BASE = DEFER_DOOR_STATE_BASE + DEFER_DOOR_STATE_COUNT;
inline constexpr uint32_t DEFER_DISTANCE_END = DEFER_DISTANCE_BASE + DEFER_DISTANCE_COUNT;
// Single-subscriber IDs
enum : uint32_t {
DEFER_OPENING_DURATION = DEFER_DISTANCE_END,
DEFER_CLOSING_DURATION,
DEFER_OPENINGS,
DEFER_LIGHT_STATE,
DEFER_LOCK_STATE,
// Named timeout IDs (replacing string-based names)
TIMEOUT_DOOR_QUERY_STATE,
TIMEOUT_DOOR_ACTION,
TIMEOUT_MOVE_TO_POSITION,
TIMEOUT_DOOR_STATE_EXPIRY,
TIMEOUT_SYNC,
};
} // namespace scheduler_ids
// Template implementations for subscribe methods.
// Each wraps the callback in a deferred call so that if the observable
// fires multiple times during one loop iteration, only the last value
// is dispatched to the child component.
template <typename F>
void RATGDOComponent::subscribe_opening_duration(F&& f)
{
this->opening_duration.subscribe([this, f](float state) {
defer(scheduler_ids::DEFER_OPENING_DURATION, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_closing_duration(F&& f)
{
this->closing_duration.subscribe([this, f](float state) {
defer(scheduler_ids::DEFER_CLOSING_DURATION, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_openings(F&& f)
{
this->openings.subscribe([this, f](uint16_t state) {
defer(scheduler_ids::DEFER_OPENINGS, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_door_state(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_DOOR_STATE_BASE, scheduler_ids::DEFER_DOOR_STATE_COUNT,
this->door_state_sub_num_, LOG_STR("door_state"));
this->door_state.subscribe([this, f, id](DoorState state) {
defer(id, [this, f, state] { f(state, *this->door_position); });
});
this->door_position.subscribe([this, f, id](float position) {
defer(id, [this, f, position] { f(*this->door_state, position); });
});
}
template <typename F>
void RATGDOComponent::subscribe_light_state(F&& f)
{
this->light_state.subscribe([this, f](LightState state) {
defer(scheduler_ids::DEFER_LIGHT_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_lock_state(F&& f)
{
this->lock_state.subscribe([this, f](LockState state) {
defer(scheduler_ids::DEFER_LOCK_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_sync_failed(F&& f)
{
this->sync_failed.subscribe(std::forward<F>(f));
}
template <typename F>
void RATGDOComponent::subscribe_distance_measurement(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_DISTANCE_BASE, scheduler_ids::DEFER_DISTANCE_COUNT,
this->distance_sub_num_, LOG_STR("distance_measurement"));
this->last_distance_measurement.subscribe([this, f, id](int16_t state) {
defer(id, [f, state] { f(state); });
});
}
} // namespace esphome::ratgdo