Make garage-gate.yaml fully local

This commit is contained in:
2026-05-10 17:03:33 +00:00
parent ec837d2c28
commit 24d06982a7
44 changed files with 5218 additions and 7 deletions
+250
View File
@@ -0,0 +1,250 @@
from dataclasses import dataclass
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from esphome.core import CORE
from esphome.coroutine import CoroPriority, coroutine_with_priority
import voluptuous as vol
DEPENDENCIES = ["preferences"]
MULTI_CONF = False
DOMAIN = "ratgdo"
ratgdo_ns = cg.esphome_ns.namespace("ratgdo")
RATGDO = ratgdo_ns.class_("RATGDOComponent", cg.Component)
@dataclass
class RATGDOData:
"""Track observable subscriber counts for compile-time sizing."""
door_state: int = 0
door_action_delayed: int = 0
distance: int = 0
vehicle_detected: int = 0
vehicle_arriving: int = 0
vehicle_leaving: int = 0
def _get_data() -> RATGDOData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = RATGDOData()
return CORE.data[DOMAIN]
def subscribe_door_state() -> None:
_get_data().door_state += 1
def subscribe_door_action_delayed() -> None:
_get_data().door_action_delayed += 1
def subscribe_distance() -> None:
_get_data().distance += 1
def subscribe_vehicle_detected() -> None:
_get_data().vehicle_detected += 1
def subscribe_vehicle_arriving() -> None:
_get_data().vehicle_arriving += 1
def subscribe_vehicle_leaving() -> None:
_get_data().vehicle_leaving += 1
@coroutine_with_priority(CoroPriority.FINAL)
async def _emit_subscriber_defines():
"""Emit observable subscriber count defines after all children have registered."""
data = _get_data()
cg.add_define("RATGDO_MAX_DOOR_STATE_SUBSCRIBERS", data.door_state)
cg.add_define(
"RATGDO_MAX_DOOR_ACTION_DELAYED_SUBSCRIBERS", data.door_action_delayed
)
cg.add_define("RATGDO_MAX_DISTANCE_SUBSCRIBERS", data.distance)
cg.add_define("RATGDO_MAX_VEHICLE_DETECTED_SUBSCRIBERS", data.vehicle_detected)
cg.add_define("RATGDO_MAX_VEHICLE_ARRIVING_SUBSCRIBERS", data.vehicle_arriving)
cg.add_define("RATGDO_MAX_VEHICLE_LEAVING_SUBSCRIBERS", data.vehicle_leaving)
SyncFailed = ratgdo_ns.class_("SyncFailed", automation.Trigger.template())
CONF_OUTPUT_GDO = "output_gdo_pin"
DEFAULT_OUTPUT_GDO = (
"D4" # D4 red control terminal / GarageDoorOpener (UART1 TX) pin is D4 on D1 Mini
)
CONF_INPUT_GDO = "input_gdo_pin"
DEFAULT_INPUT_GDO = (
"D2" # D2 red control terminal / GarageDoorOpener (UART1 RX) pin is D2 on D1 Mini
)
CONF_INPUT_OBST = "input_obst_pin"
DEFAULT_INPUT_OBST = "D7" # D7 black obstruction sensor terminal
CONF_OBST_SLEEP_LOW = "obst_sleep_low"
CONF_DISCRETE_OPEN_PIN = "discrete_open_pin"
CONF_DISCRETE_CLOSE_PIN = "discrete_close_pin"
CONF_RATGDO_ID = "ratgdo_id"
CONF_ON_SYNC_FAILED = "on_sync_failed"
CONF_PROTOCOL = "protocol"
PROTOCOL_SECPLUSV1 = "secplusv1"
PROTOCOL_SECPLUSV2 = "secplusv2"
PROTOCOL_DRYCONTACT = "drycontact"
SUPPORTED_PROTOCOLS = [PROTOCOL_SECPLUSV1, PROTOCOL_SECPLUSV2, PROTOCOL_DRYCONTACT]
CONF_DRY_CONTACT_OPEN_SENSOR = "dry_contact_open_sensor"
CONF_DRY_CONTACT_CLOSE_SENSOR = "dry_contact_close_sensor"
CONF_DRY_CONTACT_SENSOR_GROUP = "dry_contact_sensor_group"
def validate_protocol(config):
if config.get(CONF_PROTOCOL, None) == PROTOCOL_DRYCONTACT and (
CONF_DRY_CONTACT_CLOSE_SENSOR not in config
or CONF_DRY_CONTACT_OPEN_SENSOR not in config
):
raise cv.Invalid(
"dry_contact_close_sensor and dry_contact_open_sensor are required when using protocol drycontact"
)
if config.get(CONF_PROTOCOL, None) != PROTOCOL_DRYCONTACT and (
CONF_DRY_CONTACT_CLOSE_SENSOR in config
or CONF_DRY_CONTACT_OPEN_SENSOR in config
):
raise cv.Invalid(
"dry_contact_close_sensor and dry_contact_open_sensor are only valid when using protocol drycontact"
)
# if config.get(CONF_PROTOCOL, None) == PROTOCOL_DRYCONTACT and CONF_DRY_CONTACT_OPEN_SENSOR not in config:
# raise cv.Invalid("dry_contact_open_sensor is required when using protocol drycontact")
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(RATGDO),
cv.Optional(
CONF_OUTPUT_GDO, default=DEFAULT_OUTPUT_GDO
): pins.gpio_output_pin_schema,
cv.Optional(
CONF_INPUT_GDO, default=DEFAULT_INPUT_GDO
): pins.gpio_input_pin_schema,
cv.Optional(CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST): cv.Any(
cv.none, pins.gpio_input_pin_schema
),
cv.SplitDefault(CONF_OBST_SLEEP_LOW, esp32=False, esp8266=True): cv.boolean,
cv.Optional(CONF_DISCRETE_OPEN_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DISCRETE_CLOSE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_ON_SYNC_FAILED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncFailed),
}
),
cv.Optional(CONF_PROTOCOL, default=PROTOCOL_SECPLUSV2): cv.All(
vol.In(SUPPORTED_PROTOCOLS)
),
# cv.Inclusive(CONF_DRY_CONTACT_OPEN_SENSOR,CONF_DRY_CONTACT_SENSOR_GROUP): cv.use_id(binary_sensor.BinarySensor),
# cv.Inclusive(CONF_DRY_CONTACT_CLOSE_SENSOR,CONF_DRY_CONTACT_SENSOR_GROUP): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_DRY_CONTACT_OPEN_SENSOR): cv.use_id(
binary_sensor.BinarySensor
),
cv.Optional(CONF_DRY_CONTACT_CLOSE_SENSOR): cv.use_id(
binary_sensor.BinarySensor
),
}
).extend(cv.COMPONENT_SCHEMA),
validate_protocol,
)
RATGDO_CLIENT_SCHMEA = cv.Schema(
{
cv.GenerateID(CONF_RATGDO_ID): cv.use_id(RATGDO),
}
)
async def register_ratgdo_child(var, config):
parent = await cg.get_variable(config[CONF_RATGDO_ID])
cg.add(var.set_parent(parent))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
pin = await cg.gpio_pin_expression(config[CONF_OUTPUT_GDO])
cg.add(var.set_output_gdo_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_INPUT_GDO])
cg.add(var.set_input_gdo_pin(pin))
if config.get(CONF_INPUT_OBST):
pin = await cg.gpio_pin_expression(config[CONF_INPUT_OBST])
cg.add(var.set_input_obst_pin(pin))
cg.add(var.set_obst_sleep_low(config[CONF_OBST_SLEEP_LOW]))
if config.get(CONF_DRY_CONTACT_OPEN_SENSOR):
dry_contact_open_sensor = await cg.get_variable(
config[CONF_DRY_CONTACT_OPEN_SENSOR]
)
cg.add(var.set_dry_contact_open_sensor(dry_contact_open_sensor))
if config.get(CONF_DRY_CONTACT_CLOSE_SENSOR):
dry_contact_close_sensor = await cg.get_variable(
config[CONF_DRY_CONTACT_CLOSE_SENSOR]
)
cg.add(var.set_dry_contact_close_sensor(dry_contact_close_sensor))
for conf in config.get(CONF_ON_SYNC_FAILED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
if CORE.is_esp32 and not CORE.using_arduino:
from esphome.components import esp32
esp32.include_builtin_idf_component("esp_driver_rmt")
esp32.add_idf_component(
name="secplus",
repo="https://github.com/ratgdo/secplus.git",
ref="add-esp-idf-support",
)
else:
cg.add_library(
name="secplus",
repository="https://github.com/ratgdo/secplus#f98c3220356c27717a25102c0b35815ebbd26ccc",
version=None,
)
if CORE.is_esp8266:
cg.add_library(
name="espsoftwareserial",
repository="https://github.com/ratgdo/espsoftwareserial#autobaud",
version=None,
)
if config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV1:
cg.add_build_flag("-DPROTOCOL_SECPLUSV1")
elif config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV2:
cg.add_build_flag("-DPROTOCOL_SECPLUSV2")
elif config[CONF_PROTOCOL] == PROTOCOL_DRYCONTACT:
cg.add_build_flag("-DPROTOCOL_DRYCONTACT")
cg.add(var.init_protocol())
# RATGDOComponent::setup() subscribes to door_state
subscribe_door_state()
# Emit observable subscriber count defines after all children register
CORE.add_job(_emit_subscriber_defines)
if config.get(CONF_DISCRETE_OPEN_PIN):
pin = await cg.gpio_pin_expression(config[CONF_DISCRETE_OPEN_PIN])
cg.add(var.set_discrete_open_pin(pin))
if config.get(CONF_DISCRETE_CLOSE_PIN):
pin = await cg.gpio_pin_expression(config[CONF_DISCRETE_CLOSE_PIN])
cg.add(var.set_discrete_close_pin(pin))
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "ratgdo.h"
namespace esphome::ratgdo {
class SyncFailed : public Trigger<> {
public:
explicit SyncFailed(RATGDOComponent* parent)
{
parent->subscribe_sync_failed([this](bool state) {
if (state)
this->trigger();
});
}
};
} // namespace esphome::ratgdo
@@ -0,0 +1,77 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import (
RATGDO_CLIENT_SCHMEA,
ratgdo_ns,
register_ratgdo_child,
subscribe_vehicle_arriving,
subscribe_vehicle_detected,
subscribe_vehicle_leaving,
)
DEPENDENCIES = ["ratgdo"]
# Track which sensor types have been used
USED_TYPES: set[str] = set()
RATGDOBinarySensor = ratgdo_ns.class_(
"RATGDOBinarySensor", binary_sensor.BinarySensor, cg.Component
)
SensorType = ratgdo_ns.enum("SensorType")
CONF_TYPE = "type"
TYPES = {
"motion": SensorType.RATGDO_SENSOR_MOTION,
"obstruction": SensorType.RATGDO_SENSOR_OBSTRUCTION,
"motor": SensorType.RATGDO_SENSOR_MOTOR,
"button": SensorType.RATGDO_SENSOR_BUTTON,
"vehicle_detected": SensorType.RATGDO_SENSOR_VEHICLE_DETECTED,
"vehicle_arriving": SensorType.RATGDO_SENSOR_VEHICLE_ARRIVING,
"vehicle_leaving": SensorType.RATGDO_SENSOR_VEHICLE_LEAVING,
}
# Sensor types that require vehicle sensor support
VEHICLE_SENSOR_TYPES = {"vehicle_detected", "vehicle_arriving", "vehicle_leaving"}
def validate_unique_type(config):
"""Validate that each sensor type is only used once."""
sensor_type = config[CONF_TYPE]
if sensor_type in USED_TYPES:
raise cv.Invalid(f"Only one binary sensor of type '{sensor_type}' is allowed")
USED_TYPES.add(sensor_type)
return config
CONFIG_SCHEMA = cv.All(
binary_sensor.binary_sensor_schema(RATGDOBinarySensor)
.extend(
{
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
}
)
.extend(RATGDO_CLIENT_SCHMEA),
validate_unique_type,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await binary_sensor.register_binary_sensor(var, config)
await cg.register_component(var, config)
cg.add(var.set_binary_sensor_type(config[CONF_TYPE]))
await register_ratgdo_child(var, config)
# Add defines for enabled features and register observable subscriptions
sensor_type = config[CONF_TYPE]
if sensor_type in VEHICLE_SENSOR_TYPES:
cg.add_define("RATGDO_USE_VEHICLE_SENSORS")
if sensor_type == "vehicle_detected":
subscribe_vehicle_detected()
elif sensor_type == "vehicle_arriving":
subscribe_vehicle_arriving()
elif sensor_type == "vehicle_leaving":
subscribe_vehicle_leaving()
@@ -0,0 +1,92 @@
#include "ratgdo_binary_sensor.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo.binary_sensor";
void RATGDOBinarySensor::setup()
{
// Initialize all sensors to false except motor (which doesn't set initial state)
if (this->binary_sensor_type_ != SensorType::RATGDO_SENSOR_MOTOR) {
this->publish_initial_state(false);
}
switch (this->binary_sensor_type_) {
case SensorType::RATGDO_SENSOR_MOTION:
this->parent_->subscribe_motion_state([this](MotionState state) {
this->publish_state(state == MotionState::DETECTED);
});
break;
case SensorType::RATGDO_SENSOR_OBSTRUCTION:
this->parent_->subscribe_obstruction_state([this](ObstructionState state) {
this->publish_state(state == ObstructionState::OBSTRUCTED);
});
break;
case SensorType::RATGDO_SENSOR_MOTOR:
this->parent_->subscribe_motor_state([this](MotorState state) {
this->publish_state(state == MotorState::ON);
});
break;
case SensorType::RATGDO_SENSOR_BUTTON:
this->parent_->subscribe_button_state([this](ButtonState state) {
this->publish_state(state == ButtonState::PRESSED);
});
break;
#ifdef RATGDO_USE_VEHICLE_SENSORS
case SensorType::RATGDO_SENSOR_VEHICLE_DETECTED:
this->parent_->subscribe_vehicle_detected_state([this](VehicleDetectedState state) {
this->publish_state(state == VehicleDetectedState::YES);
this->parent_->presence_change(state == VehicleDetectedState::YES);
});
break;
case SensorType::RATGDO_SENSOR_VEHICLE_ARRIVING:
this->parent_->subscribe_vehicle_arriving_state([this](VehicleArrivingState state) {
this->publish_state(state == VehicleArrivingState::YES);
});
break;
case SensorType::RATGDO_SENSOR_VEHICLE_LEAVING:
this->parent_->subscribe_vehicle_leaving_state([this](VehicleLeavingState state) {
this->publish_state(state == VehicleLeavingState::YES);
});
break;
#endif
default:
break;
}
}
void RATGDOBinarySensor::dump_config()
{
LOG_BINARY_SENSOR("", "RATGDO BinarySensor", this);
switch (this->binary_sensor_type_) {
case SensorType::RATGDO_SENSOR_MOTION:
ESP_LOGCONFIG(TAG, " Type: Motion");
break;
case SensorType::RATGDO_SENSOR_OBSTRUCTION:
ESP_LOGCONFIG(TAG, " Type: Obstruction");
break;
case SensorType::RATGDO_SENSOR_MOTOR:
ESP_LOGCONFIG(TAG, " Type: Motor");
break;
case SensorType::RATGDO_SENSOR_BUTTON:
ESP_LOGCONFIG(TAG, " Type: Button");
break;
#ifdef RATGDO_USE_VEHICLE_SENSORS
case SensorType::RATGDO_SENSOR_VEHICLE_DETECTED:
ESP_LOGCONFIG(TAG, " Type: VehicleDetected");
break;
case SensorType::RATGDO_SENSOR_VEHICLE_ARRIVING:
ESP_LOGCONFIG(TAG, " Type: VehicleArriving");
break;
case SensorType::RATGDO_SENSOR_VEHICLE_LEAVING:
ESP_LOGCONFIG(TAG, " Type: VehicleLeaving");
break;
#endif
default:
break;
}
}
} // namespace esphome::ratgdo
@@ -0,0 +1,33 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome::ratgdo {
enum SensorType : uint8_t {
RATGDO_SENSOR_MOTION,
RATGDO_SENSOR_OBSTRUCTION,
RATGDO_SENSOR_MOTOR,
RATGDO_SENSOR_BUTTON,
#ifdef RATGDO_USE_VEHICLE_SENSORS
RATGDO_SENSOR_VEHICLE_DETECTED,
RATGDO_SENSOR_VEHICLE_ARRIVING,
RATGDO_SENSOR_VEHICLE_LEAVING,
#endif
};
class RATGDOBinarySensor : public binary_sensor::BinarySensor, public RATGDOClient, public Component {
public:
void setup() override;
void dump_config() override;
void set_binary_sensor_type(SensorType binary_sensor_type) { this->binary_sensor_type_ = binary_sensor_type; }
protected:
SensorType binary_sensor_type_;
};
} // namespace esphome::ratgdo
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include "observable.h"
#include <cstdint>
#include <utility>
namespace esphome::ratgdo {
void log_once_callbacks_overflow(uint8_t max);
template <typename... X>
class OnceCallbacks;
template <typename... Ts>
class OnceCallbacks<void(Ts...)> {
public:
// Runtime max is 1 for all current usage (door_state waits, command_sent waits).
// Set to 2 for safety margin.
static constexpr uint8_t MAX_CALLBACKS = 2;
template <typename F>
void operator()(F&& callback)
{
if (this->count_ >= MAX_CALLBACKS) {
log_once_callbacks_overflow(MAX_CALLBACKS);
return;
}
this->callbacks_[this->count_++] = Callback<Ts...>::create(std::forward<F>(callback));
}
// Re-entrant safe: count_ is zeroed before invoking callbacks,
// so callbacks can queue new entries during trigger().
void trigger(Ts... args)
{
uint8_t count = this->count_;
this->count_ = 0;
for (uint8_t i = 0; i < count; i++) {
this->callbacks_[i].call(args...);
}
}
void clear() { this->count_ = 0; }
uint8_t count() const { return this->count_; }
protected:
Callback<Ts...> callbacks_[MAX_CALLBACKS] { };
uint8_t count_ { 0 };
};
} // namespace esphome::ratgdo
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define ESP_LOG1 ESP_LOGV
#define ESP_LOG2 ESP_LOGV
+68
View File
@@ -0,0 +1,68 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import cover
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from .. import (
RATGDO_CLIENT_SCHMEA,
ratgdo_ns,
register_ratgdo_child,
subscribe_door_state,
)
DEPENDENCIES = ["ratgdo"]
RATGDOCover = ratgdo_ns.class_("RATGDOCover", cover.Cover, cg.Component)
# Triggers
CoverOpeningTrigger = ratgdo_ns.class_(
"CoverOpeningTrigger", automation.Trigger.template()
)
CoverClosingTrigger = ratgdo_ns.class_(
"CoverClosingTrigger", automation.Trigger.template()
)
CoverStateTrigger = ratgdo_ns.class_("CoverStateTrigger", automation.Trigger.template())
CONF_ON_OPENING = "on_opening"
CONF_ON_CLOSING = "on_closing"
CONF_ON_STATE_CHANGE = "on_state_change"
CONFIG_SCHEMA = (
cover.cover_schema(RATGDOCover)
.extend(
{
cv.GenerateID(): cv.declare_id(RATGDOCover),
cv.Optional(CONF_ON_OPENING): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpeningTrigger)}
),
cv.Optional(CONF_ON_CLOSING): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosingTrigger)}
),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverStateTrigger)}
),
}
)
.extend(RATGDO_CLIENT_SCHMEA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cover.register_cover(var, config)
for conf in config.get(CONF_ON_OPENING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CLOSING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await register_ratgdo_child(var, config)
subscribe_door_state()
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "esphome/components/cover/cover.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome::ratgdo {
class CoverOpeningTrigger : public Trigger<> {
public:
CoverOpeningTrigger(cover::Cover* a_cover)
{
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->current_operation == cover::COVER_OPERATION_OPENING) {
this->trigger();
}
});
}
};
class CoverClosingTrigger : public Trigger<> {
public:
CoverClosingTrigger(cover::Cover* a_cover)
{
a_cover->add_on_state_callback([this, a_cover]() {
if (a_cover->current_operation == cover::COVER_OPERATION_CLOSING) {
this->trigger();
}
});
}
};
class CoverStateTrigger : public Trigger<> {
public:
CoverStateTrigger(cover::Cover* a_cover)
{
a_cover->add_on_state_callback([this, a_cover]() {
this->trigger();
});
}
};
} // namespace esphome::ratgdo
+94
View File
@@ -0,0 +1,94 @@
#include "ratgdo_cover.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
using namespace esphome::cover;
static const char* const TAG = "ratgdo.cover";
void RATGDOCover::dump_config()
{
LOG_COVER("", "RATGDO Cover", this);
}
void RATGDOCover::setup()
{
auto state = this->restore_state_();
if (state.has_value()) {
this->parent_->set_door_position(state.value().position);
}
this->parent_->subscribe_door_state([this](DoorState state, float position) {
this->on_door_state(state, position);
});
}
void RATGDOCover::on_door_state(DoorState state, float position)
{
// ESP_LOGD("ON_DOOR_STATE", "%s %f", LOG_STR_ARG(DoorState_to_string(state)), position);
bool save_to_flash = true;
switch (state) {
case DoorState::OPEN:
this->position = COVER_OPEN;
this->current_operation = COVER_OPERATION_IDLE;
break;
case DoorState::CLOSED:
this->position = COVER_CLOSED;
this->current_operation = COVER_OPERATION_IDLE;
break;
case DoorState::OPENING:
this->current_operation = COVER_OPERATION_OPENING;
this->position = position;
save_to_flash = false;
break;
case DoorState::CLOSING:
this->current_operation = COVER_OPERATION_CLOSING;
this->position = position;
save_to_flash = false;
break;
case DoorState::STOPPED:
this->current_operation = COVER_OPERATION_IDLE;
this->position = position;
break;
case DoorState::UNKNOWN:
default:
this->current_operation = COVER_OPERATION_IDLE;
this->position = position;
break;
}
this->publish_state(save_to_flash);
}
CoverTraits RATGDOCover::get_traits()
{
auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_toggle(true);
traits.set_supports_position(true);
return traits;
}
void RATGDOCover::control(const CoverCall& call)
{
if (call.get_stop()) {
this->parent_->door_stop();
}
if (call.get_toggle()) {
this->parent_->door_toggle();
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == COVER_OPEN) {
this->parent_->door_open();
} else if (pos == COVER_CLOSED) {
this->parent_->door_close();
} else {
this->parent_->door_move_to_position(pos);
}
}
}
} // namespace esphome::ratgdo
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/cover/cover.h"
#include "esphome/core/component.h"
namespace esphome::ratgdo {
class RATGDOCover : public cover::Cover, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
cover::CoverTraits get_traits() override;
void on_door_state(DoorState state, float position);
protected:
void control(const cover::CoverCall& call) override;
};
} // namespace esphome::ratgdo
+39
View File
@@ -0,0 +1,39 @@
import esphome.codegen as cg
from esphome.components import light
import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT_ID # New in 2023.5
from .. import RATGDO_CLIENT_SCHMEA, ratgdo_ns, register_ratgdo_child
DEPENDENCIES = ["ratgdo"]
# Track if light has been used
USED_LIGHTS: set[str] = set()
RATGDOLightOutput = ratgdo_ns.class_(
"RATGDOLightOutput", light.LightOutput, cg.Component
)
def validate_single_light(config):
"""Validate that only one RATGDO light is configured."""
light_id = "ratgdo_light"
if light_id in USED_LIGHTS:
raise cv.Invalid("Only one RATGDO light is allowed")
USED_LIGHTS.add(light_id)
return config
CONFIG_SCHEMA = cv.All(
light.LIGHT_SCHEMA.extend(
{cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RATGDOLightOutput)}
).extend(RATGDO_CLIENT_SCHMEA),
validate_single_light,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await cg.register_component(var, config)
await light.register_light(var, config)
await register_ratgdo_child(var, config)
@@ -0,0 +1,66 @@
#include "ratgdo_light_output.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
using namespace esphome::light;
static const char* const TAG = "ratgdo.light";
void RATGDOLightOutput::dump_config()
{
ESP_LOGCONFIG(TAG, "RATGDO Light");
}
void RATGDOLightOutput::setup()
{
this->parent_->subscribe_light_state([this](LightState state) {
this->on_light_state(state);
});
}
void RATGDOLightOutput::on_light_state(esphome::ratgdo::LightState state)
{
if (this->light_state_) {
this->has_initial_state_ = true;
set_state(state);
}
}
void RATGDOLightOutput::set_state(esphome::ratgdo::LightState state)
{
bool is_on = state == LightState::ON;
this->light_state_->current_values.set_state(is_on);
this->light_state_->remote_values.set_state(is_on);
this->light_state_->publish_state();
}
void RATGDOLightOutput::setup_state(light::LightState* light_state)
{
esphome::ratgdo::LightState state = this->parent_->get_light_state();
this->light_state_ = light_state;
this->set_state(state);
}
LightTraits RATGDOLightOutput::get_traits()
{
auto traits = LightTraits();
traits.set_supported_color_modes({ light::ColorMode::ON_OFF });
return traits;
}
void RATGDOLightOutput::write_state(light::LightState* state)
{
if (!this->has_initial_state_)
return;
bool binary;
state->current_values_as_binary(&binary);
if (binary) {
this->parent_->light_on();
} else {
this->parent_->light_off();
}
}
} // namespace esphome::ratgdo
@@ -0,0 +1,27 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/component.h"
namespace esphome::ratgdo {
class RATGDOLightOutput : public light::LightOutput, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
light::LightTraits get_traits() override;
void write_state(light::LightState* state) override;
void setup_state(light::LightState* state) override;
void set_state(esphome::ratgdo::LightState state);
light::LightState* get_state() { return this->light_state_; }
void on_light_state(esphome::ratgdo::LightState state);
protected:
light::LightState* light_state_;
bool has_initial_state_ = false;
};
} // namespace esphome::ratgdo
+41
View File
@@ -0,0 +1,41 @@
import esphome.codegen as cg
from esphome.components import lock
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import RATGDO_CLIENT_SCHMEA, ratgdo_ns, register_ratgdo_child
DEPENDENCIES = ["ratgdo"]
# Track if lock has been used
USED_LOCKS: set[str] = set()
RATGDOLock = ratgdo_ns.class_("RATGDOLock", lock.Lock, cg.Component)
def validate_single_lock(config):
"""Validate that only one RATGDO lock is configured."""
lock_id = "ratgdo_lock"
if lock_id in USED_LOCKS:
raise cv.Invalid("Only one RATGDO lock is allowed")
USED_LOCKS.add(lock_id)
return config
CONFIG_SCHEMA = cv.All(
lock.lock_schema(RATGDOLock)
.extend(
{
cv.GenerateID(): cv.declare_id(RATGDOLock),
}
)
.extend(RATGDO_CLIENT_SCHMEA),
validate_single_lock,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await lock.register_lock(var, config)
await cg.register_component(var, config)
await register_ratgdo_child(var, config)
+53
View File
@@ -0,0 +1,53 @@
#include "ratgdo_lock.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo.lock";
void RATGDOLock::dump_config()
{
LOG_LOCK("", "RATGDO Lock", this);
ESP_LOGCONFIG(TAG, " Type: Lock");
}
void RATGDOLock::setup()
{
this->parent_->subscribe_lock_state([this](LockState state) {
this->on_lock_state(state);
});
}
void RATGDOLock::on_lock_state(LockState state)
{
if (state == LockState::LOCKED && this->state == lock::LockState::LOCK_STATE_LOCKED) {
return;
}
if (state == LockState::UNLOCKED && this->state == lock::LockState::LOCK_STATE_UNLOCKED) {
return;
}
auto call = this->make_call();
if (state == LockState::LOCKED) {
call.set_state(lock::LockState::LOCK_STATE_LOCKED);
} else if (state == LockState::UNLOCKED) {
call.set_state(lock::LockState::LOCK_STATE_UNLOCKED);
}
this->publish_state(*call.get_state());
}
void RATGDOLock::control(const lock::LockCall& call)
{
auto state = *call.get_state();
if (state == lock::LockState::LOCK_STATE_LOCKED) {
this->parent_->lock();
} else if (state == lock::LockState::LOCK_STATE_UNLOCKED) {
this->parent_->unlock();
}
this->publish_state(state);
}
} // namespace esphome::ratgdo
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/lock/lock.h"
#include "esphome/core/component.h"
namespace esphome::ratgdo {
class RATGDOLock : public lock::Lock, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
void on_lock_state(LockState state);
void control(const lock::LockCall& call) override;
};
} // namespace esphome::ratgdo
+172
View File
@@ -0,0 +1,172 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/core/log.h"
#define PARENS ()
// Rescan macro tokens 256 times
#define EXPAND(...) EXPAND4(EXPAND4(EXPAND4(EXPAND4(__VA_ARGS__))))
#define EXPAND4(...) EXPAND3(EXPAND3(EXPAND3(EXPAND3(__VA_ARGS__))))
#define EXPAND3(...) EXPAND2(EXPAND2(EXPAND2(EXPAND2(__VA_ARGS__))))
#define EXPAND2(...) EXPAND1(EXPAND1(EXPAND1(EXPAND1(__VA_ARGS__))))
#define EXPAND1(...) __VA_ARGS__
#define FOR_EACH(macro, name, ...) \
__VA_OPT__(EXPAND(FOR_EACH_HELPER(macro, name, __VA_ARGS__)))
#define FOR_EACH_HELPER(macro, name, a1, ...) \
macro(name, a1) \
__VA_OPT__(FOR_EACH_AGAIN PARENS(macro, name, __VA_ARGS__))
#define FOR_EACH_AGAIN() FOR_EACH_HELPER
#define ENUM_VARIANT0(name, val) name = val,
#define ENUM_VARIANT(name, tuple) ENUM_VARIANT0 tuple
#define TUPLE(x, y) x, y
#define LPAREN (
#ifdef USE_ESP8266
#define TO_STRING_IF0(type, name, val) \
if (_e == type::name) \
return LOG_STR(#name);
#else
#define TO_STRING_IF0(type, name, val) \
if (_e == type::name) \
return #name;
#endif
#define TO_STRING_IF(type, tuple) TO_STRING_IF0 LPAREN type, TUPLE tuple)
#define FROM_INT_CASE0(type, name, val) \
case val: \
return type::name;
#define FROM_INT_CASE(type, tuple) FROM_INT_CASE0 LPAREN type, TUPLE tuple)
// String blob helpers for packed enum-to-string lookup tables
#define STR_BLOB_ENTRY0(type, name, val) #name "\0"
#define STR_BLOB_ENTRY(type, tuple) STR_BLOB_ENTRY0 LPAREN type, TUPLE tuple)
#define COUNT_ONE0(type, name, val) +1
#define COUNT_ONE(name, tuple) COUNT_ONE0 LPAREN name, TUPLE tuple)
namespace esphome::ratgdo {
namespace detail {
template <size_t N>
struct EnumStringOffsets {
uint8_t data[N];
};
template <size_t Count, size_t BlobSize>
constexpr EnumStringOffsets<Count> compute_enum_string_offsets(const char (&blob)[BlobSize])
{
EnumStringOffsets<Count> result { };
result.data[0] = 0;
size_t entry = 1;
for (size_t i = 0; i < BlobSize - 1 && entry < Count; ++i) {
if (blob[i] == '\0') {
result.data[entry++] = static_cast<uint8_t>(i + 1);
}
}
return result;
}
} // namespace detail
} // namespace esphome::ratgdo
// Platform-specific helpers for enum string return types
#ifdef USE_ESP8266
#define ENUM_STR_RET const esphome::LogString*
#define ENUM_STR_UNKNOWN LOG_STR("UNKNOWN")
#define ENUM_BLOB_ATTR PROGMEM
#define ENUM_BLOB_RETURN(blob, offset) reinterpret_cast<const esphome::LogString*>(&(blob)[offset])
#else
#define ENUM_STR_RET const char*
#define ENUM_STR_UNKNOWN "UNKNOWN"
#define ENUM_BLOB_ATTR
#define ENUM_BLOB_RETURN(blob, offset) (&(blob)[offset])
#endif
// ENUM: packed string blob with O(1) offset lookup (for contiguous 0-based enums with uint8_t type)
#define ENUM(name, type, ...) \
enum class name : type { \
FOR_EACH(ENUM_VARIANT, name, __VA_ARGS__) \
}; \
static_assert(sizeof(type) == 1, "ENUM() requires uint8_t type; use ENUM_SPARSE() for wider types"); \
inline ENUM_STR_RET \
name##_to_string(name _e) \
{ \
static constexpr size_t _n = (0 FOR_EACH(COUNT_ONE, name, __VA_ARGS__)); \
static const char _b[] ENUM_BLOB_ATTR = FOR_EACH(STR_BLOB_ENTRY, name, __VA_ARGS__); \
static_assert(sizeof(_b) <= 256, "ENUM() string blob exceeds 255 bytes; use shorter names"); \
static constexpr auto _o = ::esphome::ratgdo::detail::compute_enum_string_offsets<_n>( \
FOR_EACH(STR_BLOB_ENTRY, name, __VA_ARGS__)); \
auto _i = static_cast<uint8_t>(_e); \
if (_i >= _n) \
return ENUM_STR_UNKNOWN; \
return ENUM_BLOB_RETURN(_b, _o.data[_i]); \
} \
inline name \
to_##name(type _t, name _unknown) \
{ \
switch (_t) { \
FOR_EACH(FROM_INT_CASE, name, __VA_ARGS__) \
default: \
return _unknown; \
} \
}
// ENUM_SPARSE: if-chain lookup (for non-contiguous enum values, avoids CSWTCH)
#define ENUM_SPARSE(name, type, ...) \
enum class name : type { \
FOR_EACH(ENUM_VARIANT, name, __VA_ARGS__) \
}; \
inline ENUM_STR_RET \
name##_to_string(name _e) \
{ \
FOR_EACH(TO_STRING_IF, name, __VA_ARGS__) \
return ENUM_STR_UNKNOWN; \
} \
inline name \
to_##name(type _t, name _unknown) \
{ \
switch (_t) { \
FOR_EACH(FROM_INT_CASE, name, __VA_ARGS__) \
default: \
return _unknown; \
} \
}
#define SUM_TYPE_UNION_MEMBER0(type, var) type var;
#define SUM_TYPE_UNION_MEMBER(name, tuple) SUM_TYPE_UNION_MEMBER0 tuple
#define SUM_TYPE_ENUM_MEMBER0(type, var) var,
#define SUM_TYPE_ENUM_MEMBER(name, tuple) SUM_TYPE_ENUM_MEMBER0 tuple
#define SUM_TYPE_CONSTRUCTOR0(name, type, val) \
name(type&& arg) \
: tag(Tag::val) \
{ \
value.val = std::move(arg); \
}
#define SUM_TYPE_CONSTRUCTOR(name, tuple) SUM_TYPE_CONSTRUCTOR0 LPAREN name, TUPLE tuple)
#define SUM_TYPE(name, ...) \
class name { \
public: \
union { \
FOR_EACH(SUM_TYPE_UNION_MEMBER, name, __VA_ARGS__) \
} value; \
enum class Tag { \
void_, \
FOR_EACH(SUM_TYPE_ENUM_MEMBER, name, __VA_ARGS__) \
} tag; \
\
name() \
: tag(Tag::void_) \
{ \
} \
FOR_EACH(SUM_TYPE_CONSTRUCTOR, name, __VA_ARGS__) \
};
+58
View File
@@ -0,0 +1,58 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import RATGDO_CLIENT_SCHMEA, ratgdo_ns, register_ratgdo_child
DEPENDENCIES = ["ratgdo"]
# Track which number types have been used
USED_TYPES: set[str] = set()
RATGDONumber = ratgdo_ns.class_("RATGDONumber", number.Number, cg.Component)
NumberType = ratgdo_ns.enum("NumberType")
CONF_TYPE = "type"
TYPES = {
"client_id": NumberType.RATGDO_CLIENT_ID,
"rolling_code_counter": NumberType.RATGDO_ROLLING_CODE_COUNTER,
"opening_duration": NumberType.RATGDO_OPENING_DURATION,
"closing_duration": NumberType.RATGDO_CLOSING_DURATION,
"closing_delay": NumberType.RATGDO_CLOSING_DELAY,
"target_distance_measurement": NumberType.RATGDO_TARGET_DISTANCE_MEASUREMENT,
}
def validate_unique_type(config):
"""Validate that each number type is only used once."""
number_type = config[CONF_TYPE]
if number_type in USED_TYPES:
raise cv.Invalid(f"Only one number of type '{number_type}' is allowed")
USED_TYPES.add(number_type)
return config
CONFIG_SCHEMA = cv.All(
number.number_schema(RATGDONumber)
.extend(
{
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
}
)
.extend(RATGDO_CLIENT_SCHMEA),
validate_unique_type,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await number.register_number(var, config, step=1, min_value=0, max_value=4294967295)
await cg.register_component(var, config)
cg.add(var.set_number_type(config[CONF_TYPE]))
await register_ratgdo_child(var, config)
# Add defines for enabled features
# sensor will add the define for the distance sensor
if config[CONF_TYPE] == "closing_delay":
cg.add_define("RATGDO_USE_CLOSING_DELAY")
+186
View File
@@ -0,0 +1,186 @@
#include "ratgdo_number.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
using protocol::SetClientID;
using protocol::SetRollingCodeCounter;
float normalize_client_id(float client_id)
{
uint32_t int_value = static_cast<uint32_t>(client_id);
if ((int_value & 0xFFF) != 0x539) {
client_id = ceil((client_id - 0x539) / 0x1000) * 0x1000 + 0x539;
}
return client_id;
}
static const char* const TAG = "ratgdo.number";
void RATGDONumber::dump_config()
{
LOG_NUMBER("", "RATGDO Number", this);
switch (this->number_type_) {
case RATGDO_CLIENT_ID:
ESP_LOGCONFIG(TAG, " Type: Client ID");
break;
case RATGDO_ROLLING_CODE_COUNTER:
ESP_LOGCONFIG(TAG, " Type: Rolling Code Counter");
break;
case RATGDO_OPENING_DURATION:
ESP_LOGCONFIG(TAG, " Type: Opening Duration");
break;
case RATGDO_CLOSING_DURATION:
ESP_LOGCONFIG(TAG, " Type: Closing Duration");
break;
#ifdef RATGDO_USE_CLOSING_DELAY
case RATGDO_CLOSING_DELAY:
ESP_LOGCONFIG(TAG, " Type: Closing Delay");
break;
#endif
#ifdef RATGDO_USE_DISTANCE_SENSOR
case RATGDO_TARGET_DISTANCE_MEASUREMENT:
ESP_LOGCONFIG(TAG, " Type: Target Distance Measurement");
break;
#endif
default:
break;
}
}
void RATGDONumber::setup()
{
float value;
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (this->number_type_ == RATGDO_CLIENT_ID) {
value = ((random_uint32() + 1) % 0x7FF) << 12 | 0x539; // max size limited to be precisely convertible to float
} else {
value = 0;
}
} else {
if (this->number_type_ == RATGDO_CLIENT_ID) {
uint32_t int_value = static_cast<uint32_t>(value);
if ((int_value & 0xFFF) != 0x539) {
value = ((random_uint32() + 1) % 0x7FF) << 12 | 0x539; // max size limited to be precisely convertible to float
this->pref_.save(&value);
}
}
}
this->control(value);
switch (this->number_type_) {
case RATGDO_ROLLING_CODE_COUNTER:
this->parent_->subscribe_rolling_code_counter([this](uint32_t value) {
this->update_state(value);
});
break;
case RATGDO_OPENING_DURATION:
this->parent_->subscribe_opening_duration([this](float value) {
this->update_state(value);
});
break;
case RATGDO_CLOSING_DURATION:
this->parent_->subscribe_closing_duration([this](float value) {
this->update_state(value);
});
break;
#ifdef RATGDO_USE_CLOSING_DELAY
case RATGDO_CLOSING_DELAY:
this->parent_->subscribe_closing_delay([this](uint32_t value) {
this->update_state(value);
});
break;
#endif
#ifdef RATGDO_USE_DISTANCE_SENSOR
case RATGDO_TARGET_DISTANCE_MEASUREMENT:
// this->parent_->subscribe_target_distance_measurement([=](float value) {
// this->update_state(value);
// });
break;
#endif
default:
break;
}
}
void RATGDONumber::set_number_type(NumberType number_type_)
{
this->number_type_ = number_type_;
switch (this->number_type_) {
case RATGDO_OPENING_DURATION:
case RATGDO_CLOSING_DURATION:
this->traits.set_step(0.1);
this->traits.set_min_value(0.0);
this->traits.set_max_value(180.0);
break;
#ifdef RATGDO_USE_CLOSING_DELAY
case RATGDO_CLOSING_DELAY:
this->traits.set_step(1);
this->traits.set_min_value(0.0);
this->traits.set_max_value(60.0);
break;
#endif
case RATGDO_ROLLING_CODE_COUNTER:
this->traits.set_max_value(0xfffffff);
break;
case RATGDO_CLIENT_ID:
this->traits.set_step(0x1000);
this->traits.set_min_value(0x539);
this->traits.set_max_value(0x7ff539);
break;
#ifdef RATGDO_USE_DISTANCE_SENSOR
case RATGDO_TARGET_DISTANCE_MEASUREMENT:
this->traits.set_step(1);
this->traits.set_min_value(5);
this->traits.set_max_value(3500);
break;
#endif
default:
break;
}
}
void RATGDONumber::update_state(float value)
{
if (value == this->state) {
return;
}
this->pref_.save(&value);
this->publish_state(value);
}
void RATGDONumber::control(float value)
{
switch (this->number_type_) {
case RATGDO_ROLLING_CODE_COUNTER:
this->parent_->call_protocol(SetRollingCodeCounter { static_cast<uint32_t>(value) });
break;
case RATGDO_OPENING_DURATION:
this->parent_->set_opening_duration(value);
break;
case RATGDO_CLOSING_DURATION:
this->parent_->set_closing_duration(value);
break;
#ifdef RATGDO_USE_CLOSING_DELAY
case RATGDO_CLOSING_DELAY:
this->parent_->set_closing_delay(value);
break;
#endif
case RATGDO_CLIENT_ID:
value = normalize_client_id(value);
this->parent_->call_protocol(SetClientID { static_cast<uint32_t>(value) });
break;
#ifdef RATGDO_USE_DISTANCE_SENSOR
case RATGDO_TARGET_DISTANCE_MEASUREMENT:
this->parent_->set_target_distance_measurement(value);
break;
#endif
default:
break;
}
this->update_state(value);
}
} // namespace esphome::ratgdo
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/number/number.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome::ratgdo {
enum NumberType {
RATGDO_CLIENT_ID,
RATGDO_ROLLING_CODE_COUNTER,
RATGDO_OPENING_DURATION,
RATGDO_CLOSING_DURATION,
#ifdef RATGDO_USE_CLOSING_DELAY
RATGDO_CLOSING_DELAY,
#endif
#ifdef RATGDO_USE_DISTANCE_SENSOR
RATGDO_TARGET_DISTANCE_MEASUREMENT,
#endif
};
class RATGDONumber : public number::Number, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
void set_number_type(NumberType number_type);
// other esphome components that persist state in the flash have HARDWARE priority
// ensure we get initialized before them, so that the state doesn't get invalidated
// by components that might be added in the future
float get_setup_priority() const override { return setup_priority::HARDWARE + 1; }
void update_state(float value);
void control(float value) override;
protected:
NumberType number_type_;
ESPPreferenceObject pref_;
};
} // namespace esphome::ratgdo
+24
View File
@@ -0,0 +1,24 @@
#include "observable.h"
#include "callbacks.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo.observable";
void log_multiple_subscribers()
{
ESP_LOGE(TAG, "single_observable already has a subscriber! This will overwrite the existing subscriber.");
}
void log_observer_overflow()
{
ESP_LOGE(TAG, "observable has too many subscribers! Ignoring new subscriber.");
}
void log_once_callbacks_overflow(uint8_t max)
{
ESP_LOGE(TAG, "OnceCallbacks overflow (max %u)! Ignoring callback.", static_cast<unsigned>(max));
}
} // namespace esphome::ratgdo
+167
View File
@@ -0,0 +1,167 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <new>
#include <type_traits>
#include <utility>
namespace esphome::ratgdo {
void log_multiple_subscribers();
void log_observer_overflow();
// Lightweight type-erased callback (16 bytes on 32-bit).
// For small trivially-copyable callables (like [this], [this, f], or [this, f, id] lambdas),
// stores the callable inline — zero heap allocation.
// Supports up to 3 * sizeof(void*) bytes (12 bytes on 32-bit, 24 on 64-bit).
inline constexpr size_t CALLBACK_STORAGE_SIZE = 3 * sizeof(void*);
template <typename... Ts>
struct Callback {
using fn_t = void (*)(const void*, Ts...);
fn_t fn_ { nullptr };
alignas(void*) uint8_t storage_[CALLBACK_STORAGE_SIZE] { };
void call(Ts... args) const { this->fn_(this->storage_, args...); }
explicit operator bool() const { return this->fn_ != nullptr; }
template <typename F>
static Callback create(F&& f)
{
Callback cb;
using Decay = std::decay_t<F>;
static_assert(!std::is_function_v<std::remove_reference_t<F>>,
"Pass function pointers, not function references");
static_assert(std::is_trivially_copyable_v<Decay>, "Observable callbacks must be trivially copyable (e.g. [this] lambdas)");
static_assert(sizeof(Decay) <= CALLBACK_STORAGE_SIZE, "Observable callbacks must fit in storage (capture at most 3 pointers)");
cb.fn_ = [](const void* storage, Ts... args) {
alignas(Decay) char buf[sizeof(Decay)];
__builtin_memcpy(buf, storage, sizeof(Decay));
(*std::launder(reinterpret_cast<Decay*>(buf)))(args...);
};
__builtin_memcpy(cb.storage_, &f, sizeof(Decay));
return cb;
}
};
// Primary template for observable with subscribers.
template <typename T, uint8_t MaxObservers>
class observable {
public:
observable(const T& value)
: value_(value)
{
}
template <typename U>
observable& operator=(U value)
{
if (value != this->value_) {
this->value_ = value;
this->notify();
}
return *this;
}
T const* operator&() const { return &this->value_; }
T const& operator*() const { return this->value_; }
template <typename F>
void subscribe(F&& observer)
{
if (this->count_ >= MaxObservers) {
log_observer_overflow();
return;
}
this->observers_[this->count_++] = Callback<T>::create(std::forward<F>(observer));
}
void notify() const
{
for (uint8_t i = 0; i < this->count_; i++) {
this->observers_[i].call(this->value_);
}
}
private:
T value_;
Callback<T> observers_[MaxObservers] { };
uint8_t count_ { 0 };
};
// Specialization for zero subscribers — no array, no count, notify is a no-op.
template <typename T>
class observable<T, 0> {
public:
observable(const T& value)
: value_(value)
{
}
template <typename U>
observable& operator=(U value)
{
if (value != this->value_) {
this->value_ = value;
}
return *this;
}
T const* operator&() const { return &this->value_; }
T const& operator*() const { return this->value_; }
template <typename F>
void subscribe(F&&)
{
log_observer_overflow();
}
void notify() const { }
private:
T value_;
};
template <typename T>
class single_observable {
public:
single_observable(const T& value)
: value_(value)
{
}
template <typename U>
single_observable& operator=(U value)
{
if (value != this->value_) {
this->value_ = value;
this->notify();
}
return *this;
}
T const* operator&() const { return &this->value_; }
T const& operator*() const { return this->value_; }
template <typename F>
void subscribe(F&& observer)
{
if (this->observer_) {
log_multiple_subscribers();
}
this->observer_ = Callback<T>::create(std::forward<F>(observer));
}
void notify() const
{
if (this->observer_) {
this->observer_.call(this->value_);
}
}
private:
T value_;
Callback<T> observer_ { };
};
} // namespace esphome::ratgdo
+43
View File
@@ -0,0 +1,43 @@
import esphome.codegen as cg
from esphome.components import rtttl
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import (
RATGDO_CLIENT_SCHMEA,
ratgdo_ns,
register_ratgdo_child,
subscribe_door_action_delayed,
subscribe_vehicle_arriving,
)
CONF_RTTTL = "rtttl"
CONF_SONG = "song"
DEPENDENCIES = ["esp32", "ratgdo", "rtttl"]
RATGDOOutput = ratgdo_ns.class_("RATGDOOutput", cg.Component)
OutputType = ratgdo_ns.enum("OutputType")
CONF_TYPE = "type"
TYPES = {"beeper": OutputType.RATGDO_BEEPER}
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(RATGDOOutput),
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
cv.Required(CONF_RTTTL): cv.use_id(rtttl),
cv.Required(CONF_SONG): cv.string,
}
).extend(RATGDO_CLIENT_SCHMEA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
rtttl = await cg.get_variable(config[CONF_RTTTL])
cg.add(var.set_rtttl(rtttl))
cg.add(var.set_song(config[CONF_SONG]))
await register_ratgdo_child(var, config)
subscribe_vehicle_arriving()
subscribe_door_action_delayed()
@@ -0,0 +1,58 @@
#include "ratgdo_output.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* TAG = "ratgdo.output";
void RATGDOOutput::setup()
{
ESP_LOGD(TAG, "Output was setup");
if (this->output_type_ == OutputType::RATGDO_BEEPER) {
this->beeper_->add_on_finished_playback_callback([this] { this->finished_playback(); });
#ifdef RATGDO_USE_VEHICLE_SENSORS
this->parent_->subscribe_vehicle_arriving_state([this](VehicleArrivingState state) {
if (state == VehicleArrivingState::YES) {
this->play();
}
});
#endif
this->parent_->subscribe_door_action_delayed([this](DoorActionDelayed state) {
if (state == DoorActionDelayed::YES) {
this->play();
this->repeat_ = true;
} else if (state == DoorActionDelayed::NO) {
this->repeat_ = false;
}
});
}
}
void RATGDOOutput::play()
{
this->beeper_->play(this->rtttlSong_);
}
void RATGDOOutput::finished_playback()
{
if (this->repeat_)
this->play();
}
void RATGDOOutput::dump_config()
{
if (this->output_type_ == OutputType::RATGDO_BEEPER) {
ESP_LOGCONFIG(TAG, " Type: Beeper");
}
}
void RATGDOOutput::set_output_type(OutputType output_type_)
{
this->output_type_ = output_type_;
}
} // namespace esphome::ratgdo
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "../ratgdo.h"
#include "esphome/components/rtttl/rtttl.h"
#include "esphome/core/component.h"
namespace esphome::ratgdo {
enum OutputType {
RATGDO_BEEPER
};
class RATGDOOutput : public RATGDOClient, public Component {
public:
void setup() override;
void play();
void finished_playback();
void dump_config() override;
void set_output_type(OutputType output_type);
void set_song(std::string rtttlSong) { this->rtttlSong_ = rtttlSong; }
void set_rtttl(rtttl::Rtttl* output) { this->beeper_ = output; }
protected:
OutputType output_type_;
rtttl::Rtttl* beeper_;
std::string rtttlSong_;
bool repeat_;
};
} // namespace esphome::ratgdo
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#include "common.h"
#include "ratgdo_state.h"
namespace esphome {
class Scheduler;
class InternalGPIOPin;
} // namespace esphome
namespace esphome::ratgdo {
class RATGDOComponent;
namespace protocol {
const uint32_t HAS_DOOR_OPEN = 1 << 0; // has idempotent open door command
const uint32_t HAS_DOOR_CLOSE = 1 << 1; // has idempotent close door command
const uint32_t HAS_DOOR_STOP = 1 << 2; // has idempotent stop door command
const uint32_t HAS_DOOR_STATUS = 1 << 3;
const uint32_t HAS_LIGHT_TOGGLE = 1 << 10; // some protocols might not support this
const uint32_t HAS_LOCK_TOGGLE = 1 << 20;
class Traits {
uint32_t value;
public:
Traits()
: value(0)
{
}
bool has_door_open() const { return this->value & HAS_DOOR_OPEN; }
bool has_door_close() const { return this->value & HAS_DOOR_CLOSE; }
bool has_door_stop() const { return this->value & HAS_DOOR_STOP; }
bool has_door_status() const { return this->value & HAS_DOOR_STATUS; }
bool has_light_toggle() const { return this->value & HAS_LIGHT_TOGGLE; }
bool has_lock_toggle() const { return this->value & HAS_LOCK_TOGGLE; }
void set_features(uint32_t feature) { this->value |= feature; }
void clear_features(uint32_t feature) { this->value &= ~feature; }
static uint32_t all()
{
return HAS_DOOR_CLOSE | HAS_DOOR_OPEN | HAS_DOOR_STOP | HAS_DOOR_STATUS | HAS_LIGHT_TOGGLE | HAS_LOCK_TOGGLE;
}
};
struct SetRollingCodeCounter {
uint32_t counter;
};
struct GetRollingCodeCounter {
};
struct SetClientID {
uint64_t client_id;
};
struct QueryStatus {
};
struct QueryOpenings {
};
struct ActivateLearn {
};
struct InactivateLearn {
};
struct QueryPairedDevices {
PairedDevice kind;
};
struct QueryPairedDevicesAll {
};
struct ClearPairedDevices {
PairedDevice kind;
};
// a poor man's sum-type, because C++
SUM_TYPE(Args,
(SetRollingCodeCounter, set_rolling_code_counter),
(GetRollingCodeCounter, get_rolling_code_counter),
(SetClientID, set_client_id),
(QueryStatus, query_status),
(QueryOpenings, query_openings),
(ActivateLearn, activate_learn),
(InactivateLearn, inactivate_learn),
(QueryPairedDevices, query_paired_devices),
(QueryPairedDevicesAll, query_paired_devices_all),
(ClearPairedDevices, clear_paired_devices), )
struct RollingCodeCounter {
single_observable<uint32_t>* value;
};
SUM_TYPE(Result,
(RollingCodeCounter, rolling_code_counter), )
class Protocol {
public:
virtual void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
virtual void loop();
virtual void dump_config();
virtual void on_shutdown() { }
virtual void sync();
// dry contact methods
virtual void set_open_limit(bool);
virtual void set_close_limit(bool);
virtual void set_discrete_open_pin(InternalGPIOPin* pin);
virtual void set_discrete_close_pin(InternalGPIOPin* pin);
virtual const Traits& traits() const;
virtual void light_action(LightAction action);
virtual void lock_action(LockAction action);
virtual void door_action(DoorAction action);
virtual protocol::Result call(protocol::Args args);
};
}
} // namespace esphome::ratgdo
+817
View File
@@ -0,0 +1,817 @@
/************************************
* Rage
* Against
* The
* Garage
* Door
* Opener
*
* Copyright (C) 2022 Paul Wieland
*
* GNU GENERAL PUBLIC LICENSE
************************************/
#include "ratgdo.h"
#include "common.h"
#include "ratgdo_state.h"
#ifdef PROTOCOL_DRYCONTACT
#include "dry_contact.h"
#endif
#ifdef PROTOCOL_SECPLUSV1
#include "secplus1.h"
#endif
#ifdef PROTOCOL_SECPLUSV2
#include "secplus2.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
using namespace protocol;
static const char* const TAG = "ratgdo";
static constexpr int SYNC_DELAY = 1000;
// Door state updates arrive over UART every ~200-400ms during movement.
// 2 seconds gives ample margin for slow openers while still expiring
// stale callbacks before a user could reasonably trigger an unrelated
// door state change.
static constexpr uint32_t DOOR_STATE_CALLBACK_TIMEOUT = 2000;
using namespace scheduler_ids;
void log_subscriber_overflow(const LogString* observable_name, uint32_t max)
{
ESP_LOGE(TAG, "Too many subscribers for %s (max %d)",
LOG_STR_ARG(observable_name), (int)max);
}
#ifdef RATGDO_USE_VEHICLE_SENSORS
static constexpr int CLEAR_PRESENCE = 60000; // how long to keep arriving/leaving active
static constexpr int PRESENCE_DETECT_WINDOW = 300000; // how long to calculate presence after door state change
static constexpr int PRESENCE_DETECT_WINDOW_AFTER_CLOSE = 15000; // how long to keep presence window active after door reaches closed
// increasing these values increases reliability but also increases detection
// time
static constexpr int PRESENCE_DETECTION_ON_THRESHOLD = 5; // Minimum percentage of valid bitset::in_range samples required to
// detect vehicle
static constexpr int PRESENCE_DETECTION_OFF_DEBOUNCE = 2; // The number of consecutive bitset::in_range iterations that must be 0
// before clearing vehicle detected state
#endif
void RATGDOComponent::setup()
{
this->output_gdo_pin_->setup();
this->output_gdo_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->input_gdo_pin_->setup();
this->input_gdo_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->input_obst_pin_->setup();
#ifdef USE_ESP32
this->input_obst_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
#else
this->input_obst_pin_->pin_mode(gpio::FLAG_INPUT);
#endif
this->input_obst_pin_->attach_interrupt(RATGDOStore::isr_obstruction,
&this->isr_store_,
gpio::INTERRUPT_FALLING_EDGE);
this->protocol_->setup(this, &App.scheduler, this->input_gdo_pin_,
this->output_gdo_pin_);
// many things happening at startup, use some delay for sync
this->set_timeout(SYNC_DELAY, [this] { this->sync(); });
ESP_LOGD(TAG, " _____ _____ _____ _____ ____ _____ ");
ESP_LOGD(TAG, "| __ | _ |_ _| __| \\| |");
ESP_LOGD(TAG, "| -| | | | | | | | | | |");
ESP_LOGD(TAG, "|__|__|__|__| |_| |_____|____/|_____|");
ESP_LOGD(TAG, "https://paulwieland.github.io/ratgdo/");
this->subscribe_door_state([this](DoorState state, float position) {
#ifdef RATGDO_USE_VEHICLE_SENSORS
if (this->last_door_state_for_presence_ != DoorState::UNKNOWN && state != DoorState::CLOSED && !this->flags_.presence_detect_window_active) {
this->flags_.presence_detect_window_active = true;
this->set_timeout(
TIMEOUT_PRESENCE_DETECT_WINDOW, PRESENCE_DETECT_WINDOW,
[this] { this->flags_.presence_detect_window_active = false; });
}
if (state == DoorState::CLOSED) {
this->set_timeout(
TIMEOUT_PRESENCE_DETECT_WINDOW, PRESENCE_DETECT_WINDOW_AFTER_CLOSE,
[this] { this->flags_.presence_detect_window_active = false; });
}
this->last_door_state_for_presence_ = state;
#endif
});
}
// initializing protocol, this gets called before setup() because
// its children components might require that
void RATGDOComponent::init_protocol()
{
#ifdef PROTOCOL_SECPLUSV2
this->protocol_ = new secplus2::Secplus2();
#endif
#ifdef PROTOCOL_SECPLUSV1
this->protocol_ = new secplus1::Secplus1();
#endif
#ifdef PROTOCOL_DRYCONTACT
this->protocol_ = new dry_contact::DryContact();
#endif
}
void RATGDOComponent::loop()
{
// obstruction_loop() must run before protocol_->loop() because it uses
// App.get_loop_component_start_time() and protocol_->loop() may block
// for up to 1.3ms (secplus2 transmit collision wait), which would make
// the cached timestamp stale.
this->obstruction_loop();
this->protocol_->loop();
}
void RATGDOComponent::dump_config()
{
ESP_LOGCONFIG(TAG, "Setting up RATGDO...");
LOG_PIN(" Output GDO Pin: ", this->output_gdo_pin_);
LOG_PIN(" Input GDO Pin: ", this->input_gdo_pin_);
LOG_PIN(" Input Obstruction Pin: ", this->input_obst_pin_);
this->protocol_->dump_config();
}
void RATGDOComponent::on_shutdown()
{
if (this->protocol_ != nullptr) {
this->protocol_->on_shutdown();
}
}
void RATGDOComponent::received(const DoorState door_state)
{
ESP_LOGD(TAG, "Door state=%s", LOG_STR_ARG(DoorState_to_string(door_state)));
auto prev_door_state = *this->door_state;
if (prev_door_state == door_state) {
return;
}
// opening duration calibration
if (*this->opening_duration == 0) {
if (door_state == DoorState::OPENING && prev_door_state == DoorState::CLOSED) {
this->start_opening = millis();
}
if (door_state == DoorState::OPEN && prev_door_state == DoorState::OPENING && this->start_opening > 0) {
auto duration = (millis() - this->start_opening) / 1000;
this->set_opening_duration(round(duration * 10) / 10);
}
if (door_state == DoorState::STOPPED) {
this->start_opening = -1;
}
}
// closing duration calibration
if (*this->closing_duration == 0) {
if (door_state == DoorState::CLOSING && prev_door_state == DoorState::OPEN) {
this->start_closing = millis();
}
if (door_state == DoorState::CLOSED && prev_door_state == DoorState::CLOSING && this->start_closing > 0) {
auto duration = (millis() - this->start_closing) / 1000;
this->set_closing_duration(round(duration * 10) / 10);
}
if (door_state == DoorState::STOPPED) {
this->start_closing = -1;
}
}
if (door_state == DoorState::OPENING) {
// door started opening
if (prev_door_state == DoorState::CLOSING) {
this->door_position_update();
this->cancel_position_sync_callbacks();
this->door_move_delta = DOOR_DELTA_UNKNOWN;
}
this->door_start_moving = millis();
this->door_start_position = *this->door_position;
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
this->door_move_delta = 1.0 - this->door_start_position;
}
if (*this->opening_duration != 0) {
this->schedule_door_position_sync();
}
} else if (door_state == DoorState::CLOSING) {
// door started closing
if (prev_door_state == DoorState::OPENING) {
this->door_position_update();
this->cancel_position_sync_callbacks();
this->door_move_delta = DOOR_DELTA_UNKNOWN;
}
this->door_start_moving = millis();
this->door_start_position = *this->door_position;
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
this->door_move_delta = 0.0 - this->door_start_position;
}
if (*this->closing_duration != 0) {
this->schedule_door_position_sync();
}
} else if (door_state == DoorState::STOPPED) {
this->door_position_update();
if (*this->door_position == DOOR_POSITION_UNKNOWN) {
this->door_position = 0.5; // best guess
}
this->cancel_position_sync_callbacks();
this->cancel_timeout(TIMEOUT_DOOR_QUERY_STATE);
} else if (door_state == DoorState::OPEN) {
this->door_position = 1.0;
this->cancel_position_sync_callbacks();
} else if (door_state == DoorState::CLOSED) {
this->door_position = 0.0;
this->cancel_position_sync_callbacks();
}
if (door_state == DoorState::OPEN || door_state == DoorState::CLOSED || door_state == DoorState::STOPPED) {
this->motor_state = MotorState::OFF;
}
if (door_state == DoorState::CLOSED && door_state != prev_door_state) {
this->query_openings();
}
this->door_state = door_state;
this->on_door_state_.trigger(door_state);
}
void RATGDOComponent::received(const LearnState learn_state)
{
ESP_LOGD(TAG, "Learn state=%s",
LOG_STR_ARG(LearnState_to_string(learn_state)));
if (*this->learn_state == learn_state) {
return;
}
if (learn_state == LearnState::INACTIVE) {
this->query_paired_devices();
}
this->learn_state = learn_state;
}
void RATGDOComponent::received(const LightState light_state)
{
ESP_LOGD(TAG, "Light state=%s",
LOG_STR_ARG(LightState_to_string(light_state)));
this->light_state = light_state;
}
void RATGDOComponent::received(const LockState lock_state)
{
ESP_LOGD(TAG, "Lock state=%s", LOG_STR_ARG(LockState_to_string(lock_state)));
this->lock_state = lock_state;
}
void RATGDOComponent::received(const ObstructionState obstruction_state)
{
if (!this->flags_.obstruction_sensor_detected) {
ESP_LOGD(TAG, "Obstruction: state=%s",
LOG_STR_ARG(ObstructionState_to_string(*this->obstruction_state)));
this->obstruction_state = obstruction_state;
// This isn't very fast to update, but its still better
// than nothing in the case the obstruction sensor is not
// wired up.
}
}
void RATGDOComponent::received(const MotorState motor_state)
{
ESP_LOGD(TAG, "Motor: state=%s",
LOG_STR_ARG(MotorState_to_string(*this->motor_state)));
this->motor_state = motor_state;
}
void RATGDOComponent::received(const ButtonState button_state)
{
ESP_LOGD(TAG, "Button state=%s",
LOG_STR_ARG(ButtonState_to_string(*this->button_state)));
this->button_state = button_state;
}
void RATGDOComponent::received(const MotionState motion_state)
{
ESP_LOGD(TAG, "Motion: %s",
LOG_STR_ARG(MotionState_to_string(*this->motion_state)));
this->motion_state = motion_state;
if (motion_state == MotionState::DETECTED) {
this->set_timeout(TIMEOUT_CLEAR_MOTION, 3000,
[this] { this->motion_state = MotionState::CLEAR; });
if (*this->light_state == LightState::OFF) {
this->query_status();
}
}
}
void RATGDOComponent::received(const LightAction light_action)
{
ESP_LOGD(TAG, "Light cmd=%s state=%s",
LOG_STR_ARG(LightAction_to_string(light_action)),
LOG_STR_ARG(LightState_to_string(*this->light_state)));
if (light_action == LightAction::OFF) {
this->light_state = LightState::OFF;
} else if (light_action == LightAction::ON) {
this->light_state = LightState::ON;
} else if (light_action == LightAction::TOGGLE) {
this->light_state = light_state_toggle(*this->light_state);
}
}
void RATGDOComponent::received(const Openings openings)
{
if (openings.flag == 0 || *this->openings != 0) {
this->openings = openings.count;
ESP_LOGD(TAG, "Openings: %d", *this->openings);
} else {
ESP_LOGD(TAG, "Ignoring openings, not from our request");
}
}
void RATGDOComponent::received(const PairedDeviceCount pdc)
{
ESP_LOGD(TAG, "Paired device count, kind=%s count=%d",
LOG_STR_ARG(PairedDevice_to_string(pdc.kind)), pdc.count);
if (pdc.kind == PairedDevice::ALL) {
this->paired_total = pdc.count;
} else if (pdc.kind == PairedDevice::REMOTE) {
this->paired_remotes = pdc.count;
} else if (pdc.kind == PairedDevice::KEYPAD) {
this->paired_keypads = pdc.count;
} else if (pdc.kind == PairedDevice::WALL_CONTROL) {
this->paired_wall_controls = pdc.count;
} else if (pdc.kind == PairedDevice::ACCESSORY) {
this->paired_accessories = pdc.count;
}
}
void RATGDOComponent::received(const TimeToClose ttc)
{
ESP_LOGD(TAG, "Time to close (TTC): %ds", ttc.seconds);
}
void RATGDOComponent::received(const BatteryState battery_state)
{
ESP_LOGD(TAG, "Battery state=%s",
LOG_STR_ARG(BatteryState_to_string(battery_state)));
}
void RATGDOComponent::schedule_door_position_sync(float update_period)
{
ESP_LOG1(
TAG,
"Schedule position sync: delta %f, start position: %f, start moving: %d",
this->door_move_delta, this->door_start_position,
this->door_start_moving);
auto duration = this->door_move_delta > 0 ? *this->opening_duration
: *this->closing_duration;
if (duration == 0) {
return;
}
this->position_sync_remaining_ = std::max(static_cast<uint16_t>(1000 * duration / update_period),
static_cast<uint16_t>(1));
set_interval(INTERVAL_POSITION_SYNC, static_cast<uint32_t>(update_period),
[this]() {
this->door_position_update();
if (--this->position_sync_remaining_ == 0) {
cancel_interval(INTERVAL_POSITION_SYNC);
}
});
}
void RATGDOComponent::door_position_update()
{
if (this->door_start_moving == 0 || this->door_start_position == DOOR_POSITION_UNKNOWN || this->door_move_delta == DOOR_DELTA_UNKNOWN) {
return;
}
auto now = millis();
auto duration = this->door_move_delta > 0 ? *this->opening_duration
: -*this->closing_duration;
if (duration == 0) {
return;
}
auto position = this->door_start_position + (now - this->door_start_moving) / (1000 * duration);
ESP_LOG2(TAG, "[%d] Position update: %f", now, position);
this->door_position = clamp(position, 0.0f, 1.0f);
}
void RATGDOComponent::set_opening_duration(float duration)
{
ESP_LOGD(TAG, "Set opening duration: %.1fs", duration);
this->opening_duration = duration;
}
void RATGDOComponent::set_closing_duration(float duration)
{
ESP_LOGD(TAG, "Set closing duration: %.1fs", duration);
this->closing_duration = duration;
}
#ifdef RATGDO_USE_DISTANCE_SENSOR
void RATGDOComponent::set_target_distance_measurement(int16_t distance)
{
this->target_distance_measurement = distance;
}
void RATGDOComponent::set_distance_measurement(int16_t distance)
{
this->last_distance_measurement = distance;
#ifdef RATGDO_USE_VEHICLE_SENSORS
this->in_range <<= 1;
this->in_range.set(0, distance <= *this->target_distance_measurement);
this->calculate_presence();
#endif
}
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
void RATGDOComponent::calculate_presence()
{
int percent = this->in_range.count() * 100 / this->in_range.size();
if (percent >= PRESENCE_DETECTION_ON_THRESHOLD)
this->vehicle_detected_state = VehicleDetectedState::YES;
if (percent == 0 && *this->vehicle_detected_state == VehicleDetectedState::YES) {
this->presence_off_counter_++;
ESP_LOGD(TAG, "Off counter: %d", this->presence_off_counter_);
if (this->presence_off_counter_ / this->in_range.size() >= PRESENCE_DETECTION_OFF_DEBOUNCE) {
this->presence_off_counter_ = 0;
this->vehicle_detected_state = VehicleDetectedState::NO;
}
}
if (percent != this->last_presence_percent_) {
ESP_LOGD(TAG, "pct_in_range: %d", percent);
this->last_presence_percent_ = percent;
this->presence_off_counter_ = 0;
}
// ESP_LOGD(TAG, "in_range: %s", this->in_range.to_string().c_str());
}
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
void RATGDOComponent::presence_change(bool sensor_value)
{
if (this->flags_.presence_detect_window_active) {
// Arriving and leaving are mutually exclusive — each branch clears the
// other state. Sharing TIMEOUT_CLEAR_PRESENCE ensures that switching from
// arriving to leaving (or vice versa) cancels the previous clear timeout,
// which is correct since the previous state was already cleared above.
if (sensor_value) {
this->vehicle_arriving_state = VehicleArrivingState::YES;
this->vehicle_leaving_state = VehicleLeavingState::NO;
this->set_timeout(TIMEOUT_CLEAR_PRESENCE, CLEAR_PRESENCE, [this] {
this->vehicle_arriving_state = VehicleArrivingState::NO;
});
} else {
this->vehicle_arriving_state = VehicleArrivingState::NO;
this->vehicle_leaving_state = VehicleLeavingState::YES;
this->set_timeout(TIMEOUT_CLEAR_PRESENCE, CLEAR_PRESENCE, [this] {
this->vehicle_leaving_state = VehicleLeavingState::NO;
});
}
// if the door is closed, clear the presence detect window since a vehicle
// can't be arriving or leaving with the door shut
if (*this->door_state == DoorState::CLOSED) {
this->flags_.presence_detect_window_active = false;
this->cancel_timeout(TIMEOUT_PRESENCE_DETECT_WINDOW);
}
}
}
#endif
Result RATGDOComponent::call_protocol(Args args)
{
return this->protocol_->call(args);
}
/*************************** OBSTRUCTION DETECTION ***************************/
void RATGDOComponent::obstruction_loop()
{
// Safe to use cached loop timestamp here because obstruction_loop()
// runs before protocol_->loop() which contains the 1.3ms blocking
// transmit in secplus2. The 50ms CHECK_PERIOD has ample margin.
const uint32_t current_millis = App.get_loop_component_start_time();
static uint32_t last_millis = 0;
static uint32_t last_asleep = 0;
// the obstruction sensor has 3 states: clear (HIGH with LOW pulse every 7ms),
// obstructed (HIGH), asleep (LOW) the transitions between awake and asleep
// are tricky because the voltage drops slowly when falling asleep and is high
// without pulses when waking up
// If at least 3 low pulses are counted within 50ms, the door is awake, not
// obstructed and we don't have to check anything else
constexpr uint32_t CHECK_PERIOD = 50;
constexpr uint32_t PULSES_LOWER_LIMIT = 3;
if (current_millis - last_millis > CHECK_PERIOD) {
// ESP_LOGD(TAG, "%ld: Obstruction count: %d, expected: %d, since asleep:
// %ld",
// current_millis, this->isr_store_.obstruction_low_count,
// PULSES_LOWER_LIMIT, current_millis - last_asleep
// );
// check to see if we got more then PULSES_LOWER_LIMIT pulses
if (this->isr_store_.obstruction_low_count > PULSES_LOWER_LIMIT) {
this->obstruction_state = ObstructionState::CLEAR;
this->flags_.obstruction_sensor_detected = true;
} else if (this->isr_store_.obstruction_low_count == 0) {
// if there have been no pulses the line is steady high or low
if (this->input_obst_pin_->digital_read() != this->flags_.obst_sleep_low) {
// asleep
last_asleep = current_millis;
} else {
// if the line is high and was last asleep more than 700ms ago, then
// there is an obstruction present
if (current_millis - last_asleep > 700) {
this->obstruction_state = ObstructionState::OBSTRUCTED;
}
}
}
last_millis = current_millis;
this->isr_store_.obstruction_low_count = 0;
}
}
void RATGDOComponent::query_status() { this->protocol_->call(QueryStatus { }); }
void RATGDOComponent::query_openings()
{
this->protocol_->call(QueryOpenings { });
}
void RATGDOComponent::query_paired_devices()
{
this->protocol_->call(QueryPairedDevicesAll { });
}
void RATGDOComponent::query_paired_devices(PairedDevice kind)
{
this->protocol_->call(QueryPairedDevices { kind });
}
void RATGDOComponent::clear_paired_devices(PairedDevice kind)
{
this->protocol_->call(ClearPairedDevices { kind });
}
void RATGDOComponent::sync()
{
this->protocol_->sync();
// dry contact protocol:
// needed to trigger the intial state of the limit switch sensors
// ideally this would be in drycontact::sync
#ifdef PROTOCOL_DRYCONTACT
this->protocol_->set_open_limit(this->dry_contact_open_sensor_->state);
this->protocol_->set_close_limit(this->dry_contact_close_sensor_->state);
#endif
}
void RATGDOComponent::set_door_state_expiry()
{
this->set_timeout(TIMEOUT_DOOR_STATE_EXPIRY, DOOR_STATE_CALLBACK_TIMEOUT,
[this]() {
ESP_LOGW(TAG, "Door state callback expired, clearing");
this->on_door_state_.clear();
});
}
void RATGDOComponent::cancel_door_state_expiry()
{
this->cancel_timeout(TIMEOUT_DOOR_STATE_EXPIRY);
}
void RATGDOComponent::door_open()
{
if (*this->door_state == DoorState::OPENING) {
return; // gets ignored by opener
}
this->door_action(DoorAction::OPEN);
if (*this->opening_duration > 0) {
// query state in case we don't get a status message
this->set_timeout(
TIMEOUT_DOOR_QUERY_STATE, (*this->opening_duration + 2) * 1000,
[this]() {
if (*this->door_state != DoorState::OPEN && *this->door_state != DoorState::STOPPED) {
this->received(DoorState::OPEN); // probably missed a status mesage,
// assume it's open
this->query_status(); // query in case we're wrong and it's stopped
}
});
}
}
void RATGDOComponent::door_close()
{
if (*this->door_state == DoorState::CLOSING) {
return; // gets ignored by opener
}
if (*this->door_state == DoorState::OPENING) {
// have to stop door first, otherwise close command is ignored
this->door_action(DoorAction::STOP);
this->on_door_state([this](DoorState s) {
if (s == DoorState::STOPPED) {
this->door_action(DoorAction::CLOSE);
} else {
ESP_LOGW(TAG, "Door did not stop, ignoring close command");
}
});
return;
}
if (this->flags_.obstruction_sensor_detected) {
this->door_action(DoorAction::CLOSE);
} else if (*this->door_state == DoorState::OPEN) {
ESP_LOGD(TAG, "No obstruction sensors detected. Close using TOGGLE.");
this->door_action(DoorAction::TOGGLE);
}
if (*this->closing_duration > 0) {
// query state in case we don't get a status message
this->set_timeout(
TIMEOUT_DOOR_QUERY_STATE, (*this->closing_duration + 2) * 1000,
[this]() {
if (*this->door_state != DoorState::CLOSED && *this->door_state != DoorState::STOPPED) {
this->received(DoorState::CLOSED); // probably missed a status
// mesage, assume it's closed
this->query_status(); // query in case we're wrong and it's stopped
}
});
}
}
void RATGDOComponent::door_stop()
{
if (*this->door_state != DoorState::OPENING && *this->door_state != DoorState::CLOSING) {
ESP_LOGW(TAG, "The door is not moving.");
return;
}
this->door_action(DoorAction::STOP);
}
void RATGDOComponent::door_toggle() { this->door_action(DoorAction::TOGGLE); }
void RATGDOComponent::door_action(DoorAction action)
{
#ifdef RATGDO_USE_CLOSING_DELAY
if (*this->closing_delay > 0 && (action == DoorAction::CLOSE || (action == DoorAction::TOGGLE && *this->door_state != DoorState::CLOSED))) {
this->door_action_delayed = DoorActionDelayed::YES;
this->set_timeout(TIMEOUT_DOOR_ACTION, *this->closing_delay * 1000, [this] {
this->door_action_delayed = DoorActionDelayed::NO;
this->protocol_->door_action(DoorAction::CLOSE);
});
} else {
this->protocol_->door_action(action);
}
#else
this->protocol_->door_action(action);
#endif
}
void RATGDOComponent::door_move_to_position(float position)
{
if (*this->door_state == DoorState::OPENING || *this->door_state == DoorState::CLOSING) {
this->door_action(DoorAction::STOP);
this->on_door_state([this, position](DoorState s) {
if (s == DoorState::STOPPED) {
this->door_move_to_position(position);
}
});
return;
}
auto delta = position - *this->door_position;
if (delta == 0) {
ESP_LOGD(TAG, "Door is already at position %.2f", position);
return;
}
auto duration = delta > 0 ? *this->opening_duration : -*this->closing_duration;
if (duration == 0) {
ESP_LOGW(TAG, "I don't know duration, ignoring move to position");
return;
}
auto operation_time = 1000 * duration * delta;
this->door_move_delta = delta;
ESP_LOGD(TAG, "Moving to position %.2f in %.1fs", position,
operation_time / 1000.0);
this->door_action(delta > 0 ? DoorAction::OPEN : DoorAction::CLOSE);
this->set_timeout(TIMEOUT_MOVE_TO_POSITION, operation_time,
[this] { this->door_action(DoorAction::STOP); });
}
void RATGDOComponent::cancel_position_sync_callbacks()
{
if (this->door_start_moving != 0) {
ESP_LOGD(TAG, "Cancelling position callbacks");
this->cancel_timeout(TIMEOUT_MOVE_TO_POSITION);
cancel_interval(INTERVAL_POSITION_SYNC);
this->door_start_moving = 0;
this->door_start_position = DOOR_POSITION_UNKNOWN;
this->door_move_delta = DOOR_DELTA_UNKNOWN;
}
}
void RATGDOComponent::light_on()
{
this->light_state = LightState::ON;
this->protocol_->light_action(LightAction::ON);
}
void RATGDOComponent::light_off()
{
this->light_state = LightState::OFF;
this->protocol_->light_action(LightAction::OFF);
}
void RATGDOComponent::light_toggle()
{
this->light_state = light_state_toggle(*this->light_state);
this->protocol_->light_action(LightAction::TOGGLE);
}
LightState RATGDOComponent::get_light_state() const
{
return *this->light_state;
}
// Lock functions
void RATGDOComponent::lock()
{
this->lock_state = LockState::LOCKED;
this->protocol_->lock_action(LockAction::LOCK);
}
void RATGDOComponent::unlock()
{
this->lock_state = LockState::UNLOCKED;
this->protocol_->lock_action(LockAction::UNLOCK);
}
void RATGDOComponent::lock_toggle()
{
this->lock_state = lock_state_toggle(*this->lock_state);
this->protocol_->lock_action(LockAction::TOGGLE);
}
// Learn functions
void RATGDOComponent::activate_learn()
{
this->protocol_->call(ActivateLearn { });
}
void RATGDOComponent::inactivate_learn()
{
this->protocol_->call(InactivateLearn { });
}
// Subscribe implementations are now templates in ratgdo.h
// dry contact methods
void RATGDOComponent::set_dry_contact_open_sensor(
esphome::binary_sensor::BinarySensor* dry_contact_open_sensor)
{
dry_contact_open_sensor_ = dry_contact_open_sensor;
dry_contact_open_sensor_->add_on_state_callback([this](bool sensor_value) {
this->protocol_->set_open_limit(sensor_value);
this->door_position = 1.0;
});
}
void RATGDOComponent::set_dry_contact_close_sensor(
esphome::binary_sensor::BinarySensor* dry_contact_close_sensor)
{
dry_contact_close_sensor_ = dry_contact_close_sensor;
dry_contact_close_sensor_->add_on_state_callback([this](bool sensor_value) {
this->protocol_->set_close_limit(sensor_value);
this->door_position = 0.0;
});
}
} // namespace esphome::ratgdo
+685
View File
@@ -0,0 +1,685 @@
/************************************
* Rage
* Against
* The
* Garage
* Door
* Opener
*
* Copyright (C) 2022 Paul Wieland
*
* GNU GENERAL PUBLIC LICENSE
************************************/
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include <bitset>
#include <type_traits>
#include <utility>
#include "callbacks.h"
#include "macros.h"
#include "observable.h"
#include "protocol.h"
#include "ratgdo_state.h"
// Observable subscriber counts — set by Python codegen via cg.add_define().
// Missing defines are a build error to catch codegen issues early.
#ifndef RATGDO_MAX_DOOR_STATE_SUBSCRIBERS
#error "RATGDO_MAX_DOOR_STATE_SUBSCRIBERS must be defined by codegen"
#endif
#ifndef RATGDO_MAX_DOOR_ACTION_DELAYED_SUBSCRIBERS
#error "RATGDO_MAX_DOOR_ACTION_DELAYED_SUBSCRIBERS must be defined by codegen"
#endif
#ifndef RATGDO_MAX_DISTANCE_SUBSCRIBERS
#error "RATGDO_MAX_DISTANCE_SUBSCRIBERS must be defined by codegen"
#endif
#ifndef RATGDO_MAX_VEHICLE_DETECTED_SUBSCRIBERS
#error "RATGDO_MAX_VEHICLE_DETECTED_SUBSCRIBERS must be defined by codegen"
#endif
#ifndef RATGDO_MAX_VEHICLE_ARRIVING_SUBSCRIBERS
#error "RATGDO_MAX_VEHICLE_ARRIVING_SUBSCRIBERS must be defined by codegen"
#endif
#ifndef RATGDO_MAX_VEHICLE_LEAVING_SUBSCRIBERS
#error "RATGDO_MAX_VEHICLE_LEAVING_SUBSCRIBERS must be defined by codegen"
#endif
namespace esphome {
class InternalGPIOPin;
} // namespace esphome
namespace esphome::ratgdo {
class RATGDOComponent;
typedef Parented<RATGDOComponent> RATGDOClient;
const float DOOR_POSITION_UNKNOWN = -1.0;
const float DOOR_DELTA_UNKNOWN = -2.0;
const uint8_t PAIRED_DEVICES_UNKNOWN = 0xFF;
struct RATGDOStore {
volatile uint32_t obstruction_low_count = 0; // count obstruction low pulses
static void IRAM_ATTR HOT isr_obstruction(RATGDOStore* arg)
{
arg->obstruction_low_count++;
}
};
using protocol::Args;
using protocol::Result;
class RATGDOComponent : public Component {
public:
RATGDOComponent()
{
}
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
void init_protocol();
void obstruction_loop();
float start_opening { -1 };
single_observable<float> opening_duration { 0 };
float start_closing { -1 };
single_observable<float> closing_duration { 0 };
#ifdef RATGDO_USE_CLOSING_DELAY
single_observable<uint32_t> closing_delay { 0 };
#endif
#ifdef RATGDO_USE_DISTANCE_SENSOR
single_observable<int16_t> target_distance_measurement { -1 };
std::bitset<256> in_range; // the length of this bitset determines how many out of range readings are required for presence detection to change states
observable<int16_t, RATGDO_MAX_DISTANCE_SUBSCRIBERS> last_distance_measurement { 0 };
#endif
single_observable<uint16_t> openings { 0 }; // number of times the door has been opened
single_observable<uint8_t> paired_total { PAIRED_DEVICES_UNKNOWN };
single_observable<uint8_t> paired_remotes { PAIRED_DEVICES_UNKNOWN };
single_observable<uint8_t> paired_keypads { PAIRED_DEVICES_UNKNOWN };
single_observable<uint8_t> paired_wall_controls { PAIRED_DEVICES_UNKNOWN };
single_observable<uint8_t> paired_accessories { PAIRED_DEVICES_UNKNOWN };
observable<DoorState, RATGDO_MAX_DOOR_STATE_SUBSCRIBERS> door_state { DoorState::UNKNOWN };
observable<float, RATGDO_MAX_DOOR_STATE_SUBSCRIBERS> door_position { DOOR_POSITION_UNKNOWN };
observable<DoorActionDelayed, RATGDO_MAX_DOOR_ACTION_DELAYED_SUBSCRIBERS> door_action_delayed { DoorActionDelayed::NO };
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 };
single_observable<LightState> light_state { LightState::UNKNOWN };
single_observable<LockState> lock_state { LockState::UNKNOWN };
single_observable<ObstructionState> obstruction_state { ObstructionState::UNKNOWN };
single_observable<MotorState> motor_state { MotorState::UNKNOWN };
single_observable<ButtonState> button_state { ButtonState::UNKNOWN };
single_observable<MotionState> motion_state { MotionState::UNKNOWN };
single_observable<LearnState> learn_state { LearnState::UNKNOWN };
#ifdef RATGDO_USE_VEHICLE_SENSORS
observable<VehicleDetectedState, RATGDO_MAX_VEHICLE_DETECTED_SUBSCRIBERS> vehicle_detected_state { VehicleDetectedState::NO };
observable<VehicleArrivingState, RATGDO_MAX_VEHICLE_ARRIVING_SUBSCRIBERS> vehicle_arriving_state { VehicleArrivingState::NO };
observable<VehicleLeavingState, RATGDO_MAX_VEHICLE_LEAVING_SUBSCRIBERS> vehicle_leaving_state { VehicleLeavingState::NO };
#endif
OnceCallbacks<void(DoorState)> on_door_state_;
single_observable<bool> sync_failed { false };
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 set_input_obst_pin(InternalGPIOPin* pin) { this->input_obst_pin_ = pin; }
void set_obst_sleep_low(bool low) { this->flags_.obst_sleep_low = low; }
// dry contact methods
void set_dry_contact_open_sensor(esphome::binary_sensor::BinarySensor* dry_contact_open_sensor_);
void set_dry_contact_close_sensor(esphome::binary_sensor::BinarySensor* dry_contact_close_sensor_);
void set_discrete_open_pin(InternalGPIOPin* pin) { this->protocol_->set_discrete_open_pin(pin); }
void set_discrete_close_pin(InternalGPIOPin* pin) { this->protocol_->set_discrete_close_pin(pin); }
Result call_protocol(Args args);
void received(const DoorState door_state);
void received(const LightState light_state);
void received(const LockState lock_state);
void received(const ObstructionState obstruction_state);
void received(const LightAction light_action);
void received(const MotorState motor_state);
void received(const ButtonState button_state);
void received(const MotionState motion_state);
void received(const LearnState light_state);
void received(const Openings openings);
void received(const TimeToClose ttc);
void received(const PairedDeviceCount pdc);
void received(const BatteryState pdc);
// door
void door_toggle();
void door_open();
void door_close();
void door_stop();
void door_action(DoorAction action);
void ensure_door_action(DoorAction action, uint32_t delay = 1500);
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);
#ifdef RATGDO_USE_CLOSING_DELAY
void set_closing_delay(uint32_t delay) { this->closing_delay = delay; }
#endif
void schedule_door_position_sync(float update_period = 500);
void door_position_update();
void cancel_position_sync_callbacks();
#ifdef RATGDO_USE_DISTANCE_SENSOR
void set_target_distance_measurement(int16_t distance);
void set_distance_measurement(int16_t distance);
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
void calculate_presence();
void presence_change(bool sensor_value);
#endif
// light
void light_toggle();
void light_on();
void light_off();
LightState get_light_state() const;
// lock
void lock_toggle();
void lock();
void unlock();
// Learn & Paired
void activate_learn();
void inactivate_learn();
void query_paired_devices();
void query_paired_devices(PairedDevice kind);
void clear_paired_devices(PairedDevice kind);
// Uses length + first character instead of string comparisons to avoid
// string literals in RODATA which consume RAM on ESP8266.
// Valid values: "all" (3,a), "remote" (6,r), "keypad" (6,k), "wall" (4,w), "accessory" (9,a)
// Template so it works with std::string, StringRef, or any type with length() and operator[].
template <typename StringT>
void clear_paired_devices(const StringT& kind)
{
PairedDevice device;
if (kind.length() == 3 && kind[0] == 'a') {
device = PairedDevice::ALL;
} else if (kind.length() == 6 && kind[0] == 'r') {
device = PairedDevice::REMOTE;
} else if (kind.length() == 6 && kind[0] == 'k') {
device = PairedDevice::KEYPAD;
} else if (kind.length() == 4 && kind[0] == 'w') {
device = PairedDevice::WALL_CONTROL;
} else if (kind.length() == 9 && kind[0] == 'a') {
device = PairedDevice::ACCESSORY;
} else {
return;
}
this->clear_paired_devices(device);
}
// 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_rolling_code_counter(F&& f);
template <typename F>
void subscribe_opening_duration(F&& f);
template <typename F>
void subscribe_closing_duration(F&& f);
#ifdef RATGDO_USE_CLOSING_DELAY
template <typename F>
void subscribe_closing_delay(F&& f);
#endif
template <typename F>
void subscribe_openings(F&& f);
template <typename F>
void subscribe_paired_devices_total(F&& f);
template <typename F>
void subscribe_paired_remotes(F&& f);
template <typename F>
void subscribe_paired_keypads(F&& f);
template <typename F>
void subscribe_paired_wall_controls(F&& f);
template <typename F>
void subscribe_paired_accessories(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_obstruction_state(F&& f);
template <typename F>
void subscribe_motor_state(F&& f);
template <typename F>
void subscribe_button_state(F&& f);
template <typename F>
void subscribe_motion_state(F&& f);
template <typename F>
void subscribe_sync_failed(F&& f);
template <typename F>
void subscribe_learn_state(F&& f);
template <typename F>
void subscribe_door_action_delayed(F&& f);
#ifdef RATGDO_USE_DISTANCE_SENSOR
template <typename F>
void subscribe_distance_measurement(F&& f);
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
template <typename F>
void subscribe_vehicle_detected_state(F&& f);
template <typename F>
void subscribe_vehicle_arriving_state(F&& f);
template <typename F>
void subscribe_vehicle_leaving_state(F&& f);
#endif
protected:
// Pointers first (4-byte aligned)
protocol::Protocol* protocol_;
InternalGPIOPin* output_gdo_pin_;
InternalGPIOPin* input_gdo_pin_;
InternalGPIOPin* input_obst_pin_;
esphome::binary_sensor::BinarySensor* dry_contact_open_sensor_;
esphome::binary_sensor::BinarySensor* dry_contact_close_sensor_;
// 4-byte members
RATGDOStore isr_store_ { };
// Bool members packed into bitfield
struct {
uint8_t obstruction_sensor_detected : 1;
uint8_t obst_sleep_low : 1;
#ifdef RATGDO_USE_VEHICLE_SENSORS
uint8_t presence_detect_window_active : 1;
uint8_t reserved : 5; // Reserved for future use
#else
uint8_t reserved : 6; // Reserved for future use
#endif
} flags_ { 0 };
// Subscriber counters for defer name allocation
uint8_t door_state_sub_num_ { 0 };
uint8_t door_action_delayed_sub_num_ { 0 };
#ifdef RATGDO_USE_DISTANCE_SENSOR
uint8_t distance_sub_num_ { 0 };
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
uint8_t vehicle_detected_sub_num_ { 0 };
uint8_t vehicle_arriving_sub_num_ { 0 };
uint8_t vehicle_leaving_sub_num_ { 0 };
int last_presence_percent_ { -1 };
int presence_off_counter_ { 0 };
DoorState last_door_state_for_presence_ { DoorState::UNKNOWN };
#endif
}; // 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_DOOR_ACTION_DELAYED_COUNT = RATGDO_MAX_DOOR_ACTION_DELAYED_SUBSCRIBERS;
inline constexpr uint32_t DEFER_DOOR_ACTION_DELAYED_BASE = DEFER_DOOR_STATE_BASE + DEFER_DOOR_STATE_COUNT;
#ifdef RATGDO_USE_DISTANCE_SENSOR
inline constexpr uint32_t DEFER_DISTANCE_COUNT = RATGDO_MAX_DISTANCE_SUBSCRIBERS;
inline constexpr uint32_t DEFER_DISTANCE_BASE = DEFER_DOOR_ACTION_DELAYED_BASE + DEFER_DOOR_ACTION_DELAYED_COUNT;
inline constexpr uint32_t DEFER_DISTANCE_END = DEFER_DISTANCE_BASE + DEFER_DISTANCE_COUNT;
#else
inline constexpr uint32_t DEFER_DISTANCE_END = DEFER_DOOR_ACTION_DELAYED_BASE + DEFER_DOOR_ACTION_DELAYED_COUNT;
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
inline constexpr uint32_t DEFER_VEHICLE_DETECTED_COUNT = RATGDO_MAX_VEHICLE_DETECTED_SUBSCRIBERS;
inline constexpr uint32_t DEFER_VEHICLE_DETECTED_BASE = DEFER_DISTANCE_END;
inline constexpr uint32_t DEFER_VEHICLE_ARRIVING_COUNT = RATGDO_MAX_VEHICLE_ARRIVING_SUBSCRIBERS;
inline constexpr uint32_t DEFER_VEHICLE_ARRIVING_BASE = DEFER_VEHICLE_DETECTED_BASE + DEFER_VEHICLE_DETECTED_COUNT;
inline constexpr uint32_t DEFER_VEHICLE_LEAVING_COUNT = RATGDO_MAX_VEHICLE_LEAVING_SUBSCRIBERS;
inline constexpr uint32_t DEFER_VEHICLE_LEAVING_BASE = DEFER_VEHICLE_ARRIVING_BASE + DEFER_VEHICLE_ARRIVING_COUNT;
inline constexpr uint32_t DEFER_VEHICLE_END = DEFER_VEHICLE_LEAVING_BASE + DEFER_VEHICLE_LEAVING_COUNT;
#else
inline constexpr uint32_t DEFER_VEHICLE_END = DEFER_DISTANCE_END;
#endif
// Single-subscriber IDs
enum : uint32_t {
DEFER_ROLLING_CODE = DEFER_VEHICLE_END,
DEFER_OPENING_DURATION,
DEFER_CLOSING_DURATION,
DEFER_CLOSING_DELAY,
DEFER_OPENINGS,
DEFER_PAIRED_TOTAL,
DEFER_PAIRED_REMOTES,
DEFER_PAIRED_KEYPADS,
DEFER_PAIRED_WALL_CONTROLS,
DEFER_PAIRED_ACCESSORIES,
DEFER_LIGHT_STATE,
DEFER_LOCK_STATE,
DEFER_OBSTRUCTION_STATE,
DEFER_MOTOR_STATE,
DEFER_BUTTON_STATE,
DEFER_MOTION_STATE,
DEFER_LEARN_STATE,
// Named timeout IDs (replacing string-based names)
TIMEOUT_DOOR_QUERY_STATE,
TIMEOUT_DOOR_ACTION,
TIMEOUT_MOVE_TO_POSITION,
TIMEOUT_CLEAR_MOTION,
// Shared by RATGDOComponent and Secplus1 — safe because only one
// protocol is compiled at a time (#ifdef PROTOCOL_SECPLUSV1) and
// both use ratgdo_ as the scheduler owner.
TIMEOUT_DOOR_STATE_EXPIRY,
TIMEOUT_PRESENCE_DETECT_WINDOW,
TIMEOUT_CLEAR_PRESENCE,
TIMEOUT_WALL_PANEL_EMULATION,
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_rolling_code_counter(F&& f)
{
// change update to children is defered until after component loop
// if multiple changes occur during component loop, only the last one is notified
auto counter = this->protocol_->call(protocol::GetRollingCodeCounter { });
if (counter.tag == protocol::Result::Tag::rolling_code_counter) {
counter.value.rolling_code_counter.value->subscribe([this, f](uint32_t state) {
defer(scheduler_ids::DEFER_ROLLING_CODE, [f, state] { f(state); });
});
}
}
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); });
});
}
#ifdef RATGDO_USE_CLOSING_DELAY
template <typename F>
void RATGDOComponent::subscribe_closing_delay(F&& f)
{
this->closing_delay.subscribe([this, f](uint32_t state) {
defer(scheduler_ids::DEFER_CLOSING_DELAY, [f, state] { f(state); });
});
}
#endif
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_paired_devices_total(F&& f)
{
this->paired_total.subscribe([this, f](uint8_t state) {
defer(scheduler_ids::DEFER_PAIRED_TOTAL, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_paired_remotes(F&& f)
{
this->paired_remotes.subscribe([this, f](uint8_t state) {
defer(scheduler_ids::DEFER_PAIRED_REMOTES, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_paired_keypads(F&& f)
{
this->paired_keypads.subscribe([this, f](uint8_t state) {
defer(scheduler_ids::DEFER_PAIRED_KEYPADS, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_paired_wall_controls(F&& f)
{
this->paired_wall_controls.subscribe([this, f](uint8_t state) {
defer(scheduler_ids::DEFER_PAIRED_WALL_CONTROLS, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_paired_accessories(F&& f)
{
this->paired_accessories.subscribe([this, f](uint8_t state) {
defer(scheduler_ids::DEFER_PAIRED_ACCESSORIES, [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_obstruction_state(F&& f)
{
this->obstruction_state.subscribe([this, f](ObstructionState state) {
defer(scheduler_ids::DEFER_OBSTRUCTION_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_motor_state(F&& f)
{
this->motor_state.subscribe([this, f](MotorState state) {
defer(scheduler_ids::DEFER_MOTOR_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_button_state(F&& f)
{
this->button_state.subscribe([this, f](ButtonState state) {
defer(scheduler_ids::DEFER_BUTTON_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_motion_state(F&& f)
{
this->motion_state.subscribe([this, f](MotionState state) {
defer(scheduler_ids::DEFER_MOTION_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_learn_state(F&& f)
{
this->learn_state.subscribe([this, f](LearnState state) {
defer(scheduler_ids::DEFER_LEARN_STATE, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_door_action_delayed(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_DOOR_ACTION_DELAYED_BASE, scheduler_ids::DEFER_DOOR_ACTION_DELAYED_COUNT,
this->door_action_delayed_sub_num_, LOG_STR("door_action_delayed"));
this->door_action_delayed.subscribe([this, f, id](DoorActionDelayed state) {
defer(id, [f, state] { f(state); });
});
}
#ifdef RATGDO_USE_DISTANCE_SENSOR
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); });
});
}
#endif
#ifdef RATGDO_USE_VEHICLE_SENSORS
template <typename F>
void RATGDOComponent::subscribe_vehicle_detected_state(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_VEHICLE_DETECTED_BASE, scheduler_ids::DEFER_VEHICLE_DETECTED_COUNT,
this->vehicle_detected_sub_num_, LOG_STR("vehicle_detected"));
this->vehicle_detected_state.subscribe([this, f, id](VehicleDetectedState state) {
defer(id, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_vehicle_arriving_state(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_VEHICLE_ARRIVING_BASE, scheduler_ids::DEFER_VEHICLE_ARRIVING_COUNT,
this->vehicle_arriving_sub_num_, LOG_STR("vehicle_arriving"));
this->vehicle_arriving_state.subscribe([this, f, id](VehicleArrivingState state) {
defer(id, [f, state] { f(state); });
});
}
template <typename F>
void RATGDOComponent::subscribe_vehicle_leaving_state(F&& f)
{
uint32_t id = get_scheduler_id(scheduler_ids::DEFER_VEHICLE_LEAVING_BASE, scheduler_ids::DEFER_VEHICLE_LEAVING_COUNT,
this->vehicle_leaving_sub_num_, LOG_STR("vehicle_leaving"));
this->vehicle_leaving_state.subscribe([this, f, id](VehicleLeavingState state) {
defer(id, [f, state] { f(state); });
});
}
#endif
} // namespace esphome::ratgdo
+47
View File
@@ -0,0 +1,47 @@
#include "ratgdo_state.h"
namespace esphome::ratgdo {
LightState light_state_toggle(LightState state)
{
switch (state) {
case LightState::OFF:
return LightState::ON;
case LightState::ON:
return LightState::OFF;
// 2 and 3 appears sometimes
case LightState::UNKNOWN:
default:
return LightState::UNKNOWN;
}
}
LockState lock_state_toggle(LockState state)
{
switch (state) {
case LockState::UNLOCKED:
return LockState::LOCKED;
case LockState::LOCKED:
return LockState::UNLOCKED;
// 2 and 3 appears sometimes
case LockState::UNKNOWN:
default:
return LockState::UNKNOWN;
}
}
LearnState learn_state_toggle(LearnState state)
{
switch (state) {
case LearnState::ACTIVE:
return LearnState::INACTIVE;
case LearnState::INACTIVE:
return LearnState::ACTIVE;
// 2 and 3 appears sometimes
case LearnState::UNKNOWN:
default:
return LearnState::UNKNOWN;
}
}
} // namespace esphome::ratgdo
+139
View File
@@ -0,0 +1,139 @@
/************************************
* Rage
* Against
* The
* Garage
* Door
* Opener
*
* Copyright (C) 2022 Paul Wieland
*
* GNU GENERAL PUBLIC LICENSE
************************************/
#pragma once
#include "esphome/core/defines.h"
#include "macros.h"
#include <cstdint>
namespace esphome::ratgdo {
ENUM(DoorState, uint8_t,
(UNKNOWN, 0),
(OPEN, 1),
(CLOSED, 2),
(STOPPED, 3),
(OPENING, 4),
(CLOSING, 5))
ENUM(DoorActionDelayed, uint8_t,
(NO, 0),
(YES, 1))
/// Enum for all states a the light can be in.
ENUM(LightState, uint8_t,
(OFF, 0),
(ON, 1),
(UNKNOWN, 2))
LightState light_state_toggle(LightState state);
/// Enum for all states a the lock can be in.
ENUM(LockState, uint8_t,
(UNLOCKED, 0),
(LOCKED, 1),
(UNKNOWN, 2))
LockState lock_state_toggle(LockState state);
/// MotionState for all states a the motion can be in.
ENUM(MotionState, uint8_t,
(CLEAR, 0),
(DETECTED, 1),
(UNKNOWN, 2))
/// Enum for all states a the obstruction can be in.
ENUM(ObstructionState, uint8_t,
(OBSTRUCTED, 0),
(CLEAR, 1),
(UNKNOWN, 2))
/// Enum for all states a the motor can be in.
ENUM(MotorState, uint8_t,
(OFF, 0),
(ON, 1),
(UNKNOWN, 2))
/// Enum for all states the button can be in.
ENUM(ButtonState, uint8_t,
(PRESSED, 0),
(RELEASED, 1),
(UNKNOWN, 2))
ENUM_SPARSE(BatteryState, uint8_t,
(UNKNOWN, 0),
(CHARGING, 0x6),
(FULL, 0x8))
/// Enum for learn states.
ENUM(LearnState, uint8_t,
(INACTIVE, 0),
(ACTIVE, 1),
(UNKNOWN, 2))
LearnState learn_state_toggle(LearnState state);
ENUM(PairedDevice, uint8_t,
(ALL, 0),
(REMOTE, 1),
(KEYPAD, 2),
(WALL_CONTROL, 3),
(ACCESSORY, 4),
(UNKNOWN, 0xff))
// actions
ENUM(LightAction, uint8_t,
(OFF, 0),
(ON, 1),
(TOGGLE, 2),
(UNKNOWN, 3))
ENUM(LockAction, uint8_t,
(UNLOCK, 0),
(LOCK, 1),
(TOGGLE, 2),
(UNKNOWN, 3))
ENUM(DoorAction, uint8_t,
(CLOSE, 0),
(OPEN, 1),
(TOGGLE, 2),
(STOP, 3),
(UNKNOWN, 4))
#ifdef RATGDO_USE_VEHICLE_SENSORS
ENUM(VehicleDetectedState, uint8_t,
(NO, 0),
(YES, 1))
ENUM(VehicleArrivingState, uint8_t,
(NO, 0),
(YES, 1))
ENUM(VehicleLeavingState, uint8_t,
(NO, 0),
(YES, 1))
#endif
struct Openings {
uint16_t count;
uint8_t flag;
};
struct PairedDeviceCount {
PairedDevice kind;
uint8_t count;
};
struct TimeToClose {
uint16_t seconds;
};
} // namespace esphome::ratgdo
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "ratgdo_uart_esp32.h"
#elif defined(USE_ESP8266)
#include "ratgdo_uart_esp8266.h"
#endif
+187
View File
@@ -0,0 +1,187 @@
#include "ratgdo_uart.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include <driver/uart.h>
#include <driver/rmt_tx.h>
#include <esp_private/rmt.h> // for rmt_get_channel_id (used once during init)
#include <driver/gpio.h>
#include <esp_rom_gpio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <soc/gpio_sig_map.h>
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo_uart";
static constexpr size_t UART_RX_BUFFER_SIZE = 512;
// Security+ 2.0 preamble timing (microseconds, at 1MHz RMT resolution = ticks)
static constexpr uint16_t PREAMBLE_DURATION_US = 1300;
static constexpr uint16_t PREAMBLE_MARK_US = 130;
static constexpr uint8_t SIGNAL_SETTLE_US = 5;
// RMT channel configuration
static constexpr uint32_t RMT_RESOLUTION_HZ = 1000000; // 1MHz = 1us per tick
static constexpr size_t RMT_MEM_BLOCK_SYMBOLS = 64;
static constexpr size_t RMT_TRANS_QUEUE_DEPTH = 4;
// UART port and signal index — must stay in sync
static constexpr int UART_PORT = UART_NUM_1;
static constexpr int UART_TX_SIGNAL_IDX = U1TXD_OUT_IDX;
RatgdoUART::RatgdoUART() { }
RatgdoUART::~RatgdoUART()
{
if (this->is_initialized_) {
uart_driver_delete((uart_port_t)this->uart_num_);
if (this->rmt_copy_encoder_) {
rmt_del_encoder(this->rmt_copy_encoder_);
this->rmt_copy_encoder_ = nullptr;
}
if (this->rmt_chan_handle_) {
rmt_disable(this->rmt_chan_handle_);
rmt_del_channel(this->rmt_chan_handle_);
this->rmt_chan_handle_ = nullptr;
}
}
}
void RatgdoUART::begin(int baud, RatgdoUARTConfig config, int rx_pin,
int tx_pin, bool invert)
{
this->tx_pin_ = tx_pin;
this->rx_pin_ = rx_pin;
this->baud_ = baud;
this->inverted_ = invert;
this->uart_num_ = UART_PORT;
uart_config_t uart_config = { };
uart_config.baud_rate = baud;
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = (config == RATGDO_UART_8E1) ? UART_PARITY_EVEN : UART_PARITY_DISABLE;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = UART_SCLK_APB;
ESP_ERROR_CHECK(
uart_driver_install((uart_port_t)this->uart_num_, UART_RX_BUFFER_SIZE, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config((uart_port_t)this->uart_num_, &uart_config));
rmt_tx_channel_config_t tx_chan_config = { };
tx_chan_config.gpio_num = (gpio_num_t)tx_pin;
tx_chan_config.clk_src = RMT_CLK_SRC_DEFAULT;
tx_chan_config.resolution_hz = RMT_RESOLUTION_HZ;
tx_chan_config.mem_block_symbols = RMT_MEM_BLOCK_SYMBOLS;
tx_chan_config.trans_queue_depth = RMT_TRANS_QUEUE_DEPTH;
tx_chan_config.flags.invert_out = 0;
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &this->rmt_chan_handle_));
rmt_copy_encoder_config_t copy_encoder_config = { };
ESP_ERROR_CHECK(
rmt_new_copy_encoder(&copy_encoder_config, &this->rmt_copy_encoder_));
ESP_ERROR_CHECK(rmt_enable(this->rmt_chan_handle_));
// Cache the channel ID for GPIO matrix switching during preamble.
// The RMT driver allocates channels dynamically, so we query it once
// here rather than relying on the private esp_private/rmt.h API at runtime.
ESP_ERROR_CHECK(rmt_get_channel_id(this->rmt_chan_handle_, &this->rmt_channel_id_));
ESP_ERROR_CHECK(uart_set_pin((uart_port_t)this->uart_num_, tx_pin, rx_pin,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
if (invert) {
uart_set_line_inverse((uart_port_t)this->uart_num_,
UART_SIGNAL_TXD_INV | UART_SIGNAL_RXD_INV);
}
this->is_initialized_ = true;
ESP_LOGD(TAG, "Hardware UART and RMT initialized on TX=%d RX=%d", tx_pin,
rx_pin);
}
void RatgdoUART::transmit_secplus2_preamble()
{
if (!this->is_initialized_)
return;
// Switch GPIO matrix from UART TX to RMT output
esp_rom_gpio_connect_out_signal(this->tx_pin_, RMT_SIG_OUT0_IDX + this->rmt_channel_id_,
false, false);
esp_rom_delay_us(SIGNAL_SETTLE_US);
// Indicate the start of a frame by pulling the 12V line low for at least
// 1 byte followed by one STOP bit, which indicates to the receiving end
// that the start of the message follows.
// The output pin controls a transistor, so the logic is inverted:
// RMT level 1 (HIGH) pulls the wire low, level 0 (LOW) lets it float high.
rmt_symbol_word_t symbols[1];
symbols[0].duration0 = PREAMBLE_DURATION_US;
symbols[0].level0 = 1;
symbols[0].duration1 = PREAMBLE_MARK_US;
symbols[0].level1 = 0;
rmt_transmit_config_t transmit_config = { };
transmit_config.loop_count = 0;
rmt_transmit(this->rmt_chan_handle_, this->rmt_copy_encoder_, symbols,
sizeof(symbols), &transmit_config);
rmt_tx_wait_all_done(this->rmt_chan_handle_, -1);
// Switch GPIO matrix back to UART TX
esp_rom_gpio_connect_out_signal(this->tx_pin_, UART_TX_SIGNAL_IDX, false, false);
esp_rom_delay_us(SIGNAL_SETTLE_US);
}
void RatgdoUART::write(const uint8_t* data, size_t len)
{
if (this->is_initialized_) {
uart_write_bytes((uart_port_t)this->uart_num_, (const char*)data, len);
uart_wait_tx_done((uart_port_t)this->uart_num_, portMAX_DELAY);
}
}
void RatgdoUART::write(uint8_t data) { write(&data, 1); }
int RatgdoUART::available()
{
if (!this->is_initialized_)
return 0;
size_t length = 0;
uart_get_buffered_data_len((uart_port_t)this->uart_num_, &length);
return length;
}
int RatgdoUART::read()
{
if (!this->is_initialized_)
return -1;
uint8_t data = 0;
int len = uart_read_bytes((uart_port_t)this->uart_num_, &data, 1, 0);
if (len > 0) {
return data;
}
return -1;
}
void RatgdoUART::on_shutdown()
{
if (this->is_initialized_) {
// Unmap the matrix output signal so that UART peripheral resets do not
// pull the hardware line dominant.
esp_rom_gpio_connect_out_signal(this->tx_pin_, SIG_GPIO_OUT_IDX, false, false);
gpio_set_direction((gpio_num_t)this->tx_pin_, GPIO_MODE_INPUT);
gpio_set_direction((gpio_num_t)this->rx_pin_, GPIO_MODE_INPUT);
}
}
} // namespace esphome::ratgdo
#endif
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include <driver/rmt_tx.h>
#include <stddef.h>
#include <stdint.h>
namespace esphome::ratgdo {
enum RatgdoUARTConfig {
RATGDO_UART_8N1,
RATGDO_UART_8E1,
};
class RatgdoUART {
public:
RatgdoUART();
~RatgdoUART();
void begin(int baud, RatgdoUARTConfig config, int rx_pin, int tx_pin,
bool invert);
void write(const uint8_t* data, size_t len);
void write(uint8_t data);
int available();
int read();
void enableIntTx(bool enable) { }
void enableAutoBaud(bool enable) { }
int baudRate() { return this->baud_; }
// Sends the SecPlus 2.0 preamble using RMT
void transmit_secplus2_preamble();
void on_shutdown();
private:
// Pointers (4 bytes on 32-bit)
rmt_channel_handle_t rmt_chan_handle_ { nullptr };
rmt_encoder_handle_t rmt_copy_encoder_ { nullptr };
// 4-byte members
int tx_pin_ { -1 };
int rx_pin_ { -1 };
int baud_ { 9600 };
int uart_num_ { -1 };
int rmt_channel_id_ { 0 };
// 1-byte members packed at the end
bool inverted_ { true };
bool is_initialized_ { false };
};
} // namespace esphome::ratgdo
#endif
+494
View File
@@ -0,0 +1,494 @@
#ifdef PROTOCOL_SECPLUSV2
#include "secplus2.h"
#include "ratgdo.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/scheduler.h"
extern "C" {
#include "secplus.h"
}
namespace esphome::ratgdo {
namespace secplus2 {
using namespace scheduler_ids;
// MAX_CODES_WITHOUT_FLASH_WRITE is a bit of a guess
// since we write the flash at most every every 1min
//
// We want the rolling counter to be high enough that the
// GDO will accept the command after an unexpected reboot
// that did not save the counter to flash in time which
// results in the rolling counter being behind what the GDO
// expects.
static const uint8_t MAX_CODES_WITHOUT_FLASH_WRITE = 60;
static const char* const TAG = "ratgdo_secplus2";
void Secplus2::setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin)
{
this->ratgdo_ = ratgdo;
this->scheduler_ = scheduler;
this->tx_pin_ = tx_pin;
this->rx_pin_ = rx_pin;
this->uart_.begin(9600, RATGDO_UART_8N1, rx_pin->get_pin(), tx_pin->get_pin(), true);
this->uart_.enableIntTx(false);
this->uart_.enableAutoBaud(true);
this->traits_.set_features(Traits::all());
}
void Secplus2::loop()
{
if (this->flags_.transmit_pending) {
if (!this->transmit_packet()) {
return;
}
}
auto cmd = this->read_command();
if (cmd) {
this->handle_command(*cmd);
}
}
void Secplus2::dump_config()
{
ESP_LOGCONFIG(TAG, " Rolling Code Counter: %d", *this->rolling_code_counter_);
ESP_LOGCONFIG(TAG, " Client ID: %d", this->client_id_);
ESP_LOGCONFIG(TAG, " Protocol: SEC+ v2");
}
void Secplus2::on_shutdown()
{
this->uart_.on_shutdown();
}
void Secplus2::sync_helper(uint32_t start, uint32_t delay, uint8_t tries)
{
bool synced = true;
if (*this->ratgdo_->door_state == DoorState::UNKNOWN) {
this->query_status();
synced = false;
}
if (*this->ratgdo_->openings == 0) {
this->query_openings();
synced = false;
}
if (*this->ratgdo_->paired_total == PAIRED_DEVICES_UNKNOWN) {
this->query_paired_devices(PairedDevice::ALL);
synced = false;
}
if (*this->ratgdo_->paired_remotes == PAIRED_DEVICES_UNKNOWN) {
this->query_paired_devices(PairedDevice::REMOTE);
synced = false;
}
if (*this->ratgdo_->paired_keypads == PAIRED_DEVICES_UNKNOWN) {
this->query_paired_devices(PairedDevice::KEYPAD);
synced = false;
}
if (*this->ratgdo_->paired_wall_controls == PAIRED_DEVICES_UNKNOWN) {
this->query_paired_devices(PairedDevice::WALL_CONTROL);
synced = false;
}
if (*this->ratgdo_->paired_accessories == PAIRED_DEVICES_UNKNOWN) {
this->query_paired_devices(PairedDevice::ACCESSORY);
synced = false;
}
if (synced) {
return;
}
if (tries == 2 && *this->ratgdo_->door_state == DoorState::UNKNOWN) { // made a few attempts and no progress (door state is the first sync request)
// increment rolling code counter by some amount in case we crashed without writing to flash the latest value
this->increment_rolling_code_counter(MAX_CODES_WITHOUT_FLASH_WRITE);
}
// not sync-ed after 30s, notify failure
if (millis() - start > 30000) {
ESP_LOGW(TAG, "Triggering sync failed actions.");
this->ratgdo_->sync_failed = true;
} else {
if (tries % 3 == 0) {
delay *= 1.5;
}
this->scheduler_->set_timeout(this->ratgdo_, TIMEOUT_SYNC, delay, [this, start, delay, tries]() {
this->sync_helper(start, delay, tries + 1);
});
};
}
void Secplus2::sync()
{
this->scheduler_->cancel_timeout(this->ratgdo_, TIMEOUT_SYNC);
this->sync_helper(millis(), 500, 0);
}
void Secplus2::light_action(LightAction action)
{
if (action == LightAction::UNKNOWN) {
return;
}
this->send_command(Command(CommandType::LIGHT, static_cast<uint8_t>(action)));
}
void Secplus2::lock_action(LockAction action)
{
if (action == LockAction::UNKNOWN) {
return;
}
this->send_command(Command(CommandType::LOCK, static_cast<uint8_t>(action)));
}
void Secplus2::door_action(DoorAction action)
{
if (action == DoorAction::UNKNOWN) {
return;
}
this->door_command(action);
}
Result Secplus2::call(Args args)
{
using Tag = Args::Tag;
if (args.tag == Tag::query_status) {
this->send_command(CommandType::GET_STATUS);
} else if (args.tag == Tag::query_openings) {
this->send_command(CommandType::GET_OPENINGS);
} else if (args.tag == Tag::get_rolling_code_counter) {
return Result(RollingCodeCounter { std::addressof(this->rolling_code_counter_) });
} else if (args.tag == Tag::set_rolling_code_counter) {
this->set_rolling_code_counter(args.value.set_rolling_code_counter.counter);
} else if (args.tag == Tag::set_client_id) {
this->set_client_id(args.value.set_client_id.client_id);
} else if (args.tag == Tag::query_paired_devices) {
this->query_paired_devices(args.value.query_paired_devices.kind);
} else if (args.tag == Tag::query_paired_devices_all) {
this->query_paired_devices();
} else if (args.tag == Tag::clear_paired_devices) {
this->clear_paired_devices(args.value.clear_paired_devices.kind);
} else if (args.tag == Tag::activate_learn) {
this->activate_learn();
} else if (args.tag == Tag::inactivate_learn) {
this->inactivate_learn();
}
return { };
}
void Secplus2::door_command(DoorAction action)
{
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 1, 1), IncrementRollingCode::NO, [this, action]() {
this->ratgdo_->set_timeout(150, [this, action] {
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 0, 1));
});
});
}
void Secplus2::query_status()
{
this->send_command(CommandType::GET_STATUS);
}
void Secplus2::query_openings()
{
this->send_command(CommandType::GET_OPENINGS);
}
void Secplus2::query_paired_devices()
{
const auto kinds = {
PairedDevice::ALL,
PairedDevice::REMOTE,
PairedDevice::KEYPAD,
PairedDevice::WALL_CONTROL,
PairedDevice::ACCESSORY
};
uint32_t timeout = 0;
for (auto kind : kinds) {
timeout += 200;
this->ratgdo_->set_timeout(timeout, [this, kind] { this->query_paired_devices(kind); });
}
}
void Secplus2::query_paired_devices(PairedDevice kind)
{
ESP_LOGD(TAG, "Query paired devices of type: %s", LOG_STR_ARG(PairedDevice_to_string(kind)));
this->send_command(Command { CommandType::GET_PAIRED_DEVICES, static_cast<uint8_t>(kind) });
}
// wipe devices from memory based on get paired devices nibble values
void Secplus2::clear_paired_devices(PairedDevice kind)
{
if (kind == PairedDevice::UNKNOWN) {
return;
}
ESP_LOGW(TAG, "Clear paired devices of type: %s", LOG_STR_ARG(PairedDevice_to_string(kind)));
if (kind == PairedDevice::ALL) {
this->ratgdo_->set_timeout(200, [this] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::REMOTE) - 1 }); }); // wireless
this->ratgdo_->set_timeout(400, [this] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::KEYPAD) - 1 }); }); // keypads
this->ratgdo_->set_timeout(600, [this] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::WALL_CONTROL) - 1 }); }); // wall controls
this->ratgdo_->set_timeout(800, [this] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::ACCESSORY) - 1 }); }); // accessories
this->ratgdo_->set_timeout(1000, [this] { this->query_status(); });
this->ratgdo_->set_timeout(1200, [this] { this->query_paired_devices(); });
} else {
uint8_t dev_kind = static_cast<uint8_t>(kind) - 1;
this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, dev_kind }); // just requested device
this->ratgdo_->set_timeout(200, [this] { this->query_status(); });
this->ratgdo_->set_timeout(400, [this, kind] { this->query_paired_devices(kind); });
}
}
// Learn functions
void Secplus2::activate_learn()
{
// Send LEARN with nibble = 0 then nibble = 1 to mimic wall control learn button
this->send_command(Command { CommandType::LEARN, 0 });
this->ratgdo_->set_timeout(150, [this] { this->send_command(Command { CommandType::LEARN, 1 }); });
this->ratgdo_->set_timeout(500, [this] { this->query_status(); });
}
void Secplus2::inactivate_learn()
{
// Send LEARN twice with nibble = 0 to inactivate learn and get status to update switch state
this->send_command(Command { CommandType::LEARN, 0 });
this->ratgdo_->set_timeout(150, [this] { this->send_command(Command { CommandType::LEARN, 0 }); });
this->ratgdo_->set_timeout(500, [this] { this->query_status(); });
}
optional<Command> Secplus2::read_command()
{
if (!this->flags_.rx_reading_msg) {
while (this->uart_.available()) {
uint8_t ser_byte = this->uart_.read();
this->rx_last_read_ = millis();
if (ser_byte != 0x55 && ser_byte != 0x01 && ser_byte != 0x00) {
{
char hex[format_hex_pretty_size(1)];
ESP_LOG2(TAG, "Ignoring byte (%d): %s, baud: %d", this->rx_byte_count_, format_hex_pretty_to(hex, &ser_byte, 1), this->uart_.baudRate());
}
this->rx_byte_count_ = 0;
continue;
}
this->rx_msg_start_ = ((this->rx_msg_start_ << 8) | ser_byte) & 0xffffff;
this->rx_byte_count_++;
// if we are at the start of a message, capture the next 16 bytes
if (this->rx_msg_start_ == 0x550100) {
ESP_LOG1(TAG, "Baud: %d", this->uart_.baudRate());
this->rx_packet_[0] = 0x55;
this->rx_packet_[1] = 0x01;
this->rx_packet_[2] = 0x00;
this->rx_byte_count_ = 3;
this->flags_.rx_reading_msg = true;
break;
}
}
}
if (this->flags_.rx_reading_msg) {
while (this->uart_.available()) {
uint8_t ser_byte = this->uart_.read();
this->rx_last_read_ = millis();
this->rx_packet_[this->rx_byte_count_] = ser_byte;
this->rx_byte_count_++;
// ESP_LOG2(TAG, "Received byte (%d): %02X, baud: %d", this->rx_byte_count_, ser_byte, this->uart_.baudRate());
if (this->rx_byte_count_ == PACKET_LENGTH) {
this->flags_.rx_reading_msg = false;
this->rx_byte_count_ = 0;
this->print_packet(LOG_STR("Received packet"), this->rx_packet_);
return this->decode_packet(this->rx_packet_);
}
}
if (millis() - this->rx_last_read_ > 100) {
// if we have a partial packet and it's been over 100ms since last byte was read,
// the rest is not coming (a full packet should be received in ~20ms),
// discard it so we can read the following packet correctly
ESP_LOGW(TAG, "Discard incomplete packet, length: %d", this->rx_byte_count_);
this->flags_.rx_reading_msg = false;
this->rx_byte_count_ = 0;
}
}
return { };
}
void Secplus2::print_packet(const esphome::LogString* prefix, const WirePacket& packet) const
{
constexpr size_t hex_size = format_hex_pretty_size(PACKET_LENGTH);
char hex_buf[hex_size];
ESP_LOGD(TAG, "%s: [%s]", LOG_STR_ARG(prefix), format_hex_pretty_to(hex_buf, packet, PACKET_LENGTH));
}
optional<Command> Secplus2::decode_packet(const WirePacket& packet) const
{
uint32_t rolling = 0;
uint64_t fixed = 0;
uint32_t data = 0;
int err = decode_wireline(packet, &rolling, &fixed, &data);
if (err < 0) {
ESP_LOGW(TAG, "Decode failed (parity error or invalid frame)");
return { };
}
uint16_t cmd = ((fixed >> 24) & 0xf00) | (data & 0xff);
data &= ~0xf000; // clear parity nibble
if ((fixed & 0xFFFFFFFF) == this->client_id_) { // my commands
ESP_LOG1(TAG, "[%ld] received mine: rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
return { };
} else {
ESP_LOG1(TAG, "[%ld] received rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
}
CommandType cmd_type = to_CommandType(cmd, CommandType::UNKNOWN);
uint8_t nibble = (data >> 8) & 0xff;
uint8_t byte1 = (data >> 16) & 0xff;
uint8_t byte2 = (data >> 24) & 0xff;
ESP_LOG1(TAG, "cmd=%03x (%s) byte2=%02x byte1=%02x nibble=%01x", cmd, LOG_STR_ARG(CommandType_to_string(cmd_type)), byte2, byte1, nibble);
return Command { cmd_type, nibble, byte1, byte2 };
}
void Secplus2::handle_command(const Command& cmd)
{
ESP_LOG1(TAG, "Handle command: %s", LOG_STR_ARG(CommandType_to_string(cmd.type)));
if (cmd.type == CommandType::STATUS) {
this->ratgdo_->received(to_DoorState(cmd.nibble, DoorState::UNKNOWN));
this->ratgdo_->received(to_LightState((cmd.byte2 >> 1) & 1, LightState::UNKNOWN));
this->ratgdo_->received(to_LockState((cmd.byte2 & 1), LockState::UNKNOWN));
// ESP_LOGD(TAG, "Obstruction: reading from byte2, bit2, status=%d", ((byte2 >> 2) & 1) == 1);
this->ratgdo_->received(to_ObstructionState((cmd.byte1 >> 6) & 1, ObstructionState::UNKNOWN));
this->ratgdo_->received(to_LearnState((cmd.byte2 >> 5) & 1, LearnState::UNKNOWN));
} else if (cmd.type == CommandType::LIGHT) {
this->ratgdo_->received(to_LightAction(cmd.nibble, LightAction::UNKNOWN));
} else if (cmd.type == CommandType::MOTOR_ON) {
this->ratgdo_->received(MotorState::ON);
} else if (cmd.type == CommandType::DOOR_ACTION) {
auto button_state = (cmd.byte1 & 1) == 1 ? ButtonState::PRESSED : ButtonState::RELEASED;
this->ratgdo_->received(button_state);
} else if (cmd.type == CommandType::MOTION) {
this->ratgdo_->received(MotionState::DETECTED);
} else if (cmd.type == CommandType::OPENINGS) {
this->ratgdo_->received(Openings { static_cast<uint16_t>((cmd.byte1 << 8) | cmd.byte2), cmd.nibble });
} else if (cmd.type == CommandType::SET_TTC) {
this->ratgdo_->received(TimeToClose { static_cast<uint16_t>((cmd.byte1 << 8) | cmd.byte2) });
} else if (cmd.type == CommandType::PAIRED_DEVICES) {
PairedDeviceCount pdc;
pdc.kind = to_PairedDevice(cmd.nibble, PairedDevice::UNKNOWN);
if (pdc.kind == PairedDevice::ALL) {
pdc.count = cmd.byte2;
} else if (pdc.kind == PairedDevice::REMOTE) {
pdc.count = cmd.byte2;
} else if (pdc.kind == PairedDevice::KEYPAD) {
pdc.count = cmd.byte2;
} else if (pdc.kind == PairedDevice::WALL_CONTROL) {
pdc.count = cmd.byte2;
} else if (pdc.kind == PairedDevice::ACCESSORY) {
pdc.count = cmd.byte2;
}
this->ratgdo_->received(pdc);
} else if (cmd.type == CommandType::BATTERY_STATUS) {
this->ratgdo_->received(to_BatteryState(cmd.byte1, BatteryState::UNKNOWN));
}
ESP_LOG1(TAG, "Done handle command: %s", LOG_STR_ARG(CommandType_to_string(cmd.type)));
}
void Secplus2::send_command(Command command, IncrementRollingCode increment)
{
{
uint8_t data[] = { command.byte2, command.byte1, command.nibble };
constexpr size_t hex_size = format_hex_pretty_size(3);
char hex[hex_size];
ESP_LOGD(TAG, "Send command: %s, data: %s", LOG_STR_ARG(CommandType_to_string(command.type)), format_hex_pretty_to(hex, data, 3));
}
if (!this->flags_.transmit_pending) { // have an untransmitted packet
this->encode_packet(command, this->tx_packet_);
if (increment == IncrementRollingCode::YES) {
this->increment_rolling_code_counter();
}
} else {
// unlikely this would happed (unless not connected to GDO), we're ensuring any pending packet
// is transmitted each loop before doing anyting else
if (this->transmit_pending_start_ > 0) {
ESP_LOGW(TAG, "Have untransmitted packet, ignoring command: %s", LOG_STR_ARG(CommandType_to_string(command.type)));
} else {
ESP_LOGW(TAG, "Not connected to GDO, ignoring command: %s", LOG_STR_ARG(CommandType_to_string(command.type)));
}
}
this->transmit_packet();
}
void Secplus2::encode_packet(Command command, WirePacket& packet)
{
auto cmd = static_cast<uint64_t>(command.type);
uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
uint32_t data = (static_cast<uint64_t>(command.byte2) << 24) | (static_cast<uint64_t>(command.byte1) << 16) | (static_cast<uint64_t>(command.nibble) << 8) | (cmd & 0xff);
ESP_LOG2(TAG, "[%ld] Encode for transmit rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), *this->rolling_code_counter_, fixed, data);
encode_wireline(*this->rolling_code_counter_, fixed, data, packet);
}
bool Secplus2::transmit_packet()
{
auto now = micros();
while (micros() - now < 1300) {
if (this->rx_pin_->digital_read()) {
if (!this->flags_.transmit_pending) {
this->flags_.transmit_pending = true;
this->transmit_pending_start_ = millis();
ESP_LOGD(TAG, "Collision detected, waiting to send packet");
} else if (millis() - this->transmit_pending_start_ >= 5000) {
this->transmit_pending_start_ = 0; // to indicate GDO not connected state
}
return false;
}
delayMicroseconds(100);
}
this->print_packet(LOG_STR("Sending packet"), this->tx_packet_);
this->uart_.transmit_secplus2_preamble();
this->uart_.write(this->tx_packet_, PACKET_LENGTH);
this->flags_.transmit_pending = false;
this->transmit_pending_start_ = 0;
this->on_command_sent_.trigger();
return true;
}
void Secplus2::increment_rolling_code_counter(int delta)
{
this->rolling_code_counter_ = (*this->rolling_code_counter_ + delta) & 0xfffffff;
}
void Secplus2::set_rolling_code_counter(uint32_t counter)
{
ESP_LOGV(TAG, "Set rolling code counter to %d", counter);
this->rolling_code_counter_ = counter;
}
void Secplus2::set_client_id(uint64_t client_id)
{
this->client_id_ = client_id & 0xFFFFFFFF;
}
} // namespace secplus2
} // namespace esphome::ratgdo
#endif // PROTOCOL_SECPLUSV2
+195
View File
@@ -0,0 +1,195 @@
#pragma once
#ifdef PROTOCOL_SECPLUSV2
#include "esphome/core/optional.h"
#include "ratgdo_uart.h"
#include "callbacks.h"
#include "common.h"
#include "observable.h"
#include "protocol.h"
#include "ratgdo_state.h"
namespace esphome {
class Scheduler;
class InternalGPIOPin;
} // namespace esphome
namespace esphome::ratgdo {
class RATGDOComponent;
namespace secplus2 {
using namespace esphome::ratgdo::protocol;
static const uint8_t PACKET_LENGTH = 19;
typedef uint8_t WirePacket[PACKET_LENGTH];
ENUM_SPARSE(CommandType, uint16_t,
(UNKNOWN, 0x000),
(GET_STATUS, 0x080),
(STATUS, 0x081),
(OBST_1, 0x084), // sent when an obstruction happens?
(OBST_2, 0x085), // sent when an obstruction happens?
(BATTERY_STATUS, 0x09d),
(PAIR_3, 0x0a0),
(PAIR_3_RESP, 0x0a1),
(LEARN, 0x181),
(LOCK, 0x18c),
(DOOR_ACTION, 0x280),
(LIGHT, 0x281),
(MOTOR_ON, 0x284),
(MOTION, 0x285),
(GET_PAIRED_DEVICES, 0x307), // nibble 0 for total, 1 wireless, 2 keypads, 3 wall, 4 accessories.
(PAIRED_DEVICES, 0x308), // byte2 holds number of paired devices
(CLEAR_PAIRED_DEVICES, 0x30D), // nibble 0 to clear remotes, 1 keypads, 2 wall, 3 accessories (offset from above)
(LEARN_1, 0x391),
(PING, 0x392),
(PING_RESP, 0x393),
(PAIR_2, 0x400),
(PAIR_2_RESP, 0x401),
(SET_TTC, 0x402), // ttc_in_seconds = (byte1<<8)+byte2
(CANCEL_TTC, 0x408), // ?
(TTC, 0x40a), // Time to close
(GET_OPENINGS, 0x48b),
(OPENINGS, 0x48c), // openings = (byte1<<8)+byte2
)
inline bool operator==(const uint16_t cmd_i, const CommandType& cmd_e) { return cmd_i == static_cast<uint16_t>(cmd_e); }
inline bool operator==(const CommandType& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast<uint16_t>(cmd_e); }
enum class IncrementRollingCode {
NO,
YES,
};
struct Command {
CommandType type;
uint8_t nibble;
uint8_t byte1;
uint8_t byte2;
Command()
: type(CommandType::UNKNOWN)
{
}
Command(CommandType type_, uint8_t nibble_ = 0, uint8_t byte1_ = 0, uint8_t byte2_ = 0)
: type(type_)
, nibble(nibble_)
, byte1(byte1_)
, byte2(byte2_)
{
}
};
class Secplus2 : public Protocol {
public:
void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
void loop();
void dump_config();
void on_shutdown() override;
void sync();
void light_action(LightAction action);
void lock_action(LockAction action);
void door_action(DoorAction action);
Result call(Args args);
const Traits& traits() const { return this->traits_; }
// methods not used by secplus2
void set_open_limit(bool state) { }
void set_close_limit(bool state) { }
void set_discrete_open_pin(InternalGPIOPin* pin) { }
void set_discrete_close_pin(InternalGPIOPin* pin) { }
protected:
void increment_rolling_code_counter(int delta = 1);
void set_rolling_code_counter(uint32_t counter);
void set_client_id(uint64_t client_id);
optional<Command> read_command();
void handle_command(const Command& cmd);
void send_command(Command cmd, IncrementRollingCode increment = IncrementRollingCode::YES);
template <typename F>
void send_command(Command cmd, IncrementRollingCode increment, F&& on_sent)
{
// Only register the callback if the command will be accepted.
// If transmit_pending is set the command will be dropped, and
// a stale callback would fire when the previous pending packet
// transmits -- executing logic (e.g. the second phase of a
// door_command) at the wrong time.
//
// Register before send_command() because transmit_packet() may
// succeed immediately and call on_command_sent_.trigger() inline.
if (this->flags_.transmit_pending) {
return;
}
this->on_command_sent_(std::forward<F>(on_sent));
this->send_command(cmd, increment);
}
void encode_packet(Command cmd, WirePacket& packet);
bool transmit_packet();
void door_command(DoorAction action);
void query_status();
void query_openings();
void query_paired_devices();
void query_paired_devices(PairedDevice kind);
void clear_paired_devices(PairedDevice kind);
void activate_learn();
void inactivate_learn();
void print_packet(const esphome::LogString* prefix, const WirePacket& packet) const;
optional<Command> decode_packet(const WirePacket& packet) const;
void sync_helper(uint32_t start, uint32_t delay, uint8_t tries);
// 8-byte member first (may require 8-byte alignment on some 32-bit systems)
uint64_t client_id_ { 0x539 };
// Pointers (4-byte aligned)
InternalGPIOPin* tx_pin_;
InternalGPIOPin* rx_pin_;
RATGDOComponent* ratgdo_;
Scheduler* scheduler_;
// 4-byte members
uint32_t transmit_pending_start_ { 0 };
uint32_t rx_msg_start_ { 0 };
uint32_t rx_last_read_ { 0 };
// Larger structures
single_observable<uint32_t> rolling_code_counter_ { 0 };
OnceCallbacks<void()> on_command_sent_;
Traits traits_;
RatgdoUART uart_;
// 19-byte arrays
WirePacket tx_packet_;
WirePacket rx_packet_;
// Small members at the end
uint16_t rx_byte_count_ { 0 };
LearnState learn_state_ { LearnState::UNKNOWN };
struct {
uint8_t transmit_pending : 1;
uint8_t rx_reading_msg : 1;
uint8_t reserved : 6; // Reserved for future use
} flags_ { 0 };
};
} // namespace secplus2
} // namespace esphome::ratgdo
#endif // PROTOCOL_SECPLUSV2
+71
View File
@@ -0,0 +1,71 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import (
RATGDO_CLIENT_SCHMEA,
ratgdo_ns,
register_ratgdo_child,
subscribe_distance,
)
CONF_DISTANCE = "distance"
DEPENDENCIES = ["ratgdo"]
# Track which sensor types have been used
USED_TYPES: set[str] = set()
RATGDOSensor = ratgdo_ns.class_("RATGDOSensor", sensor.Sensor, cg.Component)
RATGDOSensorType = ratgdo_ns.enum("RATGDOSensorType")
CONF_TYPE = "type"
TYPES = {
"openings": RATGDOSensorType.RATGDO_OPENINGS,
"paired_devices_total": RATGDOSensorType.RATGDO_PAIRED_DEVICES_TOTAL,
"paired_devices_remotes": RATGDOSensorType.RATGDO_PAIRED_REMOTES,
"paired_devices_keypads": RATGDOSensorType.RATGDO_PAIRED_KEYPADS,
"paired_devices_wall_controls": RATGDOSensorType.RATGDO_PAIRED_WALL_CONTROLS,
"paired_devices_accessories": RATGDOSensorType.RATGDO_PAIRED_ACCESSORIES,
"distance": RATGDOSensorType.RATGDO_DISTANCE,
}
def validate_unique_type(config):
"""Validate that each sensor type is only used once."""
sensor_type = config[CONF_TYPE]
if sensor_type in USED_TYPES:
raise cv.Invalid(f"Only one sensor of type '{sensor_type}' is allowed")
USED_TYPES.add(sensor_type)
return config
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(RATGDOSensor)
.extend(
{
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
}
)
.extend(RATGDO_CLIENT_SCHMEA),
validate_unique_type,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await sensor.register_sensor(var, config)
await cg.register_component(var, config)
cg.add(var.set_ratgdo_sensor_type(config[CONF_TYPE]))
await register_ratgdo_child(var, config)
if config["type"] == "distance":
cg.add_library(name="Wire", version=None)
cg.add_library(
name="vl53l4cx",
repository="https://github.com/stm32duino/VL53L4CX",
version=None,
)
cg.add_define("RATGDO_USE_DISTANCE_SENSOR")
subscribe_distance()
+142
View File
@@ -0,0 +1,142 @@
#include "ratgdo_sensor.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo.sensor";
static const int MIN_DISTANCE = 100; // ignore bugs crawling on the distance sensor & dust protection film
static const int MAX_DISTANCE = 4500; // default maximum distance
void RATGDOSensor::setup()
{
switch (this->ratgdo_sensor_type_) {
case RATGDOSensorType::RATGDO_OPENINGS:
this->parent_->subscribe_openings([this](uint16_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_PAIRED_DEVICES_TOTAL:
this->parent_->subscribe_paired_devices_total([this](uint8_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_PAIRED_REMOTES:
this->parent_->subscribe_paired_remotes([this](uint8_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_PAIRED_KEYPADS:
this->parent_->subscribe_paired_keypads([this](uint8_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_PAIRED_WALL_CONTROLS:
this->parent_->subscribe_paired_wall_controls([this](uint8_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_PAIRED_ACCESSORIES:
this->parent_->subscribe_paired_accessories([this](uint8_t value) {
this->publish_state(value);
});
break;
case RATGDOSensorType::RATGDO_DISTANCE:
#ifdef RATGDO_USE_DISTANCE_SENSOR
this->distance_sensor_.setI2cDevice(&I2C);
this->distance_sensor_.setXShutPin(32);
// I2C.begin(17,16);
I2C.begin(19, 18);
this->distance_sensor_.begin();
this->distance_sensor_.VL53L4CX_Off();
this->distance_sensor_.InitSensor(0x59);
this->distance_sensor_.VL53L4CX_SetDistanceMode(VL53L4CX_DISTANCEMODE_LONG);
this->distance_sensor_.VL53L4CX_StartMeasurement();
#ifdef RATGDO_USE_DISTANCE_SENSOR
this->parent_->subscribe_distance_measurement([this](int16_t value) {
this->publish_state(value);
});
#endif
#endif
break;
default:
break;
}
}
void RATGDOSensor::dump_config()
{
LOG_SENSOR("", "RATGDO Sensor", this);
switch (this->ratgdo_sensor_type_) {
case RATGDOSensorType::RATGDO_OPENINGS:
ESP_LOGCONFIG(TAG, " Type: Openings");
break;
case RATGDOSensorType::RATGDO_PAIRED_DEVICES_TOTAL:
ESP_LOGCONFIG(TAG, " Type: Paired Devices");
break;
case RATGDOSensorType::RATGDO_PAIRED_REMOTES:
ESP_LOGCONFIG(TAG, " Type: Paired Remotes");
break;
case RATGDOSensorType::RATGDO_PAIRED_KEYPADS:
ESP_LOGCONFIG(TAG, " Type: Paired Keypads");
break;
case RATGDOSensorType::RATGDO_PAIRED_WALL_CONTROLS:
ESP_LOGCONFIG(TAG, " Type: Paired Wall Controls");
break;
case RATGDOSensorType::RATGDO_PAIRED_ACCESSORIES:
ESP_LOGCONFIG(TAG, " Type: Paired Accessories");
break;
case RATGDOSensorType::RATGDO_DISTANCE:
ESP_LOGCONFIG(TAG, " Type: Distance");
break;
default:
break;
}
}
#ifdef RATGDO_USE_DISTANCE_SENSOR
void RATGDOSensor::loop()
{
if (this->ratgdo_sensor_type_ == RATGDOSensorType::RATGDO_DISTANCE) {
VL53L4CX_MultiRangingData_t distanceData;
VL53L4CX_MultiRangingData_t* pDistanceData = &distanceData;
uint8_t dataReady = 0;
int objCount = 0;
int16_t maxDistance = -1;
int status;
if (this->distance_sensor_.VL53L4CX_GetMeasurementDataReady(&dataReady) == 0 && dataReady) {
status = this->distance_sensor_.VL53L4CX_GetMultiRangingData(pDistanceData);
objCount = pDistanceData->NumberOfObjectsFound;
for (int i = 0; i < distanceData.NumberOfObjectsFound; i++) {
VL53L4CX_TargetRangeData_t* d = &pDistanceData->RangeData[i];
if (d->RangeStatus == 0) {
maxDistance = std::max(maxDistance, d->RangeMilliMeter);
maxDistance = maxDistance <= MIN_DISTANCE ? -1 : maxDistance;
}
}
if (maxDistance < 0)
maxDistance = MAX_DISTANCE;
// maxDistance = objCount == 0 ? -1 : pDistanceData->RangeData[objCount - 1].RangeMilliMeter;
/*
* if the sensor is pointed at glass, there are many error -1 readings which will fill the
* vector with out of range data. The sensor should be sensitive enough to detect the floor
* in most situations, but daylight and/or really high ceilings can cause long distance
* measurements to be out of range.
*/
this->parent_->set_distance_measurement(maxDistance);
// ESP_LOGD(TAG,"# obj found %d; distance %d",objCount, maxDistance);
if (status == 0) {
status = this->distance_sensor_.VL53L4CX_ClearInterruptAndStartMeasurement();
}
}
}
}
#endif
} // namespace esphome::ratgdo
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef RATGDO_USE_DISTANCE_SENSOR
#include "Wire.h"
#include "vl53l4cx_class.h"
#define I2C Wire
#endif
namespace esphome::ratgdo {
enum RATGDOSensorType : uint8_t {
RATGDO_OPENINGS,
RATGDO_PAIRED_DEVICES_TOTAL,
RATGDO_PAIRED_REMOTES,
RATGDO_PAIRED_KEYPADS,
RATGDO_PAIRED_WALL_CONTROLS,
RATGDO_PAIRED_ACCESSORIES,
RATGDO_DISTANCE
};
class RATGDOSensor : public sensor::Sensor, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
#ifdef RATGDO_USE_DISTANCE_SENSOR
void loop() override;
#endif
void set_ratgdo_sensor_type(RATGDOSensorType ratgdo_sensor_type_) { this->ratgdo_sensor_type_ = ratgdo_sensor_type_; }
protected:
RATGDOSensorType ratgdo_sensor_type_;
#ifdef RATGDO_USE_DISTANCE_SENSOR
VL53L4CX distance_sensor_;
#endif
};
} // namespace esphome::ratgdo
+48
View File
@@ -0,0 +1,48 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PIN
from .. import (
RATGDO_CLIENT_SCHMEA,
ratgdo_ns,
register_ratgdo_child,
subscribe_vehicle_arriving,
)
DEPENDENCIES = ["ratgdo"]
RATGDOSwitch = ratgdo_ns.class_("RATGDOSwitch", switch.Switch, cg.Component)
SwitchType = ratgdo_ns.enum("SwitchType")
CONF_TYPE = "type"
TYPES = {"learn": SwitchType.RATGDO_LEARN, "led": SwitchType.RATGDO_LED}
CONFIG_SCHEMA = (
switch.switch_schema(RATGDOSwitch)
.extend(
{
cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True),
cv.Optional(CONF_PIN): pins.gpio_output_pin_schema,
}
)
.extend(RATGDO_CLIENT_SCHMEA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await switch.register_switch(var, config)
await cg.register_component(var, config)
cg.add(var.set_switch_type(config[CONF_TYPE]))
await register_ratgdo_child(var, config)
if CONF_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
# LED switch conditionally subscribes to vehicle_arriving in C++ (#ifdef RATGDO_USE_VEHICLE_SENSORS).
# Always register the subscription — the C++ guard ensures it's only active when vehicle sensors
# are enabled, and the codegen emits the define to size the observable accordingly.
if config[CONF_TYPE] == "led":
subscribe_vehicle_arriving()
@@ -0,0 +1,64 @@
#include "ratgdo_switch.h"
#include "../ratgdo_state.h"
#include "esphome/core/log.h"
namespace esphome::ratgdo {
static const char* const TAG = "ratgdo.switch";
void RATGDOSwitch::dump_config()
{
LOG_SWITCH("", "RATGDO Switch", this);
switch (this->switch_type_) {
case SwitchType::RATGDO_LEARN:
ESP_LOGCONFIG(TAG, " Type: Learn");
break;
case SwitchType::RATGDO_LED:
ESP_LOGCONFIG(TAG, " Type: LED");
break;
default:
break;
}
}
void RATGDOSwitch::setup()
{
switch (this->switch_type_) {
case SwitchType::RATGDO_LEARN:
this->parent_->subscribe_learn_state([this](LearnState state) {
this->publish_state(state == LearnState::ACTIVE);
});
break;
case SwitchType::RATGDO_LED:
this->pin_->setup();
#ifdef RATGDO_USE_VEHICLE_SENSORS
this->parent_->subscribe_vehicle_arriving_state([this](VehicleArrivingState state) {
this->write_state(state == VehicleArrivingState::YES);
});
#endif
break;
default:
break;
}
}
void RATGDOSwitch::write_state(bool state)
{
switch (this->switch_type_) {
case SwitchType::RATGDO_LEARN:
if (state) {
this->parent_->activate_learn();
} else {
this->parent_->inactivate_learn();
}
break;
case SwitchType::RATGDO_LED:
this->pin_->digital_write(state);
this->publish_state(state);
break;
default:
break;
}
}
} // namespace esphome::ratgdo
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "../ratgdo.h"
#include "../ratgdo_state.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome::ratgdo {
enum SwitchType {
RATGDO_LEARN,
RATGDO_LED
};
class RATGDOSwitch : public switch_::Switch, public RATGDOClient, public Component {
public:
void dump_config() override;
void setup() override;
void set_switch_type(SwitchType switch_type_) { this->switch_type_ = switch_type_; }
void write_state(bool state) override;
void set_pin(GPIOPin* pin) { pin_ = pin; }
protected:
SwitchType switch_type_;
GPIOPin* pin_;
};
} // namespace esphome::ratgdo