From f3f8f966dc16b1c4d820a47b95249de073877862 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Fri, 19 Jan 2024 13:24:16 -1000
Subject: [PATCH] feat: Security Plus v1 support (#171)
Co-authored-by: Marius Muja
---
.github/workflows/build.yml | 17 +-
README.md | 15 +-
base.yaml | 14 +-
base_secplusv1.yaml | 209 +++++
components/ratgdo/__init__.py | 29 +-
components/ratgdo/callbacks.h | 4 +-
components/ratgdo/common.h | 4 +
components/ratgdo/cover/ratgdo_cover.cpp | 13 +-
components/ratgdo/dry_contact.cpp | 69 ++
components/ratgdo/dry_contact.h | 49 ++
.../ratgdo/light/ratgdo_light_output.cpp | 1 -
components/ratgdo/lock/__init__.py | 15 +-
components/ratgdo/{enum.h => macros.h} | 32 +
components/ratgdo/number/ratgdo_number.cpp | 10 +-
components/ratgdo/protocol.h | 117 +++
components/ratgdo/ratgdo.cpp | 817 +++++++-----------
components/ratgdo/ratgdo.h | 134 +--
components/ratgdo/ratgdo_state.h | 41 +-
components/ratgdo/secplus1.cpp | 452 ++++++++++
components/ratgdo/secplus1.h | 156 ++++
components/ratgdo/secplus2.cpp | 512 +++++++++++
components/ratgdo/secplus2.h | 154 ++++
components/ratgdo/sensor/__init__.py | 1 -
components/ratgdo/switch/ratgdo_switch.cpp | 2 +-
static/index.html | 52 +-
static/v25iboard_secplusv1.png | Bin 0 -> 303545 bytes
static/v25iboard_secplusv1.yaml | 52 ++
v25iboard_secplusv1.yaml | 1 +
28 files changed, 2290 insertions(+), 682 deletions(-)
create mode 100644 base_secplusv1.yaml
create mode 100644 components/ratgdo/common.h
create mode 100644 components/ratgdo/dry_contact.cpp
create mode 100644 components/ratgdo/dry_contact.h
rename components/ratgdo/{enum.h => macros.h} (59%)
create mode 100644 components/ratgdo/protocol.h
create mode 100644 components/ratgdo/secplus1.cpp
create mode 100644 components/ratgdo/secplus1.h
create mode 100644 components/ratgdo/secplus2.cpp
create mode 100644 components/ratgdo/secplus2.h
create mode 100644 static/v25iboard_secplusv1.png
create mode 100644 static/v25iboard_secplusv1.yaml
create mode 120000 v25iboard_secplusv1.yaml
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ef23eb0..49b4c8b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -17,26 +17,29 @@ jobs:
matrix:
firmware:
- file: v2board_esp8266_d1_mini_lite.yaml
- name: V2.0 Board ESP8266 D1 Mini Lite
+ name: V2.0 Board ESP8266 D1 Mini Lite Security+ 2.0
manifest_filename: v2board_esp8266_d1_mini_lite-manifest.json
- file: v2board_esp8266_d1_mini.yaml
- name: V2.0 Board ESP8266 D1 Mini
+ name: V2.0 Board ESP8266 D1 Mini Security+ 2.0
manifest_filename: v2board_esp8266_d1_mini-manifest.json
- file: v2board_esp32_d1_mini.yaml
- name: V2.0 Board ESP32 D1 Mini
+ name: V2.0 Board ESP32 D1 Mini Security+ 2.0
manifest_filename: v2board_esp32_d1_mini-manifest.json
- file: v2board_esp32_lolin_s2_mini.yaml
- name: V2.0 Board ESP32 lolin S2 mini
+ name: V2.0 Board ESP32 lolin S2 mini Security+ 2.0
manifest_filename: v2board_esp32_lolin_s2_mini-manifest.json
- file: v25board_esp8266_d1_mini_lite.yaml
- name: V2.5 Board ESP8266 D1 Mini Lite
+ name: V2.5 Board ESP8266 D1 Mini Lite Security+ 2.0
manifest_filename: v25board_esp8266_d1_mini_lite-manifest.json
- file: v25board_esp32_d1_mini.yaml
- name: V2.5 Board ESP32 D1 Mini
+ name: V2.5 Board ESP32 D1 Mini Security+ 2.0
manifest_filename: v25board_esp32_d1_mini-manifest.json
- file: v25iboard.yaml
- name: V2.5i Board
+ name: V2.5i Board Security+ 2.0
manifest_filename: v25iboard-manifest.json
+ - file: v25iboard_secplusv1.yaml
+ name: V2.5i Board Security+ 1.0
+ manifest_filename: v25iboard-manifest_secplusv1.json
fail-fast: false
steps:
- name: Checkout source code
diff --git a/README.md b/README.md
index 1975f3f..d096344 100644
--- a/README.md
+++ b/README.md
@@ -19,13 +19,14 @@ The ESPHome firmware will allow you to open the door to any position after calib
## ESPHome config
-- [ESPHome config for v2.0 board with ESP8266 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp8266_d1_mini.yaml)
-- [ESPHome config for v2.0 board with ESP8266 D1 Mini lite](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp8266_d1_mini_lite.yaml)
-- [ESPHome config for v2.0 board with ESP32 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp32_d1_mini.yaml)
-- [ESPHome config for v2.0 board with ESP32 Lolin D2 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp32_lolin_s2_mini.yaml)
-- [ESPHome config for v2.5 board with ESP8266 D1 Mini lite](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25board_esp8266_d1_mini_lite.yaml)
-- [ESPHome config for v2.5 board with ESP32 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25board_esp32_d1_mini.yaml)
-- [ESPHome config for v2.5i board](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25iboard.yaml)
+- [Security+ 2.0 ESPHome config for v2.0 board with ESP8266 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp8266_d1_mini.yaml)
+- [Security+ 2.0 ESPHome config for v2.0 board with ESP8266 D1 Mini lite](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp8266_d1_mini_lite.yaml)
+- [Security+ 2.0 ESPHome config for v2.0 board with ESP32 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp32_d1_mini.yaml)
+- [Security+ 2.0 ESPHome config for v2.0 board with ESP32 Lolin D2 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v2board_esp32_lolin_s2_mini.yaml)
+- [Security+ 2.0 ESPHome config for v2.5 board with ESP8266 D1 Mini lite](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25board_esp8266_d1_mini_lite.yaml)
+- [Security+ 2.0 ESPHome config for v2.5 board with ESP32 D1 Mini](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25board_esp32_d1_mini.yaml)
+- [Security+ 2.0 ESPHome config for v2.5i board](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25iboard.yaml)
+- [Security+ 1.0 ESPHome config for v2.5i board](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25iboard_secplusv1.yaml)
- [Web Installer](https://ratgdo.github.io/esphome-ratgdo/)
diff --git a/base.yaml b/base.yaml
index d73687a..b1bbd33 100644
--- a/base.yaml
+++ b/base.yaml
@@ -20,13 +20,13 @@ ratgdo:
service: persistent_notification.create
data:
title: "${friendly_name} sync failed"
- message: "Failed to communicate with garage opener on startup; Check the ${friendly_name} Rolling code counter number entity history and set the entity to one number larger than the largest value in history. [ESPHome devices](/config/devices/dashboard?domain=esphome)"
+ message: "Failed to communicate with garage opener on startup."
notification_id: "esphome_ratgdo_${id_prefix}_sync_failed"
api:
services:
- service: wipe_devices_from_gdo_memory
variables:
- devices_to_wipe: string
+ devices_to_wipe: string
then:
- lambda: !lambda |-
if(devices_to_wipe.compare("all") == 0) {
@@ -37,9 +37,9 @@ api:
id($id_prefix).clear_paired_devices(ratgdo::PairedDevice::KEYPAD);
} else if (devices_to_wipe.compare("wall") == 0) {
id($id_prefix).clear_paired_devices(ratgdo::PairedDevice::WALL_CONTROL);
- } else if (devices_to_wipe.compare("accessory") == 0) {
+ } else if (devices_to_wipe.compare("accessory") == 0) {
id($id_prefix).clear_paired_devices(ratgdo::PairedDevice::ACCESSORY);
- }
+ }
sensor:
- platform: ratgdo
@@ -56,7 +56,7 @@ sensor:
entity_category: diagnostic
ratgdo_id: ${id_prefix}
name: "Paired Devices"
- icon: mdi:remote
+ icon: mdi:remote
lock:
- platform: ratgdo
@@ -86,7 +86,7 @@ switch:
- platform: ratgdo
id: "${id_prefix}_learn"
type: learn
- ratgdo_id: ${id_prefix}
+ ratgdo_id: ${id_prefix}
name: "Learn"
icon: mdi:plus-box
entity_category: config
@@ -263,4 +263,4 @@ button:
on_press:
then:
lambda: !lambda |-
- id($id_prefix).toggle_door();
+ id($id_prefix).door_toggle();
diff --git a/base_secplusv1.yaml b/base_secplusv1.yaml
new file mode 100644
index 0000000..089f779
--- /dev/null
+++ b/base_secplusv1.yaml
@@ -0,0 +1,209 @@
+---
+
+external_components:
+ - source:
+ type: git
+ url: https://github.com/ratgdo/esphome-ratgdo
+ refresh: 1s
+
+preferences:
+ flash_write_interval: 5s
+
+ratgdo:
+ id: ${id_prefix}
+ input_gdo_pin: ${uart_rx_pin}
+ output_gdo_pin: ${uart_tx_pin}
+ input_obst_pin: ${input_obst_pin}
+ protocol: secplusv1
+ on_sync_failed:
+ then:
+ - homeassistant.service:
+ service: persistent_notification.create
+ data:
+ title: "${friendly_name} sync failed"
+ message: "Failed to communicate with garage opener on startup."
+ notification_id: "esphome_ratgdo_${id_prefix}_sync_failed"
+
+lock:
+ - platform: ratgdo
+ id: ${id_prefix}_lock_remotes
+ ratgdo_id: ${id_prefix}
+ name: "Lock remotes"
+
+switch:
+ - platform: gpio
+ id: "${id_prefix}_status_door"
+ internal: true
+ pin:
+ number: ${status_door_pin} # D0 output door status, HIGH for open, LOW for closed
+ mode:
+ output: true
+ name: "Status door"
+ entity_category: diagnostic
+ - platform: gpio
+ id: "${id_prefix}_status_obstruction"
+ internal: true
+ pin:
+ number: ${status_obstruction_pin} # D8 output for obstruction status, HIGH for obstructed, LOW for clear
+ mode:
+ output: true
+ name: "Status obstruction"
+ entity_category: diagnostic
+
+binary_sensor:
+ - platform: ratgdo
+ type: motion
+ id: ${id_prefix}_motion
+ ratgdo_id: ${id_prefix}
+ name: "Motion"
+ device_class: motion
+ - platform: ratgdo
+ type: obstruction
+ id: ${id_prefix}_obstruction
+ ratgdo_id: ${id_prefix}
+ name: "Obstruction"
+ device_class: problem
+ on_press:
+ - switch.turn_on: ${id_prefix}_status_obstruction
+ on_release:
+ - switch.turn_off: ${id_prefix}_status_obstruction
+ - platform: ratgdo
+ type: button
+ id: ${id_prefix}_button
+ ratgdo_id: ${id_prefix}
+ name: "Button"
+ entity_category: diagnostic
+ - platform: gpio
+ id: "${id_prefix}_dry_contact_open"
+ pin:
+ number: ${dry_contact_open_pin} # D5 dry contact for opening door
+ inverted: true
+ mode:
+ input: true
+ pullup: true
+ name: "Dry contact open"
+ entity_category: diagnostic
+ filters:
+ - delayed_on_off: 500ms
+ on_press:
+ - if:
+ condition:
+ binary_sensor.is_off: ${id_prefix}_dry_contact_close
+ then:
+ - cover.open: ${id_prefix}_garage_door
+ - platform: gpio
+ id: "${id_prefix}_dry_contact_close"
+ pin:
+ number: ${dry_contact_close_pin} # D6 dry contact for closing door
+ inverted: true
+ mode:
+ input: true
+ pullup: true
+ name: "Dry contact close"
+ entity_category: diagnostic
+ filters:
+ - delayed_on_off: 500ms
+ on_press:
+ - if:
+ condition:
+ binary_sensor.is_off: ${id_prefix}_dry_contact_open
+ then:
+ - cover.close: ${id_prefix}_garage_door
+ - platform: gpio
+ id: "${id_prefix}_dry_contact_light"
+ pin:
+ number: ${dry_contact_light_pin} # D3 dry contact for triggering light (no discrete light commands, so toggle only)
+ inverted: true
+ mode:
+ input: true
+ pullup: true
+ name: "Dry contact light"
+ entity_category: diagnostic
+ filters:
+ - delayed_on_off: 500ms
+ on_press:
+ - light.toggle: ${id_prefix}_light
+
+number:
+ - platform: ratgdo
+ id: ${id_prefix}_rolling_code_counter
+ type: rolling_code_counter
+ entity_category: config
+ ratgdo_id: ${id_prefix}
+ name: "Rolling code counter"
+ mode: box
+ unit_of_measurement: "codes"
+
+ - platform: ratgdo
+ id: ${id_prefix}_opening_duration
+ type: opening_duration
+ entity_category: config
+ ratgdo_id: ${id_prefix}
+ name: "Opening duration"
+ unit_of_measurement: "s"
+
+ - platform: ratgdo
+ id: ${id_prefix}_closing_duration
+ type: closing_duration
+ entity_category: config
+ ratgdo_id: ${id_prefix}
+ name: "Closing duration"
+ unit_of_measurement: "s"
+
+ - platform: ratgdo
+ id: ${id_prefix}_client_id
+ type: client_id
+ entity_category: config
+ ratgdo_id: ${id_prefix}
+ name: "Client ID"
+ mode: box
+
+cover:
+ - platform: ratgdo
+ id: ${id_prefix}_garage_door
+ device_class: garage
+ name: "Door"
+ ratgdo_id: ${id_prefix}
+ on_closed:
+ - switch.turn_off: ${id_prefix}_status_door
+ on_open:
+ - switch.turn_on: ${id_prefix}_status_door
+
+light:
+ - platform: ratgdo
+ id: ${id_prefix}_light
+ name: "Light"
+ ratgdo_id: ${id_prefix}
+
+button:
+ - platform: restart
+ name: "Restart"
+ - platform: safe_mode
+ name: "Safe mode boot"
+ entity_category: diagnostic
+
+ - platform: template
+ id: ${id_prefix}_query_status
+ entity_category: diagnostic
+ name: "Query status"
+ on_press:
+ then:
+ lambda: !lambda |-
+ id($id_prefix).query_status();
+
+ - platform: template
+ id: ${id_prefix}_sync
+ name: "Sync"
+ entity_category: diagnostic
+ on_press:
+ then:
+ lambda: !lambda |-
+ id($id_prefix).sync();
+
+ - platform: template
+ id: ${id_prefix}_toggle_door
+ name: "Toggle door"
+ on_press:
+ then:
+ lambda: !lambda |-
+ id($id_prefix).door_toggle();
diff --git a/components/ratgdo/__init__.py b/components/ratgdo/__init__.py
index d584c33..6b52ddc 100644
--- a/components/ratgdo/__init__.py
+++ b/components/ratgdo/__init__.py
@@ -1,5 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
+import voluptuous as vol
from esphome import automation, pins
from esphome.const import CONF_ID, CONF_TRIGGER_ID
@@ -28,6 +29,12 @@ 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]
CONFIG_SCHEMA = cv.Schema(
{
@@ -38,14 +45,17 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(
CONF_INPUT_GDO, default=DEFAULT_INPUT_GDO
): pins.gpio_input_pin_schema,
- cv.Optional(
- CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST
- ): pins.gpio_input_pin_schema,
+ cv.Optional(CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST): cv.Any(
+ cv.none, pins.gpio_input_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): vol.In(
+ SUPPORTED_PROTOCOLS
+ ),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -68,8 +78,9 @@ async def to_code(config):
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))
- pin = await cg.gpio_pin_expression(config[CONF_INPUT_OBST])
- cg.add(var.set_input_obst_pin(pin))
+ if CONF_INPUT_OBST in config and config[CONF_INPUT_OBST]:
+ pin = await cg.gpio_pin_expression(config[CONF_INPUT_OBST])
+ cg.add(var.set_input_obst_pin(pin))
for conf in config.get(CONF_ON_SYNC_FAILED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
@@ -85,3 +96,11 @@ async def to_code(config):
repository="https://github.com/ratgdo/espsoftwareserial#autobaud",
version=None,
)
+
+ if config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV1:
+ cg.add_define("PROTOCOL_SECPLUSV1")
+ elif config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV2:
+ cg.add_define("PROTOCOL_SECPLUSV2")
+ elif config[CONF_PROTOCOL] == PROTOCOL_DRYCONTACT:
+ cg.add_define("PROTOCOL_DRYCONTACT")
+ cg.add(var.init_protocol())
diff --git a/components/ratgdo/callbacks.h b/components/ratgdo/callbacks.h
index 9fdce25..b436af4 100644
--- a/components/ratgdo/callbacks.h
+++ b/components/ratgdo/callbacks.h
@@ -13,9 +13,9 @@ namespace ratgdo {
class OnceCallbacks {
public:
template
- void then(Callback&& callback) { this->callbacks_.push_back(std::forward(callback)); }
+ void operator()(Callback&& callback) { this->callbacks_.push_back(std::forward(callback)); }
- void operator()(Ts... args)
+ void trigger(Ts... args)
{
for (auto& cb : this->callbacks_)
cb(args...);
diff --git a/components/ratgdo/common.h b/components/ratgdo/common.h
new file mode 100644
index 0000000..e6571b5
--- /dev/null
+++ b/components/ratgdo/common.h
@@ -0,0 +1,4 @@
+#pragma once
+
+#define ESP_LOG1 ESP_LOGV
+#define ESP_LOG2 ESP_LOGV
\ No newline at end of file
diff --git a/components/ratgdo/cover/ratgdo_cover.cpp b/components/ratgdo/cover/ratgdo_cover.cpp
index 6ff1c8c..955af0e 100644
--- a/components/ratgdo/cover/ratgdo_cover.cpp
+++ b/components/ratgdo/cover/ratgdo_cover.cpp
@@ -27,6 +27,7 @@ namespace ratgdo {
void RATGDOCover::on_door_state(DoorState state, float position)
{
+ bool save_to_flash = true;
switch (state) {
case DoorState::OPEN:
this->position = COVER_OPEN;
@@ -39,10 +40,12 @@ namespace ratgdo {
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;
@@ -55,7 +58,7 @@ namespace ratgdo {
break;
}
- this->publish_state();
+ this->publish_state(save_to_flash);
}
CoverTraits RATGDOCover::get_traits()
@@ -70,17 +73,17 @@ namespace ratgdo {
void RATGDOCover::control(const CoverCall& call)
{
if (call.get_stop()) {
- this->parent_->stop_door();
+ this->parent_->door_stop();
}
if (call.get_toggle()) {
- this->parent_->toggle_door();
+ this->parent_->door_toggle();
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == COVER_OPEN) {
- this->parent_->open_door();
+ this->parent_->door_open();
} else if (pos == COVER_CLOSED) {
- this->parent_->close_door();
+ this->parent_->door_close();
} else {
this->parent_->door_move_to_position(pos);
}
diff --git a/components/ratgdo/dry_contact.cpp b/components/ratgdo/dry_contact.cpp
new file mode 100644
index 0000000..5dc5a9d
--- /dev/null
+++ b/components/ratgdo/dry_contact.cpp
@@ -0,0 +1,69 @@
+
+#include "dry_contact.h"
+#include "ratgdo.h"
+
+#include "esphome/core/gpio.h"
+#include "esphome/core/log.h"
+#include "esphome/core/scheduler.h"
+
+namespace esphome {
+namespace ratgdo {
+ namespace dry_contact {
+
+ static const char* const TAG = "ratgdo_dry_contact";
+
+ void DryContact::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;
+ }
+
+ void DryContact::loop()
+ {
+ }
+
+ void DryContact::dump_config()
+ {
+ ESP_LOGCONFIG(TAG, " Protocol: dry contact");
+ }
+
+ void DryContact::sync()
+ {
+ }
+
+ void DryContact::light_action(LightAction action)
+ {
+ ESP_LOG1(TAG, "Ignoring light action: %s", LightAction_to_string(action));
+ return;
+ }
+
+ void DryContact::lock_action(LockAction action)
+ {
+ ESP_LOG1(TAG, "Ignoring lock action: %s", LockAction_to_string(action));
+ return;
+ }
+
+ void DryContact::door_action(DoorAction action)
+ {
+ if (action != DoorAction::TOGGLE) {
+ ESP_LOG1(TAG, "Ignoring door action: %s", DoorAction_to_string(action));
+ return;
+ }
+ ESP_LOG1(TAG, "Door action: %s", DoorAction_to_string(action));
+
+ this->tx_pin_->digital_write(1);
+ this->scheduler_->set_timeout(this->ratgdo_, "", 200, [=] {
+ this->tx_pin_->digital_write(0);
+ });
+ }
+
+ Result DryContact::call(Args args)
+ {
+ return {};
+ }
+
+ } // namespace DryContact
+} // namespace ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/dry_contact.h b/components/ratgdo/dry_contact.h
new file mode 100644
index 0000000..99dd024
--- /dev/null
+++ b/components/ratgdo/dry_contact.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
+#include "esphome/core/optional.h"
+
+#include "callbacks.h"
+#include "observable.h"
+#include "protocol.h"
+#include "ratgdo_state.h"
+
+namespace esphome {
+
+class Scheduler;
+class InternalGPIOPin;
+
+namespace ratgdo {
+ namespace dry_contact {
+
+ using namespace esphome::ratgdo::protocol;
+
+ class DryContact : public Protocol {
+ public:
+ void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
+ void loop();
+ void dump_config();
+
+ 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_; }
+
+ protected:
+ Traits traits_;
+
+ InternalGPIOPin* tx_pin_;
+ InternalGPIOPin* rx_pin_;
+
+ RATGDOComponent* ratgdo_;
+ Scheduler* scheduler_;
+ };
+
+ } // namespace secplus1
+} // namespace ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/light/ratgdo_light_output.cpp b/components/ratgdo/light/ratgdo_light_output.cpp
index 976d4aa..3362708 100644
--- a/components/ratgdo/light/ratgdo_light_output.cpp
+++ b/components/ratgdo/light/ratgdo_light_output.cpp
@@ -31,7 +31,6 @@ namespace ratgdo {
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);
diff --git a/components/ratgdo/lock/__init__.py b/components/ratgdo/lock/__init__.py
index fc646d3..a71a2ed 100644
--- a/components/ratgdo/lock/__init__.py
+++ b/components/ratgdo/lock/__init__.py
@@ -9,15 +9,12 @@ DEPENDENCIES = ["ratgdo"]
RATGDOLock = ratgdo_ns.class_("RATGDOLock", lock.Lock, cg.Component)
-CONFIG_SCHEMA = (
- lock.LOCK_SCHEMA
- .extend(
- {
- cv.GenerateID(): cv.declare_id(RATGDOLock),
- }
- )
- .extend(RATGDO_CLIENT_SCHMEA)
-)
+CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(RATGDOLock),
+ }
+).extend(RATGDO_CLIENT_SCHMEA)
+
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
diff --git a/components/ratgdo/enum.h b/components/ratgdo/macros.h
similarity index 59%
rename from components/ratgdo/enum.h
rename to components/ratgdo/macros.h
index db090af..e148af3 100644
--- a/components/ratgdo/enum.h
+++ b/components/ratgdo/macros.h
@@ -55,3 +55,35 @@
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__) \
+ };
diff --git a/components/ratgdo/number/ratgdo_number.cpp b/components/ratgdo/number/ratgdo_number.cpp
index 0f85055..68df7b5 100644
--- a/components/ratgdo/number/ratgdo_number.cpp
+++ b/components/ratgdo/number/ratgdo_number.cpp
@@ -5,6 +5,9 @@
namespace esphome {
namespace ratgdo {
+ using protocol::SetClientID;
+ using protocol::SetRollingCodeCounter;
+
float normalize_client_id(float client_id)
{
uint32_t int_value = static_cast(client_id);
@@ -84,6 +87,9 @@ namespace ratgdo {
void RATGDONumber::update_state(float value)
{
+ if (value == this->state) {
+ return;
+ }
this->pref_.save(&value);
this->publish_state(value);
}
@@ -91,14 +97,14 @@ namespace ratgdo {
void RATGDONumber::control(float value)
{
if (this->number_type_ == RATGDO_ROLLING_CODE_COUNTER) {
- this->parent_->set_rolling_code_counter(value);
+ this->parent_->call_protocol(SetRollingCodeCounter { static_cast(value) });
} else if (this->number_type_ == RATGDO_OPENING_DURATION) {
this->parent_->set_opening_duration(value);
} else if (this->number_type_ == RATGDO_CLOSING_DURATION) {
this->parent_->set_closing_duration(value);
} else if (this->number_type_ == RATGDO_CLIENT_ID) {
value = normalize_client_id(value);
- this->parent_->set_client_id(value);
+ this->parent_->call_protocol(SetClientID { static_cast(value) });
}
this->update_state(value);
}
diff --git a/components/ratgdo/protocol.h b/components/ratgdo/protocol.h
new file mode 100644
index 0000000..8202079
--- /dev/null
+++ b/components/ratgdo/protocol.h
@@ -0,0 +1,117 @@
+#pragma once
+
+#include "common.h"
+#include "ratgdo_state.h"
+
+namespace esphome {
+
+class Scheduler;
+class InternalGPIOPin;
+
+namespace 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 {
+ observable* 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 sync();
+
+ 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 ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/ratgdo.cpp b/components/ratgdo/ratgdo.cpp
index d43ec74..791b803 100644
--- a/components/ratgdo/ratgdo.cpp
+++ b/components/ratgdo/ratgdo.cpp
@@ -12,29 +12,23 @@
************************************/
#include "ratgdo.h"
+#include "common.h"
+#include "dry_contact.h"
#include "ratgdo_state.h"
+#include "secplus1.h"
+#include "secplus2.h"
+#include "esphome/core/application.h"
+#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
-#define ESP_LOG1 ESP_LOGV
-#define ESP_LOG2 ESP_LOGV
-
namespace esphome {
namespace ratgdo {
+ using namespace protocol;
+
static const char* const TAG = "ratgdo";
static const int SYNC_DELAY = 1000;
- //
- // MAX_CODES_WITHOUT_FLASH_WRITE is a bit of a guess
- // since we write the flash at most every every 5s
- //
- // 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 = 10;
void RATGDOComponent::setup()
{
@@ -44,7 +38,7 @@ namespace ratgdo {
this->input_gdo_pin_->setup();
this->input_gdo_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
- if (this->input_obst_pin_ == nullptr || this->input_obst_pin_->get_pin() == 0) {
+ if (this->input_obst_pin_ == nullptr) {
// Our base.yaml is always going to set this so we check for 0
// as well to avoid a breaking change.
this->obstruction_from_status_ = true;
@@ -53,27 +47,34 @@ namespace ratgdo {
this->input_obst_pin_->pin_mode(gpio::FLAG_INPUT);
this->input_obst_pin_->attach_interrupt(RATGDOStore::isr_obstruction, &this->isr_store_, gpio::INTERRUPT_FALLING_EDGE);
}
- this->sw_serial_.begin(9600, SWSERIAL_8N1, this->input_gdo_pin_->get_pin(), this->output_gdo_pin_->get_pin(), true);
- this->sw_serial_.enableIntTx(false);
- this->sw_serial_.enableAutoBaud(true);
- ESP_LOGV(TAG, "Syncing rolling code counter after reboot...");
+ this->protocol_->setup(this, &App.scheduler, this->input_gdo_pin_, this->output_gdo_pin_);
// many things happening at startup, use some delay for sync
set_timeout(SYNC_DELAY, [=] { this->sync(); });
}
+ // 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()
{
- if (this->transmit_pending_) {
- if (!this->transmit_packet()) {
- return;
- }
- }
if (!this->obstruction_from_status_) {
this->obstruction_loop();
}
- this->gdo_state_loop();
+ this->protocol_->loop();
}
void RATGDOComponent::dump_config()
@@ -86,195 +87,217 @@ namespace ratgdo {
} else {
LOG_PIN(" Input Obstruction Pin: ", this->input_obst_pin_);
}
- ESP_LOGCONFIG(TAG, " Rolling Code Counter: %d", *this->rolling_code_counter);
- ESP_LOGCONFIG(TAG, " Client ID: %d", this->client_id_);
+ this->protocol_->dump_config();
}
- uint16_t RATGDOComponent::decode_packet(const WirePacket& packet)
+ void RATGDOComponent::received(const DoorState door_state)
{
- uint32_t rolling = 0;
- uint64_t fixed = 0;
- uint32_t data = 0;
+ ESP_LOGD(TAG, "Door state=%s", DoorState_to_string(door_state));
- decode_wireline(packet, &rolling, &fixed, &data);
+ auto prev_door_state = *this->door_state;
- 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 static_cast(Command::UNKNOWN);
- } else {
- ESP_LOG1(TAG, "[%ld] received rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
+ if (prev_door_state == door_state) {
+ return;
}
- Command cmd_enum = to_Command(cmd, Command::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, Command_to_string(cmd_enum), byte2, byte1, nibble);
-
- if (cmd == Command::STATUS) {
-
- auto door_state = to_DoorState(nibble, DoorState::UNKNOWN);
- auto prev_door_state = *this->door_state;
-
- // 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;
- }
+ // opening duration calibration
+ if (*this->opening_duration == 0) {
+ if (door_state == DoorState::OPENING && prev_door_state == DoorState::CLOSED) {
+ this->start_opening = millis();
}
- // 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::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;
- }
- 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;
- }
- this->schedule_door_position_sync();
- } else if (door_state == DoorState::STOPPED) {
+ if (door_state == DoorState::OPENING) {
+ // door started opening
+ if (prev_door_state == DoorState::CLOSING) {
this->door_position_update();
- if (*this->door_position == DOOR_POSITION_UNKNOWN) {
- this->door_position = 0.5; // best guess
- }
this->cancel_position_sync_callbacks();
- } 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->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_state = door_state;
- this->door_state_received(door_state);
- this->light_state = static_cast((byte2 >> 1) & 1); // safe because it can only be 0 or 1
- this->lock_state = static_cast(byte2 & 1); // safe because it can only be 0 or 1
- this->motion_state = MotionState::CLEAR; // when the status message is read, reset motion state to 0|clear
- this->motor_state = MotorState::OFF; // when the status message is read, reset motor state to 0|off
-
- auto learn_state = static_cast((byte2 >> 5) & 1);
- if (*this->learn_state != learn_state) {
- if (learn_state == LearnState::INACTIVE) {
- this->query_paired_devices();
- }
- this->learn_state = learn_state;
+ 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->obstruction_from_status_) {
- // ESP_LOGD(TAG, "Obstruction: reading from byte2, bit2, status=%d", ((byte2 >> 2) & 1) == 1);
- this->obstruction_state = static_cast((byte1 >> 6) & 1);
- // This isn't very fast to update, but its still better
- // than nothing in the case the obstruction sensor is not
- // wired up.
- ESP_LOGD(TAG, "Obstruction: reading from GDO status byte1, bit6=%s", ObstructionState_to_string(*this->obstruction_state));
+ if (*this->closing_duration != 0) {
+ this->schedule_door_position_sync();
}
-
- if (door_state == DoorState::CLOSED && door_state != prev_door_state) {
- this->send_command(Command::GET_OPENINGS);
+ } 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();
+ cancel_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();
+ }
- ESP_LOGD(TAG, "Status: door=%s light=%s lock=%s learn=%s",
- DoorState_to_string(*this->door_state),
- LightState_to_string(*this->light_state),
- LockState_to_string(*this->lock_state),
- LearnState_to_string(*this->learn_state));
+ if (door_state == DoorState::OPEN || door_state == DoorState::CLOSED || door_state == DoorState::STOPPED) {
+ this->motor_state = MotorState::OFF;
+ }
- } else if (cmd == Command::LIGHT) {
- if (nibble == 0) {
- this->light_state = LightState::OFF;
- } else if (nibble == 1) {
- this->light_state = LightState::ON;
- } else if (nibble == 2) { // toggle
- this->light_state = light_state_toggle(*this->light_state);
- }
- ESP_LOGD(TAG, "Light: action=%s state=%s",
- nibble == 0 ? "OFF" : nibble == 1 ? "ON"
- : "TOGGLE",
- LightState_to_string(*this->light_state));
- } else if (cmd == Command::MOTOR_ON) {
- this->motor_state = MotorState::ON;
- ESP_LOGD(TAG, "Motor: state=%s", MotorState_to_string(*this->motor_state));
- } else if (cmd == Command::DOOR_ACTION) {
- this->button_state = (byte1 & 1) == 1 ? ButtonState::PRESSED : ButtonState::RELEASED;
- ESP_LOGD(TAG, "Open: button=%s", ButtonState_to_string(*this->button_state));
- } else if (cmd == Command::OPENINGS) {
- // nibble==0 if it's our request
- // update openings only from our request or if it's not unknown state
- if (nibble == 0 || *this->openings != 0) {
- this->openings = (byte1 << 8) | byte2;
- ESP_LOGD(TAG, "Openings: %d", *this->openings);
- } else {
- ESP_LOGD(TAG, "Ignoring openings, not from our request");
- }
- } else if (cmd == Command::MOTION) {
- this->motion_state = MotionState::DETECTED;
+ 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", 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", LightState_to_string(light_state));
+ this->light_state = light_state;
+ }
+
+ void RATGDOComponent::received(const LockState lock_state)
+ {
+ ESP_LOGD(TAG, "Lock state=%s", LockState_to_string(lock_state));
+ this->lock_state = lock_state;
+ }
+
+ void RATGDOComponent::received(const ObstructionState obstruction_state)
+ {
+ if (this->obstruction_from_status_) {
+ ESP_LOGD(TAG, "Obstruction: state=%s", 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", MotorState_to_string(*this->motor_state));
+ this->motor_state = motor_state;
+ }
+
+ void RATGDOComponent::received(const ButtonState button_state)
+ {
+ ESP_LOGD(TAG, "Button state=%s", ButtonState_to_string(*this->button_state));
+ this->button_state = button_state;
+ }
+
+ void RATGDOComponent::received(const MotionState motion_state)
+ {
+ ESP_LOGD(TAG, "Motion: %s", MotionState_to_string(*this->motion_state));
+ this->motion_state = motion_state;
+ if (motion_state == MotionState::DETECTED) {
this->set_timeout("clear_motion", 3000, [=] {
this->motion_state = MotionState::CLEAR;
});
if (*this->light_state == LightState::OFF) {
- this->send_command(Command::GET_STATUS);
- }
- ESP_LOGD(TAG, "Motion: %s", MotionState_to_string(*this->motion_state));
- } else if (cmd == Command::SET_TTC) {
- auto seconds = (byte1 << 8) | byte2;
- ESP_LOGD(TAG, "Time to close (TTC): %ds", seconds);
- } else if (cmd == Command::PAIRED_DEVICES) {
- if (nibble == static_cast(PairedDevice::ALL)) {
- this->paired_total = byte2;
- } else if (nibble == static_cast(PairedDevice::REMOTE)) {
- this->paired_remotes = byte2;
- } else if (nibble == static_cast(PairedDevice::KEYPAD)) {
- this->paired_keypads = byte2;
- } else if (nibble == static_cast(PairedDevice::WALL_CONTROL)) {
- this->paired_wall_controls = byte2;
- } else if (nibble == static_cast(PairedDevice::ACCESSORY)) {
- this->paired_accessories = byte2;
+ this->query_status();
}
}
+ }
- return cmd;
+ void RATGDOComponent::received(const LightAction light_action)
+ {
+ ESP_LOGD(TAG, "Light cmd=%s state=%s",
+ LightAction_to_string(light_action),
+ 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", 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", BatteryState_to_string(battery_state));
}
void RATGDOComponent::schedule_door_position_sync(float update_period)
@@ -282,6 +305,9 @@ namespace ratgdo {
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;
+ }
auto count = int(1000 * duration / update_period);
set_retry("position_sync_while_moving", update_period, count, [=](uint8_t r) {
this->door_position_update();
@@ -296,25 +322,14 @@ namespace ratgdo {
}
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::encode_packet(Command command, uint32_t data, bool increment, WirePacket& packet)
- {
- auto cmd = static_cast(command);
- uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
- uint32_t send_data = (data << 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, send_data);
- encode_wireline(*this->rolling_code_counter, fixed, send_data, packet);
-
- if (increment) {
- this->increment_rolling_code_counter();
- }
- }
-
void RATGDOComponent::set_opening_duration(float duration)
{
ESP_LOGD(TAG, "Set opening duration: %.1fs", duration);
@@ -327,39 +342,9 @@ namespace ratgdo {
this->closing_duration = duration;
}
- void RATGDOComponent::set_rolling_code_counter(uint32_t counter)
+ Result RATGDOComponent::call_protocol(Args args)
{
- ESP_LOGV(TAG, "Set rolling code counter to %d", counter);
- this->rolling_code_counter = counter;
- }
-
- void RATGDOComponent::increment_rolling_code_counter(int delta)
- {
- this->rolling_code_counter = (*this->rolling_code_counter + delta) & 0xfffffff;
- }
-
- void RATGDOComponent::print_packet(const WirePacket& packet) const
- {
- ESP_LOG2(TAG, "Packet: [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]",
- packet[0],
- packet[1],
- packet[2],
- packet[3],
- packet[4],
- packet[5],
- packet[6],
- packet[7],
- packet[8],
- packet[9],
- packet[10],
- packet[11],
- packet[12],
- packet[13],
- packet[14],
- packet[15],
- packet[16],
- packet[17],
- packet[18]);
+ return this->protocol_->call(args);
}
/*************************** OBSTRUCTION DETECTION ***************************/
@@ -405,247 +390,54 @@ namespace ratgdo {
}
}
- void RATGDOComponent::gdo_state_loop()
- {
- static bool reading_msg = false;
- static uint32_t msg_start = 0;
- static uint16_t byte_count = 0;
- static WirePacket rx_packet;
- static uint32_t last_read = 0;
-
- if (!reading_msg) {
- while (this->sw_serial_.available()) {
- uint8_t ser_byte = this->sw_serial_.read();
- last_read = millis();
-
- if (ser_byte != 0x55 && ser_byte != 0x01 && ser_byte != 0x00) {
- ESP_LOG2(TAG, "Ignoring byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
- byte_count = 0;
- continue;
- }
- msg_start = ((msg_start << 8) | ser_byte) & 0xffffff;
- byte_count++;
-
- // if we are at the start of a message, capture the next 16 bytes
- if (msg_start == 0x550100) {
- ESP_LOG1(TAG, "Baud: %d", this->sw_serial_.baudRate());
- rx_packet[0] = 0x55;
- rx_packet[1] = 0x01;
- rx_packet[2] = 0x00;
-
- reading_msg = true;
- break;
- }
- }
- }
- if (reading_msg) {
- while (this->sw_serial_.available()) {
- uint8_t ser_byte = this->sw_serial_.read();
- last_read = millis();
- rx_packet[byte_count] = ser_byte;
- byte_count++;
- // ESP_LOG2(TAG, "Received byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
-
- if (byte_count == PACKET_LENGTH) {
- reading_msg = false;
- byte_count = 0;
- this->print_packet(rx_packet);
- this->decode_packet(rx_packet);
- return;
- }
- }
-
- if (millis() - 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", byte_count);
- reading_msg = false;
- byte_count = 0;
- }
- }
- }
-
void RATGDOComponent::query_status()
{
- send_command(Command::GET_STATUS);
+ this->protocol_->call(QueryStatus {});
}
void RATGDOComponent::query_openings()
{
- send_command(Command::GET_OPENINGS);
+ this->protocol_->call(QueryOpenings {});
}
void RATGDOComponent::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;
- set_timeout(timeout, [=] { this->query_paired_devices(kind); });
- }
+ this->protocol_->call(QueryPairedDevicesAll {});
}
void RATGDOComponent::query_paired_devices(PairedDevice kind)
{
- ESP_LOGD(TAG, "Query paired devices of type: %s", PairedDevice_to_string(kind));
- this->send_command(Command::GET_PAIRED_DEVICES, static_cast(kind));
+ this->protocol_->call(QueryPairedDevices { kind });
}
- // wipe devices from memory based on get paired devices nibble values
void RATGDOComponent::clear_paired_devices(PairedDevice kind)
{
- if (kind == PairedDevice::UNKNOWN) {
- return;
- }
- ESP_LOGW(TAG, "Clear paired devices of type: %s", PairedDevice_to_string(kind));
- if (kind == PairedDevice::ALL) {
- set_timeout(200, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::REMOTE)-1); }); // wireless
- set_timeout(400, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::KEYPAD)-1); }); // keypads
- set_timeout(600, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::WALL_CONTROL)-1); }); // wall controls
- set_timeout(800, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::ACCESSORY)-1); }); // accessories
- set_timeout(1000, [=] { this->query_status(); });
- set_timeout(1200, [=] { this->query_paired_devices(); });
- } else {
- this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast(kind) - 1); // just requested device
- set_timeout(200, [=] { this->query_status(); });
- set_timeout(400, [=] { this->query_paired_devices(kind); });
- }
- }
-
- void RATGDOComponent::send_command(Command command, uint32_t data, bool increment)
- {
- ESP_LOG1(TAG, "Send command: %s, data: %08" PRIx32, Command_to_string(command), data);
- if (!this->transmit_pending_) { // have an untransmitted packet
- this->encode_packet(command, data, increment, this->tx_packet_);
- } 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", Command_to_string(command));
- } else {
- ESP_LOGW(TAG, "Not connected to GDO, ignoring command: %s", Command_to_string(command));
- }
- }
- this->transmit_packet();
- }
-
- void RATGDOComponent::send_command(Command command, uint32_t data, bool increment, std::function&& on_sent)
- {
- this->command_sent.then(on_sent);
- this->send_command(command, data, increment);
- }
-
- bool RATGDOComponent::transmit_packet()
- {
- auto now = micros();
-
- while (micros() - now < 1300) {
- if (this->input_gdo_pin_->digital_read()) {
- if (!this->transmit_pending_) {
- this->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) {
- ESP_LOGD(TAG, "Collision detected, waiting to send packet");
- } else {
- this->transmit_pending_start_ = 0; // to indicate GDO not connected state
- }
- }
- return false;
- }
- delayMicroseconds(100);
- }
-
- ESP_LOG2(TAG, "Sending packet");
- this->print_packet(this->tx_packet_);
-
- // indicate the start of a frame by pulling the 12V line low for at leat 1 byte followed by
- // one STOP bit, which indicates to the receiving end that the start of the message follows
- // The output pin is controlling a transistor, so the logic is inverted
- this->output_gdo_pin_->digital_write(true); // pull the line low for at least 1 byte
- delayMicroseconds(1300);
- this->output_gdo_pin_->digital_write(false); // line high for at least 1 bit
- delayMicroseconds(130);
-
- this->sw_serial_.write(this->tx_packet_, PACKET_LENGTH);
- this->transmit_pending_ = false;
- this->transmit_pending_start_ = 0;
- this->command_sent();
- return true;
+ this->protocol_->call(ClearPairedDevices { kind });
}
void RATGDOComponent::sync()
{
- auto sync_step = [=]() {
- if (*this->door_state == DoorState::UNKNOWN) {
- this->send_command(Command::GET_STATUS);
- return RetryResult::RETRY;
- }
- if (*this->openings == 0) {
- this->send_command(Command::GET_OPENINGS);
- return RetryResult::RETRY;
- }
- if (*this->paired_total == PAIRED_DEVICES_UNKNOWN) {
- this->query_paired_devices(PairedDevice::ALL);
- return RetryResult::RETRY;
- }
- if (*this->paired_remotes == PAIRED_DEVICES_UNKNOWN) {
- this->query_paired_devices(PairedDevice::REMOTE);
- return RetryResult::RETRY;
- }
- if (*this->paired_keypads == PAIRED_DEVICES_UNKNOWN) {
- this->query_paired_devices(PairedDevice::KEYPAD);
- return RetryResult::RETRY;
- }
- if (*this->paired_wall_controls == PAIRED_DEVICES_UNKNOWN) {
- this->query_paired_devices(PairedDevice::WALL_CONTROL);
- return RetryResult::RETRY;
- }
- if (*this->paired_accessories == PAIRED_DEVICES_UNKNOWN) {
- this->query_paired_devices(PairedDevice::ACCESSORY);
- return RetryResult::RETRY;
- }
- return RetryResult::DONE;
- };
-
- const uint8_t MAX_ATTEMPTS = 10;
- set_retry(
- 500, MAX_ATTEMPTS, [=](uint8_t r) {
- auto result = sync_step();
- if (result == RetryResult::RETRY) {
- if (r == MAX_ATTEMPTS - 2 && *this->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);
- }
- if (r == 0) {
- // this was last attempt, notify of sync failure
- ESP_LOGD(TAG, "Triggering sync failed actions.");
- this->sync_failed = true;
- }
- }
- return result;
- },
- 1.5f);
+ this->protocol_->sync();
}
- void RATGDOComponent::open_door()
+ void RATGDOComponent::door_open()
{
if (*this->door_state == DoorState::OPENING) {
return; // gets ignored by opener
}
- this->door_command(data::DOOR_OPEN);
+ this->door_action(DoorAction::OPEN);
+
+ // query state in case we don't get a status message
+ set_timeout("door_query_state", (*this->opening_duration + 1) * 1000, [=]() {
+ if (*this->door_state != DoorState::OPEN && *this->door_state != DoorState::STOPPED) {
+ this->door_state = 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::close_door()
+ void RATGDOComponent::door_close()
{
if (*this->door_state == DoorState::CLOSING) {
return; // gets ignored by opener
@@ -653,10 +445,10 @@ namespace ratgdo {
if (*this->door_state == DoorState::OPENING) {
// have to stop door first, otherwise close command is ignored
- this->door_command(data::DOOR_STOP);
- this->door_state_received.then([=](DoorState s) {
+ this->door_action(DoorAction::STOP);
+ this->on_door_state_([=](DoorState s) {
if (s == DoorState::STOPPED) {
- this->door_command(data::DOOR_CLOSE);
+ this->door_action(DoorAction::CLOSE);
} else {
ESP_LOGW(TAG, "Door did not stop, ignoring close command");
}
@@ -664,28 +456,41 @@ namespace ratgdo {
return;
}
- this->door_command(data::DOOR_CLOSE);
+ this->door_action(DoorAction::CLOSE);
+
+ // query state in case we don't get a status message
+ set_timeout("door_query_state", (*this->closing_duration + 1) * 1000, [=]() {
+ if (*this->door_state != DoorState::CLOSED && *this->door_state != DoorState::STOPPED) {
+ this->door_state = 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::stop_door()
+ 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_command(data::DOOR_STOP);
+ this->door_action(DoorAction::STOP);
}
- void RATGDOComponent::toggle_door()
+ void RATGDOComponent::door_toggle()
{
- this->door_command(data::DOOR_TOGGLE);
+ this->door_action(DoorAction::TOGGLE);
+ }
+
+ void RATGDOComponent::door_action(DoorAction action)
+ {
+ this->protocol_->door_action(action);
}
void RATGDOComponent::door_move_to_position(float position)
{
if (*this->door_state == DoorState::OPENING || *this->door_state == DoorState::CLOSING) {
- this->door_command(data::DOOR_STOP);
- this->door_state_received.then([=](DoorState s) {
+ this->door_action(DoorAction::STOP);
+ this->on_door_state_([=](DoorState s) {
if (s == DoorState::STOPPED) {
this->door_move_to_position(position);
}
@@ -709,9 +514,9 @@ namespace ratgdo {
this->door_move_delta = delta;
ESP_LOGD(TAG, "Moving to position %.2f in %.1fs", position, operation_time / 1000.0);
- this->door_command(delta > 0 ? data::DOOR_OPEN : data::DOOR_CLOSE);
+ this->door_action(delta > 0 ? DoorAction::OPEN : DoorAction::CLOSE);
set_timeout("move_to_position", operation_time, [=] {
- this->ensure_door_command(data::DOOR_STOP);
+ this->door_action(DoorAction::STOP);
});
}
@@ -728,80 +533,22 @@ namespace ratgdo {
}
}
- void RATGDOComponent::door_command(uint32_t data)
- {
- data |= (1 << 16); // button 1 ?
- data |= (1 << 8); // button press
- this->send_command(Command::DOOR_ACTION, data, false, [=]() {
- set_timeout(100, [=] {
- auto data2 = data & ~(1 << 8); // button release
- this->send_command(Command::DOOR_ACTION, data2);
- });
- });
- }
-
- void RATGDOComponent::ensure_door_command(uint32_t data, uint32_t delay)
- {
- if (data == data::DOOR_TOGGLE) {
- ESP_LOGW(TAG, "It's not recommended to use ensure_door_command with non-idempotent commands such as DOOR_TOGGLE");
- }
- auto prev_door_state = *this->door_state;
- this->door_state_received.then([=](DoorState s) {
- if ((data == data::DOOR_STOP) && (s != DoorState::STOPPED) && !(prev_door_state == DoorState::OPENING && s == DoorState::OPEN) && !(prev_door_state == DoorState::CLOSING && s == DoorState::CLOSED)) {
- return;
- }
- if (data == data::DOOR_OPEN && !(s == DoorState::OPENING || s == DoorState::OPEN)) {
- return;
- }
- if (data == data::DOOR_CLOSE && !(s == DoorState::CLOSED || s == DoorState::CLOSING)) {
- return;
- }
-
- ESP_LOG1(TAG, "Received door status, cancel door command retry");
- cancel_timeout("door_command_retry");
- });
- this->door_command(data);
- ESP_LOG1(TAG, "Ensure door command, setup door command retry");
- set_timeout("door_command_retry", delay, [=]() {
- this->ensure_door_command(data);
- });
- }
-
void RATGDOComponent::light_on()
{
this->light_state = LightState::ON;
- this->send_command(Command::LIGHT, data::LIGHT_ON);
+ this->protocol_->light_action(LightAction::ON);
}
void RATGDOComponent::light_off()
{
this->light_state = LightState::OFF;
- this->send_command(Command::LIGHT, data::LIGHT_OFF);
+ this->protocol_->light_action(LightAction::OFF);
}
- void RATGDOComponent::toggle_light()
+ void RATGDOComponent::light_toggle()
{
this->light_state = light_state_toggle(*this->light_state);
- this->send_command(Command::LIGHT, data::LIGHT_TOGGLE);
- }
-
- // Lock functions
- void RATGDOComponent::lock()
- {
- this->lock_state = LockState::LOCKED;
- this->send_command(Command::LOCK, data::LOCK_ON);
- }
-
- void RATGDOComponent::unlock()
- {
- this->lock_state = LockState::UNLOCKED;
- this->send_command(Command::LOCK, data::LOCK_OFF);
- }
-
- void RATGDOComponent::toggle_lock()
- {
- this->lock_state = lock_state_toggle(*this->lock_state);
- this->send_command(Command::LOCK, data::LOCK_TOGGLE);
+ this->protocol_->light_action(LightAction::TOGGLE);
}
LightState RATGDOComponent::get_light_state() const
@@ -809,28 +556,44 @@ namespace ratgdo {
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()
{
- // Send LEARN with nibble = 0 then nibble = 1 to mimic wall control learn button
- this->send_command(Command::LEARN, 0);
- set_timeout(150, [=] { this->send_command(Command::LEARN, 1); });
- set_timeout(500, [=] { this->send_command(Command::GET_STATUS); });
+ this->protocol_->call(ActivateLearn {});
}
void RATGDOComponent::inactivate_learn()
{
- // Send LEARN twice with nibble = 0 to inactivate learn and get status to update switch state
- this->send_command(Command::LEARN, 0);
- set_timeout(150, [=] { this->send_command(Command::LEARN, 0); });
- set_timeout(500, [=] { this->send_command(Command::GET_STATUS); });
+ this->protocol_->call(InactivateLearn {});
}
void RATGDOComponent::subscribe_rolling_code_counter(std::function&& f)
{
// change update to children is defered until after component loop
// if multiple changes occur during component loop, only the last one is notified
- this->rolling_code_counter.subscribe([=](uint32_t state) { defer("rolling_code_counter", [=] { f(state); }); });
+ auto counter = this->protocol_->call(GetRollingCodeCounter {});
+ if (counter.tag == Result::Tag::rolling_code_counter) {
+ counter.value.rolling_code_counter.value->subscribe([=](uint32_t state) { defer("rolling_code_counter", [=] { f(state); }); });
+ }
}
void RATGDOComponent::subscribe_opening_duration(std::function&& f)
{
diff --git a/components/ratgdo/ratgdo.h b/components/ratgdo/ratgdo.h
index 7ee555a..510f636 100644
--- a/components/ratgdo/ratgdo.h
+++ b/components/ratgdo/ratgdo.h
@@ -12,86 +12,28 @@
************************************/
#pragma once
-#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
-#include "callbacks.h"
-#include "enum.h"
+
#include "esphome/core/component.h"
-#include "esphome/core/gpio.h"
-#include "esphome/core/log.h"
+#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
+
+#include "callbacks.h"
+#include "macros.h"
#include "observable.h"
-
-extern "C" {
-#include "secplus.h"
-}
-
+#include "protocol.h"
#include "ratgdo_state.h"
namespace esphome {
+class InternalGPIOPin;
namespace ratgdo {
class RATGDOComponent;
typedef Parented RATGDOClient;
- static const uint8_t PACKET_LENGTH = 19;
- typedef uint8_t WirePacket[PACKET_LENGTH];
-
const float DOOR_POSITION_UNKNOWN = -1.0;
const float DOOR_DELTA_UNKNOWN = -2.0;
const uint16_t PAIRED_DEVICES_UNKNOWN = 0xFF;
- namespace data {
- const uint32_t LIGHT_OFF = 0;
- const uint32_t LIGHT_ON = 1;
- const uint32_t LIGHT_TOGGLE = 2;
- const uint32_t LIGHT_TOGGLE2 = 3;
-
- const uint32_t LOCK_OFF = 0;
- const uint32_t LOCK_ON = 1;
- const uint32_t LOCK_TOGGLE = 2;
-
- const uint32_t DOOR_CLOSE = 0;
- const uint32_t DOOR_OPEN = 1;
- const uint32_t DOOR_TOGGLE = 2;
- const uint32_t DOOR_STOP = 3;
- }
-
- ENUM(Command, 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?
- (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 Command& cmd_e) { return cmd_i == static_cast(cmd_e); }
- inline bool operator==(const Command& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast(cmd_e); }
-
struct RATGDOStore {
int obstruction_low_count = 0; // count obstruction low pulses
@@ -101,13 +43,18 @@ namespace ratgdo {
}
};
+ using protocol::Args;
+ using protocol::Result;
+
class RATGDOComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
- observable rolling_code_counter { 0 };
+ void init_protocol();
+
+ void obstruction_loop();
float start_opening { -1 };
observable opening_duration { 0 };
@@ -136,35 +83,38 @@ namespace ratgdo {
observable motion_state { MotionState::UNKNOWN };
observable learn_state { LearnState::UNKNOWN };
- OnceCallbacks door_state_received;
- OnceCallbacks command_sent;
+ OnceCallbacks on_door_state_;
observable 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_client_id(uint64_t client_id) { this->client_id_ = client_id & 0xFFFFFFFF; }
- void gdo_state_loop();
- uint16_t decode_packet(const WirePacket& packet);
- void obstruction_loop();
- void send_command(Command command, uint32_t data = 0, bool increment = true);
- void send_command(Command command, uint32_t data, bool increment, std::function&& on_sent);
- bool transmit_packet();
- void encode_packet(Command command, uint32_t data, bool increment, WirePacket& packet);
- void print_packet(const WirePacket& packet) const;
+ Result call_protocol(Args args);
- void increment_rolling_code_counter(int delta = 1);
- void set_rolling_code_counter(uint32_t code);
+ 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_command(uint32_t data);
- void ensure_door_command(uint32_t data, uint32_t delay = 1500);
- void toggle_door();
- void open_door();
- void close_door();
- void stop_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);
@@ -174,13 +124,13 @@ namespace ratgdo {
void cancel_position_sync_callbacks();
// light
- void toggle_light();
+ void light_toggle();
void light_on();
void light_off();
LightState get_light_state() const;
// lock
- void toggle_lock();
+ void lock_toggle();
void lock();
void unlock();
@@ -217,21 +167,13 @@ namespace ratgdo {
void subscribe_learn_state(std::function&& f);
protected:
- // tx data
- bool transmit_pending_ { false };
- uint32_t transmit_pending_start_ { 0 };
- WirePacket tx_packet_;
-
RATGDOStore isr_store_ {};
- SoftwareSerial sw_serial_;
-
+ protocol::Protocol* protocol_;
bool obstruction_from_status_ { false };
InternalGPIOPin* output_gdo_pin_;
InternalGPIOPin* input_gdo_pin_;
InternalGPIOPin* input_obst_pin_;
- uint64_t client_id_ { 0x539 };
-
}; // RATGDOComponent
} // namespace ratgdo
diff --git a/components/ratgdo/ratgdo_state.h b/components/ratgdo/ratgdo_state.h
index 891e270..f71440a 100644
--- a/components/ratgdo/ratgdo_state.h
+++ b/components/ratgdo/ratgdo_state.h
@@ -12,7 +12,7 @@
************************************/
#pragma once
-#include "enum.h"
+#include "macros.h"
#include
namespace esphome {
@@ -64,6 +64,11 @@ namespace ratgdo {
(RELEASED, 1),
(UNKNOWN, 2))
+ ENUM(BatteryState, uint8_t,
+ (UNKNOWN, 0),
+ (CHARGING, 0x6),
+ (FULL, 0x8))
+
/// Enum for learn states.
ENUM(LearnState, uint8_t,
(INACTIVE, 0),
@@ -79,5 +84,39 @@ namespace ratgdo {
(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))
+
+ struct Openings {
+ uint16_t count;
+ uint8_t flag;
+ };
+
+ struct PairedDeviceCount {
+ PairedDevice kind;
+ uint16_t count;
+ };
+
+ struct TimeToClose {
+ uint16_t seconds;
+ };
+
} // namespace ratgdo
} // namespace esphome
diff --git a/components/ratgdo/secplus1.cpp b/components/ratgdo/secplus1.cpp
new file mode 100644
index 0000000..ba5caf3
--- /dev/null
+++ b/components/ratgdo/secplus1.cpp
@@ -0,0 +1,452 @@
+
+#include "secplus1.h"
+#include "ratgdo.h"
+
+#include "esphome/core/gpio.h"
+#include "esphome/core/log.h"
+#include "esphome/core/scheduler.h"
+
+namespace esphome {
+namespace ratgdo {
+ namespace secplus1 {
+
+ static const char* const TAG = "ratgdo_secplus1";
+
+ void Secplus1::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->sw_serial_.begin(1200, SWSERIAL_8E1, rx_pin->get_pin(), tx_pin->get_pin(), true);
+
+ this->traits_.set_features(HAS_DOOR_STATUS | HAS_LIGHT_TOGGLE | HAS_LOCK_TOGGLE);
+ }
+
+ void Secplus1::loop()
+ {
+ auto rx_cmd = this->read_command();
+ if (rx_cmd) {
+ this->handle_command(rx_cmd.value());
+ }
+ auto tx_cmd = this->pending_tx();
+ if (
+ (millis() - this->last_tx_) > 200 && // don't send twice in a period
+ (millis() - this->last_rx_) > 50 && // time to send it
+ tx_cmd && // have pending command
+ !(this->is_0x37_panel_ && tx_cmd.value() == CommandType::TOGGLE_LOCK_PRESS) && this->wall_panel_emulation_state_ != WallPanelEmulationState::RUNNING) {
+ this->do_transmit_if_pending();
+ }
+ }
+
+ void Secplus1::dump_config()
+ {
+ ESP_LOGCONFIG(TAG, " Protocol: SEC+ v1");
+ }
+
+ void Secplus1::sync()
+ {
+ this->wall_panel_emulation_state_ = WallPanelEmulationState::WAITING;
+ this->wall_panel_emulation_start_ = millis();
+ this->door_state = DoorState::UNKNOWN;
+ this->light_state = LightState::UNKNOWN;
+ this->scheduler_->cancel_timeout(this->ratgdo_, "wall_panel_emulation");
+ this->wall_panel_emulation();
+
+ this->scheduler_->set_timeout(this->ratgdo_, "", 45000, [=] {
+ if (this->door_state == DoorState::UNKNOWN) {
+ ESP_LOGW(TAG, "Triggering sync failed actions.");
+ this->ratgdo_->sync_failed = true;
+ }
+ });
+ }
+
+ void Secplus1::wall_panel_emulation(size_t index)
+ {
+ if (this->wall_panel_emulation_state_ == WallPanelEmulationState::WAITING) {
+ ESP_LOG1(TAG, "Looking for security+ 1.0 wall panel...");
+
+ if (this->door_state != DoorState::UNKNOWN || this->light_state != LightState::UNKNOWN) {
+ ESP_LOG1(TAG, "Wall panel detected");
+ return;
+ }
+ if (millis() - this->wall_panel_emulation_start_ > 35000 && !this->wall_panel_starting_) {
+ ESP_LOG1(TAG, "No wall panel detected. Switching to emulation mode.");
+ this->wall_panel_emulation_state_ = WallPanelEmulationState::RUNNING;
+ }
+ this->scheduler_->set_timeout(this->ratgdo_, "wall_panel_emulation", 2000, [=] {
+ this->wall_panel_emulation();
+ });
+ return;
+ } else if (this->wall_panel_emulation_state_ == WallPanelEmulationState::RUNNING) {
+ // ESP_LOG2(TAG, "[Wall panel emulation] Sending byte: [%02X]", secplus1_states[index]);
+
+ if (index < 15 || !this->do_transmit_if_pending()) {
+ this->transmit_byte(secplus1_states[index]);
+ // gdo response simulation for testing
+ // auto resp = secplus1_states[index] == 0x39 ? 0x00 :
+ // secplus1_states[index] == 0x3A ? 0x5C :
+ // secplus1_states[index] == 0x38 ? 0x52 : 0xFF;
+ // if (resp != 0xFF) {
+ // this->transmit_byte(resp, true);
+ // }
+
+ index += 1;
+ if (index == 18) {
+ index = 15;
+ }
+ }
+ this->scheduler_->set_timeout(this->ratgdo_, "wall_panel_emulation", 250, [=] {
+ this->wall_panel_emulation(index);
+ });
+ }
+ }
+
+ void Secplus1::light_action(LightAction action)
+ {
+ ESP_LOG1(TAG, "Light action: %s", LightAction_to_string(action));
+ if (action == LightAction::UNKNOWN) {
+ return;
+ }
+ if (
+ action == LightAction::TOGGLE || (action == LightAction::ON && this->light_state == LightState::OFF) || (action == LightAction::OFF && this->light_state == LightState::ON)) {
+ this->toggle_light();
+ }
+ }
+
+ void Secplus1::lock_action(LockAction action)
+ {
+ ESP_LOG1(TAG, "Lock action: %s", LockAction_to_string(action));
+ if (action == LockAction::UNKNOWN) {
+ return;
+ }
+ if (
+ action == LockAction::TOGGLE || (action == LockAction::LOCK && this->lock_state == LockState::UNLOCKED) || (action == LockAction::UNLOCK && this->lock_state == LockState::LOCKED)) {
+ this->toggle_lock();
+ }
+ }
+
+ void Secplus1::door_action(DoorAction action)
+ {
+ ESP_LOG1(TAG, "Door action: %s, door state: %s", DoorAction_to_string(action), DoorState_to_string(this->door_state));
+ if (action == DoorAction::UNKNOWN) {
+ return;
+ }
+
+ const uint32_t double_toggle_delay = 1000;
+ if (action == DoorAction::TOGGLE) {
+ this->toggle_door();
+ } else if (action == DoorAction::OPEN) {
+ if (this->door_state == DoorState::CLOSED || this->door_state == DoorState::CLOSING) {
+ this->toggle_door();
+ } else if (this->door_state == DoorState::STOPPED) {
+ this->toggle_door(); // this starts closing door
+ this->on_door_state_([=](DoorState s) {
+ if (s == DoorState::CLOSING) {
+ // this changes direction of the door on some openers, on others it stops it
+ this->toggle_door();
+ this->on_door_state_([=](DoorState s) {
+ if (s == DoorState::STOPPED) {
+ this->toggle_door();
+ }
+ });
+ }
+ });
+ }
+ } else if (action == DoorAction::CLOSE) {
+ if (this->door_state == DoorState::OPEN) {
+ this->toggle_door();
+ } else if (this->door_state == DoorState::OPENING) {
+ this->toggle_door(); // this switches to stopped
+ // another toggle needed to close
+ this->on_door_state_([=](DoorState s) {
+ if (s == DoorState::STOPPED) {
+ this->toggle_door();
+ }
+ });
+ } else if (this->door_state == DoorState::STOPPED) {
+ this->toggle_door();
+ }
+ } else if (action == DoorAction::STOP) {
+ if (this->door_state == DoorState::OPENING) {
+ this->toggle_door();
+ } else if (this->door_state == DoorState::CLOSING) {
+ this->toggle_door(); // this switches to opening
+
+ // another toggle needed to stop
+ this->on_door_state_([=](DoorState s) {
+ if (s == DoorState::OPENING) {
+ this->toggle_door();
+ }
+ });
+ }
+ }
+ }
+
+ void Secplus1::toggle_light()
+ {
+ this->enqueue_transmit(CommandType::TOGGLE_LIGHT_PRESS);
+ }
+
+ void Secplus1::toggle_lock()
+ {
+ this->enqueue_transmit(CommandType::TOGGLE_LOCK_PRESS);
+ }
+
+ void Secplus1::toggle_door()
+ {
+ this->enqueue_transmit(CommandType::TOGGLE_DOOR_PRESS);
+ this->enqueue_transmit(CommandType::QUERY_DOOR_STATUS);
+ if (this->door_state == DoorState::STOPPED || this->door_state == DoorState::OPEN || this->door_state == DoorState::CLOSED) {
+ this->door_moving_ = true;
+ }
+ }
+
+ Result Secplus1::call(Args args)
+ {
+ return {};
+ }
+
+ optional Secplus1::read_command()
+ {
+ static bool reading_msg = false;
+ static uint32_t msg_start = 0;
+ static uint16_t byte_count = 0;
+ static RxPacket rx_packet;
+
+ if (!reading_msg) {
+ while (this->sw_serial_.available()) {
+ uint8_t ser_byte = this->sw_serial_.read();
+ this->last_rx_ = millis();
+
+ if (ser_byte < 0x30 || ser_byte > 0x3A) {
+ ESP_LOG2(TAG, "[%d] Ignoring byte [%02X], baud: %d", millis(), ser_byte, this->sw_serial_.baudRate());
+ byte_count = 0;
+ continue;
+ }
+ rx_packet[byte_count++] = ser_byte;
+ ESP_LOG2(TAG, "[%d] Received byte: [%02X]", millis(), ser_byte);
+ reading_msg = true;
+
+ if (ser_byte == 0x37 || (ser_byte >= 0x30 && ser_byte <= 0x35)) {
+ rx_packet[byte_count++] = 0;
+ reading_msg = false;
+ byte_count = 0;
+ ESP_LOG2(TAG, "[%d] Received command: [%02X]", millis(), rx_packet[0]);
+ return this->decode_packet(rx_packet);
+ }
+
+ break;
+ }
+ }
+ if (reading_msg) {
+ while (this->sw_serial_.available()) {
+ uint8_t ser_byte = this->sw_serial_.read();
+ this->last_rx_ = millis();
+ rx_packet[byte_count++] = ser_byte;
+ ESP_LOG2(TAG, "[%d] Received byte: [%02X]", millis(), ser_byte);
+
+ if (byte_count == RX_LENGTH) {
+ reading_msg = false;
+ byte_count = 0;
+ this->print_rx_packet(rx_packet);
+ return this->decode_packet(rx_packet);
+ }
+ }
+
+ if (millis() - this->last_rx_ > 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, "[%d] Discard incomplete packet: [%02X ...]", millis(), rx_packet[0]);
+ reading_msg = false;
+ byte_count = 0;
+ }
+ }
+
+ return {};
+ }
+
+ void Secplus1::print_rx_packet(const RxPacket& packet) const
+ {
+ ESP_LOG2(TAG, "[%d] Received packet: [%02X %02X]", millis(), packet[0], packet[1]);
+ }
+
+ void Secplus1::print_tx_packet(const TxPacket& packet) const
+ {
+ ESP_LOG2(TAG, "[%d] Sending packet: [%02X %02X]", millis(), packet[0], packet[1]);
+ }
+
+ optional Secplus1::decode_packet(const RxPacket& packet) const
+ {
+ CommandType cmd_type = to_CommandType(packet[0], CommandType::UNKNOWN);
+ return RxCommand { cmd_type, packet[1] };
+ }
+
+ // unknown meaning of observed command-responses:
+ // 40 00 and 40 80
+ // 53 01
+ // C0 3F
+ // F8 3F
+ // FE 3F
+
+ void Secplus1::handle_command(const RxCommand& cmd)
+ {
+ if (cmd.req == CommandType::QUERY_DOOR_STATUS) {
+
+ DoorState door_state;
+ auto val = cmd.resp & 0x7;
+ // 000 0x0 stopped
+ // 001 0x1 opening
+ // 010 0x2 open
+ // 100 0x4 closing
+ // 101 0x5 closed
+ // 110 0x6 stopped
+
+ if (val == 0x2) {
+ door_state = DoorState::OPEN;
+ } else if (val == 0x5) {
+ door_state = DoorState::CLOSED;
+ } else if (val == 0x0 || val == 0x6) {
+ door_state = DoorState::STOPPED;
+ } else if (val == 0x1) {
+ door_state = DoorState::OPENING;
+ } else if (val == 0x4) {
+ door_state = DoorState::CLOSING;
+ } else {
+ door_state = DoorState::UNKNOWN;
+ }
+
+ if (this->maybe_door_state != door_state) {
+ this->on_door_state_.trigger(door_state);
+ }
+
+ if (!this->is_0x37_panel_ && door_state != this->maybe_door_state) {
+ this->maybe_door_state = door_state;
+ ESP_LOG1(TAG, "Door maybe %s, waiting for 2nd status message to confirm", DoorState_to_string(door_state));
+ } else {
+ this->maybe_door_state = door_state;
+ this->door_state = door_state;
+ if (this->door_state == DoorState::STOPPED || this->door_state == DoorState::OPEN || this->door_state == DoorState::CLOSED) {
+ this->door_moving_ = false;
+ }
+ this->ratgdo_->received(door_state);
+ }
+ } else if (cmd.req == CommandType::QUERY_DOOR_STATUS_0x37) {
+ this->is_0x37_panel_ = true;
+ auto cmd = this->pending_tx();
+ if (cmd && cmd.value() == CommandType::TOGGLE_LOCK_PRESS) {
+ this->do_transmit_if_pending();
+ } else {
+ // inject door status request
+ if (door_moving_ || (millis() - this->last_status_query_ > 10000)) {
+ this->transmit_byte(static_cast(CommandType::QUERY_DOOR_STATUS));
+ this->last_status_query_ = millis();
+ }
+ }
+ } else if (cmd.req == CommandType::QUERY_OTHER_STATUS) {
+ LightState light_state = to_LightState((cmd.resp >> 2) & 1, LightState::UNKNOWN);
+
+ if (!this->is_0x37_panel_ && light_state != this->maybe_light_state) {
+ this->maybe_light_state = light_state;
+ } else {
+ this->light_state = light_state;
+ this->ratgdo_->received(light_state);
+ }
+
+ LockState lock_state = to_LockState((~cmd.resp >> 3) & 1, LockState::UNKNOWN);
+ if (!this->is_0x37_panel_ && lock_state != this->maybe_lock_state) {
+ this->maybe_lock_state = lock_state;
+ } else {
+ this->lock_state = lock_state;
+ this->ratgdo_->received(lock_state);
+ }
+ } else if (cmd.req == CommandType::OBSTRUCTION) {
+ ObstructionState obstruction_state = cmd.resp == 0 ? ObstructionState::CLEAR : ObstructionState::OBSTRUCTED;
+ this->ratgdo_->received(obstruction_state);
+ } else if (cmd.req == CommandType::TOGGLE_DOOR_RELEASE) {
+ if (cmd.resp == 0x31) {
+ this->wall_panel_starting_ = true;
+ }
+ } else if (cmd.req == CommandType::TOGGLE_LIGHT_PRESS) {
+ // motion was detected, or the light toggle button was pressed
+ // either way it's ok to trigger motion detection
+ if (this->light_state == LightState::OFF) {
+ this->ratgdo_->received(MotionState::DETECTED);
+ }
+ } else if (cmd.req == CommandType::TOGGLE_DOOR_PRESS) {
+ this->ratgdo_->received(ButtonState::PRESSED);
+ } else if (cmd.req == CommandType::TOGGLE_DOOR_RELEASE) {
+ this->ratgdo_->received(ButtonState::RELEASED);
+ }
+ }
+
+ bool Secplus1::do_transmit_if_pending()
+ {
+ auto cmd = this->pop_pending_tx();
+ if (cmd) {
+ this->enqueue_command_pair(cmd.value());
+ this->transmit_byte(static_cast(cmd.value()));
+ }
+ return cmd;
+ }
+
+ void Secplus1::enqueue_command_pair(CommandType cmd)
+ {
+ auto now = millis();
+ if (cmd == CommandType::TOGGLE_DOOR_PRESS) {
+ this->enqueue_transmit(CommandType::TOGGLE_DOOR_RELEASE, now + 500);
+ } else if (cmd == CommandType::TOGGLE_LIGHT_PRESS) {
+ this->enqueue_transmit(CommandType::TOGGLE_LIGHT_RELEASE, now + 500);
+ } else if (cmd == CommandType::TOGGLE_LOCK_PRESS) {
+ this->enqueue_transmit(CommandType::TOGGLE_LOCK_RELEASE, now + 3500);
+ };
+ }
+
+ void Secplus1::enqueue_transmit(CommandType cmd, uint32_t time)
+ {
+ if (time == 0) {
+ time = millis();
+ }
+ this->pending_tx_.push(TxCommand { cmd, time });
+ }
+
+ optional Secplus1::pending_tx()
+ {
+ if (this->pending_tx_.empty()) {
+ return {};
+ }
+ auto cmd = this->pending_tx_.top();
+ if (cmd.time > millis()) {
+ return {};
+ }
+ return cmd.request;
+ }
+
+ optional Secplus1::pop_pending_tx()
+ {
+ auto cmd = this->pending_tx();
+ if (cmd) {
+ this->pending_tx_.pop();
+ }
+ return cmd;
+ }
+
+ void Secplus1::transmit_byte(uint32_t value)
+ {
+ bool enable_rx = (value == 0x38) || (value == 0x39) || (value == 0x3A);
+ if (!enable_rx) {
+ this->sw_serial_.enableIntTx(false);
+ }
+ this->sw_serial_.write(value);
+ this->last_tx_ = millis();
+ if (!enable_rx) {
+ this->sw_serial_.enableIntTx(true);
+ }
+ ESP_LOG2(TAG, "[%d] Sent byte: [%02X]", millis(), value);
+ }
+
+ } // namespace secplus1
+} // namespace ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/secplus1.h b/components/ratgdo/secplus1.h
new file mode 100644
index 0000000..42d83fd
--- /dev/null
+++ b/components/ratgdo/secplus1.h
@@ -0,0 +1,156 @@
+#pragma once
+
+#include
+
+#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
+#include "esphome/core/optional.h"
+
+#include "callbacks.h"
+#include "observable.h"
+#include "protocol.h"
+#include "ratgdo_state.h"
+
+namespace esphome {
+
+class Scheduler;
+class InternalGPIOPin;
+
+namespace ratgdo {
+ namespace secplus1 {
+
+ using namespace esphome::ratgdo::protocol;
+
+ static const uint8_t RX_LENGTH = 2;
+ typedef uint8_t RxPacket[RX_LENGTH];
+
+ static const uint8_t TX_LENGTH = 2;
+ typedef uint8_t TxPacket[TX_LENGTH];
+
+ static const TxPacket toggle_door = { 0x30, 0x31 };
+ static const TxPacket toggle_light = { 0x32, 0x33 };
+ static const TxPacket toggle_lock = { 0x34, 0x35 };
+
+ static const uint8_t secplus1_states[] = { 0x35, 0x35, 0x35, 0x35, 0x33, 0x33, 0x53, 0x53, 0x38, 0x3A, 0x3A, 0x3A, 0x39, 0x38, 0x3A, 0x38, 0x3A, 0x39, 0x3A };
+
+ ENUM(CommandType, uint16_t,
+ (TOGGLE_DOOR_PRESS, 0x30),
+ (TOGGLE_DOOR_RELEASE, 0x31),
+ (TOGGLE_LIGHT_PRESS, 0x32),
+ (TOGGLE_LIGHT_RELEASE, 0x33),
+ (TOGGLE_LOCK_PRESS, 0x34),
+ (TOGGLE_LOCK_RELEASE, 0x35),
+ (QUERY_DOOR_STATUS_0x37, 0x37),
+ (QUERY_DOOR_STATUS, 0x38),
+ (OBSTRUCTION, 0x39),
+ (QUERY_OTHER_STATUS, 0x3A),
+ (UNKNOWN, 0xFF), )
+
+ struct RxCommand {
+ CommandType req;
+ uint8_t resp;
+
+ RxCommand()
+ : req(CommandType::UNKNOWN)
+ , resp(0)
+ {
+ }
+ RxCommand(CommandType req_)
+ : req(req_)
+ , resp(0)
+ {
+ }
+ RxCommand(CommandType req_, uint8_t resp_ = 0)
+ : req(req_)
+ , resp(resp_)
+ {
+ }
+ };
+
+ struct TxCommand {
+ CommandType request;
+ uint32_t time;
+ };
+
+ struct FirstToSend {
+ bool operator()(const TxCommand l, const TxCommand r) const { return l.time > r.time; }
+ };
+
+ enum class WallPanelEmulationState {
+ WAITING,
+ RUNNING,
+ };
+
+ class Secplus1 : public Protocol {
+ public:
+ void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
+ void loop();
+ void dump_config();
+
+ 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_; }
+
+ protected:
+ void wall_panel_emulation(size_t index = 0);
+
+ optional read_command();
+ void handle_command(const RxCommand& cmd);
+
+ void print_rx_packet(const RxPacket& packet) const;
+ void print_tx_packet(const TxPacket& packet) const;
+ optional decode_packet(const RxPacket& packet) const;
+
+ void enqueue_transmit(CommandType cmd, uint32_t time = 0);
+ optional pending_tx();
+ optional pop_pending_tx();
+ bool do_transmit_if_pending();
+ void enqueue_command_pair(CommandType cmd);
+ void transmit_byte(uint32_t value);
+
+ void toggle_light();
+ void toggle_lock();
+ void toggle_door();
+ void query_status();
+
+ LightState light_state { LightState::UNKNOWN };
+ LockState lock_state { LockState::UNKNOWN };
+ DoorState door_state { DoorState::UNKNOWN };
+
+ LightState maybe_light_state { LightState::UNKNOWN };
+ LockState maybe_lock_state { LockState::UNKNOWN };
+ DoorState maybe_door_state { DoorState::UNKNOWN };
+
+ OnceCallbacks on_door_state_;
+
+ bool door_moving_ { false };
+
+ bool wall_panel_starting_ { false };
+ uint32_t wall_panel_emulation_start_ { 0 };
+ WallPanelEmulationState wall_panel_emulation_state_ { WallPanelEmulationState::WAITING };
+
+ bool is_0x37_panel_ { false };
+ std::priority_queue, FirstToSend> pending_tx_;
+ uint32_t last_rx_ { 0 };
+ uint32_t last_tx_ { 0 };
+ uint32_t last_status_query_ { 0 };
+
+ Traits traits_;
+
+ SoftwareSerial sw_serial_;
+
+ InternalGPIOPin* tx_pin_;
+ InternalGPIOPin* rx_pin_;
+
+ RATGDOComponent* ratgdo_;
+ Scheduler* scheduler_;
+ };
+
+ } // namespace secplus1
+} // namespace ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/secplus2.cpp b/components/ratgdo/secplus2.cpp
new file mode 100644
index 0000000..ac8541b
--- /dev/null
+++ b/components/ratgdo/secplus2.cpp
@@ -0,0 +1,512 @@
+
+#include "secplus2.h"
+#include "ratgdo.h"
+
+#include "esphome/core/gpio.h"
+#include "esphome/core/log.h"
+#include "esphome/core/scheduler.h"
+
+extern "C" {
+#include "secplus.h"
+}
+
+namespace esphome {
+namespace ratgdo {
+ namespace secplus2 {
+
+ // MAX_CODES_WITHOUT_FLASH_WRITE is a bit of a guess
+ // since we write the flash at most every every 5s
+ //
+ // 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 = 10;
+
+ 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->sw_serial_.begin(9600, SWSERIAL_8N1, rx_pin->get_pin(), tx_pin->get_pin(), true);
+ this->sw_serial_.enableIntTx(false);
+ this->sw_serial_.enableAutoBaud(true);
+
+ this->traits_.set_features(Traits::all());
+ }
+
+ void Secplus2::loop()
+ {
+ if (this->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::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_, "sync", delay, [=]() {
+ this->sync_helper(start, delay, tries + 1);
+ });
+ };
+ }
+
+ void Secplus2::sync()
+ {
+ this->scheduler_->cancel_timeout(this->ratgdo_, "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(action)));
+ }
+
+ void Secplus2::lock_action(LockAction action)
+ {
+ if (action == LockAction::UNKNOWN) {
+ return;
+ }
+ this->send_command(Command(CommandType::LOCK, static_cast(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(action), 1, 1), IncrementRollingCode::NO, [=]() {
+ this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] {
+ this->send_command(Command(CommandType::DOOR_ACTION, static_cast(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->scheduler_->set_timeout(this->ratgdo_, "", timeout, [=] { this->query_paired_devices(kind); });
+ }
+ }
+
+ void Secplus2::query_paired_devices(PairedDevice kind)
+ {
+ ESP_LOGD(TAG, "Query paired devices of type: %s", PairedDevice_to_string(kind));
+ this->send_command(Command { CommandType::GET_PAIRED_DEVICES, static_cast(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", PairedDevice_to_string(kind));
+ if (kind == PairedDevice::ALL) {
+ this->scheduler_->set_timeout(this->ratgdo_, "", 200, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::REMOTE) - 1 }); }); // wireless
+ this->scheduler_->set_timeout(this->ratgdo_, "", 400, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::KEYPAD) - 1 }); }); // keypads
+ this->scheduler_->set_timeout(this->ratgdo_, "", 600, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::WALL_CONTROL) - 1 }); }); // wall controls
+ this->scheduler_->set_timeout(this->ratgdo_, "", 800, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast(PairedDevice::ACCESSORY) - 1 }); }); // accessories
+ this->scheduler_->set_timeout(this->ratgdo_, "", 1000, [=] { this->query_status(); });
+ this->scheduler_->set_timeout(this->ratgdo_, "", 1200, [=] { this->query_paired_devices(); });
+ } else {
+ uint8_t dev_kind = static_cast(kind) - 1;
+ this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, dev_kind }); // just requested device
+ this->scheduler_->set_timeout(this->ratgdo_, "", 200, [=] { this->query_status(); });
+ this->scheduler_->set_timeout(this->ratgdo_, "", 400, [=] { 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->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] { this->send_command(Command { CommandType::LEARN, 1 }); });
+ this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { 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->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] { this->send_command(Command { CommandType::LEARN, 0 }); });
+ this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { this->query_status(); });
+ }
+
+ optional Secplus2::read_command()
+ {
+ static bool reading_msg = false;
+ static uint32_t msg_start = 0;
+ static uint16_t byte_count = 0;
+ static WirePacket rx_packet;
+ static uint32_t last_read = 0;
+
+ if (!reading_msg) {
+ while (this->sw_serial_.available()) {
+ uint8_t ser_byte = this->sw_serial_.read();
+ last_read = millis();
+
+ if (ser_byte != 0x55 && ser_byte != 0x01 && ser_byte != 0x00) {
+ ESP_LOG2(TAG, "Ignoring byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
+ byte_count = 0;
+ continue;
+ }
+ msg_start = ((msg_start << 8) | ser_byte) & 0xffffff;
+ byte_count++;
+
+ // if we are at the start of a message, capture the next 16 bytes
+ if (msg_start == 0x550100) {
+ ESP_LOG1(TAG, "Baud: %d", this->sw_serial_.baudRate());
+ rx_packet[0] = 0x55;
+ rx_packet[1] = 0x01;
+ rx_packet[2] = 0x00;
+
+ reading_msg = true;
+ break;
+ }
+ }
+ }
+ if (reading_msg) {
+ while (this->sw_serial_.available()) {
+ uint8_t ser_byte = this->sw_serial_.read();
+ last_read = millis();
+ rx_packet[byte_count] = ser_byte;
+ byte_count++;
+ // ESP_LOG2(TAG, "Received byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
+
+ if (byte_count == PACKET_LENGTH) {
+ reading_msg = false;
+ byte_count = 0;
+ this->print_packet("Received packet: ", rx_packet);
+ return this->decode_packet(rx_packet);
+ }
+ }
+
+ if (millis() - 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", byte_count);
+ reading_msg = false;
+ byte_count = 0;
+ }
+ }
+
+ return {};
+ }
+
+ void Secplus2::print_packet(const char* prefix, const WirePacket& packet) const
+ {
+ ESP_LOG2(TAG, "%s: [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]",
+ prefix,
+ packet[0],
+ packet[1],
+ packet[2],
+ packet[3],
+ packet[4],
+ packet[5],
+ packet[6],
+ packet[7],
+ packet[8],
+ packet[9],
+ packet[10],
+ packet[11],
+ packet[12],
+ packet[13],
+ packet[14],
+ packet[15],
+ packet[16],
+ packet[17],
+ packet[18]);
+ }
+
+ optional Secplus2::decode_packet(const WirePacket& packet) const
+ {
+ uint32_t rolling = 0;
+ uint64_t fixed = 0;
+ uint32_t data = 0;
+
+ decode_wireline(packet, &rolling, &fixed, &data);
+
+ 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, 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", 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((cmd.byte1 << 8) | cmd.byte2), cmd.nibble });
+ } else if (cmd.type == CommandType::SET_TTC) {
+ this->ratgdo_->received(TimeToClose { static_cast((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", CommandType_to_string(cmd.type));
+ }
+
+ void Secplus2::send_command(Command command, IncrementRollingCode increment)
+ {
+ ESP_LOG1(TAG, "Send command: %s, data: %02X%02X%02X", CommandType_to_string(command.type), command.byte2, command.byte1, command.nibble);
+ if (!this->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", CommandType_to_string(command.type));
+ } else {
+ ESP_LOGW(TAG, "Not connected to GDO, ignoring command: %s", CommandType_to_string(command.type));
+ }
+ }
+ this->transmit_packet();
+ }
+
+ void Secplus2::send_command(Command command, IncrementRollingCode increment, std::function&& on_sent)
+ {
+ this->on_command_sent_(on_sent);
+ this->send_command(command, increment);
+ }
+
+ void Secplus2::encode_packet(Command command, WirePacket& packet)
+ {
+ auto cmd = static_cast(command.type);
+ uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
+ uint32_t data = (static_cast(command.byte2) << 24) | (static_cast(command.byte1) << 16) | (static_cast(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->transmit_pending_) {
+ this->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) {
+ ESP_LOGD(TAG, "Collision detected, waiting to send packet");
+ } else {
+ this->transmit_pending_start_ = 0; // to indicate GDO not connected state
+ }
+ }
+ return false;
+ }
+ delayMicroseconds(100);
+ }
+
+ this->print_packet("Sending packet", this->tx_packet_);
+
+ // indicate the start of a frame by pulling the 12V line low for at leat 1 byte followed by
+ // one STOP bit, which indicates to the receiving end that the start of the message follows
+ // The output pin is controlling a transistor, so the logic is inverted
+ this->tx_pin_->digital_write(true); // pull the line low for at least 1 byte
+ delayMicroseconds(1300);
+ this->tx_pin_->digital_write(false); // line high for at least 1 bit
+ delayMicroseconds(130);
+
+ this->sw_serial_.write(this->tx_packet_, PACKET_LENGTH);
+
+ this->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 ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/secplus2.h b/components/ratgdo/secplus2.h
new file mode 100644
index 0000000..a30747d
--- /dev/null
+++ b/components/ratgdo/secplus2.h
@@ -0,0 +1,154 @@
+#pragma once
+
+#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
+#include "esphome/core/optional.h"
+
+#include "callbacks.h"
+#include "common.h"
+#include "observable.h"
+#include "protocol.h"
+#include "ratgdo_state.h"
+
+namespace esphome {
+
+class Scheduler;
+class InternalGPIOPin;
+
+namespace ratgdo {
+ class RATGDOComponent;
+
+ namespace secplus2 {
+
+ using namespace esphome::ratgdo::protocol;
+
+ static const uint8_t PACKET_LENGTH = 19;
+ typedef uint8_t WirePacket[PACKET_LENGTH];
+
+ ENUM(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(cmd_e); }
+ inline bool operator==(const CommandType& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast(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 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_; }
+
+ 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 read_command();
+ void handle_command(const Command& cmd);
+
+ void send_command(Command cmd, IncrementRollingCode increment = IncrementRollingCode::YES);
+ void send_command(Command cmd, IncrementRollingCode increment, std::function&& on_sent);
+ 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 char* prefix, const WirePacket& packet) const;
+ optional decode_packet(const WirePacket& packet) const;
+
+ void sync_helper(uint32_t start, uint32_t delay, uint8_t tries);
+
+ LearnState learn_state_ { LearnState::UNKNOWN };
+
+ observable rolling_code_counter_ { 0 };
+ uint64_t client_id_ { 0x539 };
+
+ bool transmit_pending_ { false };
+ uint32_t transmit_pending_start_ { 0 };
+ WirePacket tx_packet_;
+ OnceCallbacks on_command_sent_;
+
+ Traits traits_;
+
+ SoftwareSerial sw_serial_;
+
+ InternalGPIOPin* tx_pin_;
+ InternalGPIOPin* rx_pin_;
+
+ RATGDOComponent* ratgdo_;
+ Scheduler* scheduler_;
+ };
+ } // namespace secplus2
+} // namespace ratgdo
+} // namespace esphome
diff --git a/components/ratgdo/sensor/__init__.py b/components/ratgdo/sensor/__init__.py
index a2f9758..5a593c7 100644
--- a/components/ratgdo/sensor/__init__.py
+++ b/components/ratgdo/sensor/__init__.py
@@ -38,4 +38,3 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_ratgdo_sensor_type(config[CONF_TYPE]))
await register_ratgdo_child(var, config)
-
diff --git a/components/ratgdo/switch/ratgdo_switch.cpp b/components/ratgdo/switch/ratgdo_switch.cpp
index 0c20af7..b0f911c 100644
--- a/components/ratgdo/switch/ratgdo_switch.cpp
+++ b/components/ratgdo/switch/ratgdo_switch.cpp
@@ -19,7 +19,7 @@ namespace ratgdo {
{
if (this->switch_type_ == SwitchType::RATGDO_LEARN) {
this->parent_->subscribe_learn_state([=](LearnState state) {
- this->publish_state(state==LearnState::ACTIVE);
+ this->publish_state(state == LearnState::ACTIVE);
});
}
}
diff --git a/static/index.html b/static/index.html
index 9493603..8773e87 100644
--- a/static/index.html
+++ b/static/index.html
@@ -154,15 +154,37 @@
Pick your board to flash your ratgdo board with ESPhome for ratgdo.
No programming or other software required.
+
+
+ - Residential overhead mounted openers
+
+ - with a yellow learn button are Security + 2.0
+ - with a red, purple or orange learn button are Security + “1.0”
+
+
+ - Residential wall mounted jackshaft openers
+
+ - With model 8500W or RJ070 are Security + 2.0
+ - All others are Security + 1.0
+
+
+ - Security + 2.0 door openers require ratgdo v2.0 control board or later
+ - Security + 1.0 & Dry Contact door openers require v2.5 control board or later
+
+
- Note: At the moment ESPHome only supports Security + 2.0 door openers (Yellow learn button). Support for other protocols is coming.
+
+
+ Security + 1.0 support is experimental and may not work for all openers. Dry contact support is coming soon.
+
+
- v2.5i/2.52i Board
+ v2.5i/2.52i Board Security+ 2.0
@@ -171,16 +193,24 @@
Version 2.5i Dry Contact Wiring Diagram
+ v2.5i/2.52i Board Security+ 1.0
+
- v2.5 Board
+
+ v2.5 Board Security+ 2.0
@@ -190,23 +220,23 @@
- v2.0 Board
+ v2.0 Board Security+ 2.0
diff --git a/static/v25iboard_secplusv1.png b/static/v25iboard_secplusv1.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3586f9a5530cdedc8603bbbfe8b10e2f6e70cbc
GIT binary patch
literal 303545
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