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 0000000..a3586f9 Binary files /dev/null and b/static/v25iboard_secplusv1.png differ diff --git a/static/v25iboard_secplusv1.yaml b/static/v25iboard_secplusv1.yaml new file mode 100644 index 0000000..c099aea --- /dev/null +++ b/static/v25iboard_secplusv1.yaml @@ -0,0 +1,52 @@ +--- +substitutions: + id_prefix: ratgdov25i + friendly_name: "ratgdov2.5i" + uart_tx_pin: D1 + uart_rx_pin: D2 + input_obst_pin: D7 + status_door_pin: D0 + status_obstruction_pin: D8 + dry_contact_open_pin: D5 + dry_contact_close_pin: D6 + dry_contact_light_pin: D3 + +web_server: + +esphome: + name: ${id_prefix} + friendly_name: ${friendly_name} + name_add_mac_suffix: true + project: + name: ratgdo.esphome + version: "2.5i" + +esp8266: + board: d1_mini + restore_from_flash: true + +dashboard_import: + package_import_url: github://ratgdo/esphome-ratgdo/v25iboard_secplusv1.yaml@main + +packages: + remote_package: + url: https://github.com/ratgdo/esphome-ratgdo + files: [base_secplusv1.yaml] + refresh: 1s + +# Sync time with Home Assistant. +time: + - platform: homeassistant + id: homeassistant_time + +api: + id: api_server + +ota: + +improv_serial: + +wifi: + ap: + +logger: diff --git a/v25iboard_secplusv1.yaml b/v25iboard_secplusv1.yaml new file mode 120000 index 0000000..abe0144 --- /dev/null +++ b/v25iboard_secplusv1.yaml @@ -0,0 +1 @@ +static/v25iboard_secplusv1.yaml \ No newline at end of file