diff --git a/base.yaml b/base.yaml index de1b250..ddf6272 100644 --- a/base.yaml +++ b/base.yaml @@ -73,7 +73,7 @@ switch: id: "${id_prefix}_status_door" internal: true pin: - number: ${status_door_pin} # D0 output door status, HIGH for open, LOW for closed + number: ${status_door_pin} # output door status, HIGH for open, LOW for closed mode: output: true name: "Status door" @@ -82,7 +82,7 @@ switch: id: "${id_prefix}_status_obstruction" internal: true pin: - number: ${status_obstruction_pin} # D8 output for obstruction status, HIGH for obstructed, LOW for clear + number: ${status_obstruction_pin} # output for obstruction status, HIGH for obstructed, LOW for clear mode: output: true name: "Status obstruction" @@ -128,7 +128,7 @@ binary_sensor: - platform: gpio id: "${id_prefix}_dry_contact_open" pin: - number: ${dry_contact_open_pin} # D5 dry contact for opening door + number: ${dry_contact_open_pin} # dry contact for opening door inverted: true mode: input: true @@ -146,7 +146,7 @@ binary_sensor: - platform: gpio id: "${id_prefix}_dry_contact_close" pin: - number: ${dry_contact_close_pin} # D6 dry contact for closing door + number: ${dry_contact_close_pin} # dry contact for closing door inverted: true mode: input: true @@ -164,7 +164,7 @@ binary_sensor: - 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) + number: ${dry_contact_light_pin} # dry contact for triggering light (no discrete light commands, so toggle only) inverted: true mode: input: true diff --git a/base_drycontact.yaml b/base_drycontact.yaml new file mode 100644 index 0000000..60ab11f --- /dev/null +++ b/base_drycontact.yaml @@ -0,0 +1,95 @@ +--- + +external_components: + - source: + # type: local + # path: components + type: git + url: https://github.com/ratgdo/esphome-ratgdo + refresh: 1s + +preferences: + flash_write_interval: 1min + +ratgdo: + id: ${id_prefix} + output_gdo_pin: ${uart_tx_pin} + input_obst_pin: ${input_obst_pin} + dry_contact_open_sensor: ${id_prefix}_dry_contact_open + dry_contact_close_sensor: ${id_prefix}_dry_contact_close + discrete_open_pin: ${discrete_open_pin} + discrete_close_pin: ${discrete_close_pin} + protocol: drycontact + +binary_sensor: + - platform: ratgdo + type: obstruction + id: ${id_prefix}_obstruction + ratgdo_id: ${id_prefix} + name: "Obstruction" + device_class: problem + - platform: gpio + id: "${id_prefix}_dry_contact_open" + pin: + number: ${dry_contact_open_pin} + inverted: true + mode: + input: true + pullup: true + name: "Open limit switch" + entity_category: diagnostic + filters: + - delayed_on_off: 500ms + - platform: gpio + id: "${id_prefix}_dry_contact_close" + pin: + number: ${dry_contact_close_pin} + inverted: true + mode: + input: true + pullup: true + name: "Close limit switch" + entity_category: diagnostic + filters: + - delayed_on_off: 500ms + +number: + - 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" + +cover: + - platform: ratgdo + id: ${id_prefix}_garage_door + device_class: garage + name: "Door" + ratgdo_id: ${id_prefix} + +button: + - platform: restart + id: ${id_prefix}_restart + name: "Restart" + - platform: safe_mode + id: ${id_prefix}_safe_mode + name: "Safe mode boot" + entity_category: diagnostic + + - platform: template + id: ${id_prefix}_toggle_door + name: "Toggle door" + on_press: + then: + lambda: !lambda |- + id($id_prefix).door_toggle(); \ No newline at end of file diff --git a/components/ratgdo/__init__.py b/components/ratgdo/__init__.py index 6b52ddc..121a004 100644 --- a/components/ratgdo/__init__.py +++ b/components/ratgdo/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv import voluptuous as vol from esphome import automation, pins from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import binary_sensor DEPENDENCIES = ["preferences"] MULTI_CONF = True @@ -25,6 +26,9 @@ DEFAULT_INPUT_GDO = ( CONF_INPUT_OBST = "input_obst_pin" DEFAULT_INPUT_OBST = "D7" # D7 black obstruction sensor terminal +CONF_DISCRETE_OPEN_PIN = "discrete_open_pin" +CONF_DISCRETE_CLOSE_PIN = "discrete_close_pin" + CONF_RATGDO_ID = "ratgdo_id" CONF_ON_SYNC_FAILED = "on_sync_failed" @@ -36,7 +40,22 @@ PROTOCOL_SECPLUSV2 = "secplusv2" PROTOCOL_DRYCONTACT = "drycontact" SUPPORTED_PROTOCOLS = [PROTOCOL_SECPLUSV1, PROTOCOL_SECPLUSV2, PROTOCOL_DRYCONTACT] -CONFIG_SCHEMA = cv.Schema( +CONF_DRY_CONTACT_OPEN_SENSOR = "dry_contact_open_sensor" +CONF_DRY_CONTACT_CLOSE_SENSOR = "dry_contact_close_sensor" +CONF_DRY_CONTACT_SENSOR_GROUP = "dry_contact_sensor_group" + +def validate_protocol(config): + print("Validation") + if config.get(CONF_PROTOCOL, None) == PROTOCOL_DRYCONTACT and (CONF_DRY_CONTACT_CLOSE_SENSOR not in config or CONF_DRY_CONTACT_OPEN_SENSOR not in config): + raise cv.Invalid("dry_contact_close_sensor and dry_contact_open_sensor are required when using protocol drycontact") + if config.get(CONF_PROTOCOL, None) != PROTOCOL_DRYCONTACT and (CONF_DRY_CONTACT_CLOSE_SENSOR in config or CONF_DRY_CONTACT_OPEN_SENSOR in config): + raise cv.Invalid("dry_contact_close_sensor and dry_contact_open_sensor are only valid when using protocol drycontact") +# if config.get(CONF_PROTOCOL, None) == PROTOCOL_DRYCONTACT and CONF_DRY_CONTACT_OPEN_SENSOR not in config: +# raise cv.Invalid("dry_contact_open_sensor is required when using protocol drycontact") + return config + +CONFIG_SCHEMA = cv.All( + cv.Schema( { cv.GenerateID(): cv.declare_id(RATGDO), cv.Optional( @@ -48,16 +67,24 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST): cv.Any( cv.none, pins.gpio_input_pin_schema ), + cv.Optional(CONF_DISCRETE_OPEN_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_DISCRETE_CLOSE_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_ON_SYNC_FAILED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncFailed), } ), - cv.Optional(CONF_PROTOCOL, default=PROTOCOL_SECPLUSV2): vol.In( + cv.Optional(CONF_PROTOCOL, default=PROTOCOL_SECPLUSV2): cv.All(vol.In( SUPPORTED_PROTOCOLS - ), + )), + # cv.Inclusive(CONF_DRY_CONTACT_OPEN_SENSOR,CONF_DRY_CONTACT_SENSOR_GROUP): cv.use_id(binary_sensor.BinarySensor), + # cv.Inclusive(CONF_DRY_CONTACT_CLOSE_SENSOR,CONF_DRY_CONTACT_SENSOR_GROUP): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_DRY_CONTACT_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_DRY_CONTACT_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), } -).extend(cv.COMPONENT_SCHEMA) + ).extend(cv.COMPONENT_SCHEMA), + validate_protocol, +) RATGDO_CLIENT_SCHMEA = cv.Schema( { @@ -82,6 +109,14 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_INPUT_OBST]) cg.add(var.set_input_obst_pin(pin)) + if CONF_DRY_CONTACT_OPEN_SENSOR in config and config[CONF_DRY_CONTACT_OPEN_SENSOR]: + dry_contact_open_sensor = await cg.get_variable(config[CONF_DRY_CONTACT_OPEN_SENSOR]) + cg.add(var.set_dry_contact_open_sensor(dry_contact_open_sensor)) + + if CONF_DRY_CONTACT_CLOSE_SENSOR in config and config[CONF_DRY_CONTACT_CLOSE_SENSOR]: + dry_contact_close_sensor = await cg.get_variable(config[CONF_DRY_CONTACT_CLOSE_SENSOR]) + cg.add(var.set_dry_contact_close_sensor(dry_contact_close_sensor)) + for conf in config.get(CONF_ON_SYNC_FAILED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -104,3 +139,10 @@ async def to_code(config): elif config[CONF_PROTOCOL] == PROTOCOL_DRYCONTACT: cg.add_define("PROTOCOL_DRYCONTACT") cg.add(var.init_protocol()) + + if CONF_DISCRETE_OPEN_PIN in config and config[CONF_DISCRETE_OPEN_PIN]: + pin = await cg.gpio_pin_expression(config[CONF_DISCRETE_OPEN_PIN]) + cg.add(var.set_discrete_open_pin(pin)) + if CONF_DISCRETE_CLOSE_PIN in config and config[CONF_DISCRETE_CLOSE_PIN]: + pin = await cg.gpio_pin_expression(config[CONF_DISCRETE_CLOSE_PIN]) + cg.add(var.set_discrete_close_pin(pin)) \ No newline at end of file diff --git a/components/ratgdo/dry_contact.cpp b/components/ratgdo/dry_contact.cpp index 5dc5a9d..9860e16 100644 --- a/components/ratgdo/dry_contact.cpp +++ b/components/ratgdo/dry_contact.cpp @@ -5,6 +5,7 @@ #include "esphome/core/gpio.h" #include "esphome/core/log.h" #include "esphome/core/scheduler.h" +#include "esphome/components/gpio/binary_sensor/gpio_binary_sensor.h" namespace esphome { namespace ratgdo { @@ -18,6 +19,12 @@ namespace ratgdo { this->scheduler_ = scheduler; this->tx_pin_ = tx_pin; this->rx_pin_ = rx_pin; + + this->open_limit_reached_ = 0; + this->last_open_limit_ = 0; + this->close_limit_reached_ = 0; + this->last_close_limit_ = 0; + this->door_state_ = DoorState::UNKNOWN; } void DryContact::loop() @@ -31,6 +38,43 @@ namespace ratgdo { void DryContact::sync() { + ESP_LOG1(TAG, "Ignoring sync action"); + } + + void DryContact::set_open_limit(bool state) + { + ESP_LOGD(TAG, "Set open_limit_reached to %d", state); + this->last_open_limit_ = this->open_limit_reached_; + this->last_close_limit_ = false; + this->open_limit_reached_ = state; + this->send_door_state(); + } + + void DryContact::set_close_limit(bool state) + { + ESP_LOGD(TAG, "Set close_limit_reached to %d", state); + this->last_close_limit_ = this->close_limit_reached_; + this->last_open_limit_ = false; + this->close_limit_reached_ = state; + this->send_door_state(); + } + + void DryContact::send_door_state(){ + if(this->open_limit_reached_){ + this->door_state_ = DoorState::OPEN; + }else if(this->close_limit_reached_){ + this->door_state_ = DoorState::CLOSED; + }else if(!this->close_limit_reached_ && !this->open_limit_reached_){ + if(this->last_close_limit_){ + this->door_state_ = DoorState::OPENING; + } + + if(this->last_open_limit_){ + this->door_state_ = DoorState::CLOSING; + } + } + + this->ratgdo_->received(this->door_state_); } void DryContact::light_action(LightAction action) @@ -47,14 +91,33 @@ namespace ratgdo { void DryContact::door_action(DoorAction action) { - if (action != DoorAction::TOGGLE) { - ESP_LOG1(TAG, "Ignoring door action: %s", DoorAction_to_string(action)); + if (action == DoorAction::OPEN && this->door_state_ != DoorState::CLOSED) { + ESP_LOGW(TAG, "The door is not closed. Ignoring door action: %s", DoorAction_to_string(action)); return; } + if (action == DoorAction::CLOSE && this->door_state_ != DoorState::OPEN) { + ESP_LOGW(TAG, "The door is not open. 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, [=] { + if (action == DoorAction::OPEN){ + this->discrete_open_pin_->digital_write(1); + this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { + this->discrete_open_pin_->digital_write(0); + }); + } + + if (action == DoorAction::CLOSE){ + this->discrete_close_pin_->digital_write(1); + this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { + this->discrete_close_pin_->digital_write(0); + }); + } + + this->tx_pin_->digital_write(1); // Single button control + this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { this->tx_pin_->digital_write(0); }); } diff --git a/components/ratgdo/dry_contact.h b/components/ratgdo/dry_contact.h index 99dd024..09362b6 100644 --- a/components/ratgdo/dry_contact.h +++ b/components/ratgdo/dry_contact.h @@ -2,6 +2,8 @@ #include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial #include "esphome/core/optional.h" +#include "esphome/core/gpio.h" +#include "esphome/components/gpio/binary_sensor/gpio_binary_sensor.h" #include "callbacks.h" #include "observable.h" @@ -17,6 +19,7 @@ namespace ratgdo { namespace dry_contact { using namespace esphome::ratgdo::protocol; + using namespace esphome::gpio; class DryContact : public Protocol { public: @@ -29,6 +32,21 @@ namespace ratgdo { void light_action(LightAction action); void lock_action(LockAction action); void door_action(DoorAction action); + void set_open_limit(bool state); + void set_close_limit(bool state); + void send_door_state(); + + void set_discrete_open_pin(InternalGPIOPin* pin) { + this->discrete_open_pin_ = pin; + this->discrete_open_pin_->setup(); + this->discrete_open_pin_->pin_mode(gpio::FLAG_OUTPUT); + } + + void set_discrete_close_pin(InternalGPIOPin* pin) { + this->discrete_close_pin_ = pin; + this->discrete_close_pin_->setup(); + this->discrete_close_pin_->pin_mode(gpio::FLAG_OUTPUT); + } Result call(Args args); @@ -39,9 +57,18 @@ namespace ratgdo { InternalGPIOPin* tx_pin_; InternalGPIOPin* rx_pin_; + InternalGPIOPin* discrete_open_pin_; + InternalGPIOPin* discrete_close_pin_; RATGDOComponent* ratgdo_; Scheduler* scheduler_; + + DoorState door_state_; + bool open_limit_reached_; + bool last_open_limit_; + bool close_limit_reached_; + bool last_close_limit_; + }; } // namespace secplus1 diff --git a/components/ratgdo/protocol.h b/components/ratgdo/protocol.h index 8202079..ca49aeb 100644 --- a/components/ratgdo/protocol.h +++ b/components/ratgdo/protocol.h @@ -103,6 +103,12 @@ namespace ratgdo { virtual void sync(); + // dry contact methods + virtual void set_open_limit(bool); + virtual void set_close_limit(bool); + virtual void set_discrete_open_pin(InternalGPIOPin* pin); + virtual void set_discrete_close_pin(InternalGPIOPin* pin); + virtual const Traits& traits() const; virtual void light_action(LightAction action); diff --git a/components/ratgdo/ratgdo.cpp b/components/ratgdo/ratgdo.cpp index f65ae25..199a685 100644 --- a/components/ratgdo/ratgdo.cpp +++ b/components/ratgdo/ratgdo.cpp @@ -418,6 +418,13 @@ namespace ratgdo { void RATGDOComponent::sync() { this->protocol_->sync(); + + // dry contact protocol: + // needed to trigger the intial state of the limit switch sensors + // ideally this would be in drycontact::sync + // this->dry_contact_open_sensor_->state; + this->protocol_->set_open_limit(this->dry_contact_open_sensor_->state); + this->protocol_->set_close_limit(this->dry_contact_close_sensor_->state); } void RATGDOComponent::door_open() @@ -673,5 +680,26 @@ namespace ratgdo { this->learn_state.subscribe([=](LearnState state) { defer("learn_state", [=] { f(state); }); }); } + // dry contact methods + void RATGDOComponent::set_dry_contact_open_sensor(esphome::gpio::GPIOBinarySensor* dry_contact_open_sensor) + { + dry_contact_open_sensor_ = dry_contact_open_sensor; + dry_contact_open_sensor_->add_on_state_callback([this](bool sensor_value) + { + this->protocol_->set_open_limit(sensor_value); + } + ); + } + + void RATGDOComponent::set_dry_contact_close_sensor(esphome::gpio::GPIOBinarySensor* dry_contact_close_sensor) + { + dry_contact_close_sensor_ = dry_contact_close_sensor; + dry_contact_close_sensor_->add_on_state_callback([this](bool sensor_value) + { + this->protocol_->set_close_limit(sensor_value); + } + ); + } + } // namespace ratgdo } // namespace esphome diff --git a/components/ratgdo/ratgdo.h b/components/ratgdo/ratgdo.h index 510f636..fbb92b1 100644 --- a/components/ratgdo/ratgdo.h +++ b/components/ratgdo/ratgdo.h @@ -16,6 +16,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/preferences.h" +#include "esphome/components/gpio/binary_sensor/gpio_binary_sensor.h" #include "callbacks.h" #include "macros.h" @@ -91,6 +92,12 @@ namespace ratgdo { void set_input_gdo_pin(InternalGPIOPin* pin) { this->input_gdo_pin_ = pin; } void set_input_obst_pin(InternalGPIOPin* pin) { this->input_obst_pin_ = pin; } + // dry contact methods + void set_dry_contact_open_sensor(esphome::gpio::GPIOBinarySensor* dry_contact_open_sensor_); + void set_dry_contact_close_sensor(esphome::gpio::GPIOBinarySensor* dry_contact_close_sensor_); + void set_discrete_open_pin(InternalGPIOPin* pin){ this->protocol_->set_discrete_open_pin(pin); } + void set_discrete_close_pin(InternalGPIOPin* pin){ this->protocol_->set_discrete_close_pin(pin); } + Result call_protocol(Args args); void received(const DoorState door_state); @@ -174,6 +181,8 @@ namespace ratgdo { InternalGPIOPin* output_gdo_pin_; InternalGPIOPin* input_gdo_pin_; InternalGPIOPin* input_obst_pin_; + esphome::gpio::GPIOBinarySensor* dry_contact_open_sensor_; + esphome::gpio::GPIOBinarySensor* dry_contact_close_sensor_; }; // RATGDOComponent } // namespace ratgdo diff --git a/components/ratgdo/secplus1.h b/components/ratgdo/secplus1.h index 42d83fd..c2cd5d9 100644 --- a/components/ratgdo/secplus1.h +++ b/components/ratgdo/secplus1.h @@ -96,6 +96,13 @@ namespace ratgdo { const Traits& traits() const { return this->traits_; } + // methods not used by secplus1 + void set_open_limit(bool state){} + void set_close_limit(bool state){} + void set_discrete_open_pin(InternalGPIOPin* pin){} + void set_discrete_close_pin(InternalGPIOPin* pin){} + + protected: void wall_panel_emulation(size_t index = 0); diff --git a/components/ratgdo/secplus2.h b/components/ratgdo/secplus2.h index a30747d..6ea02b9 100644 --- a/components/ratgdo/secplus2.h +++ b/components/ratgdo/secplus2.h @@ -101,6 +101,12 @@ namespace ratgdo { const Traits& traits() const { return this->traits_; } + // methods not used by secplus2 + void set_open_limit(bool state){} + void set_close_limit(bool state){} + void set_discrete_open_pin(InternalGPIOPin* pin){} + void set_discrete_close_pin(InternalGPIOPin* pin){} + protected: void increment_rolling_code_counter(int delta = 1); void set_rolling_code_counter(uint32_t counter); diff --git a/static/index.html b/static/index.html index dc5c164..d77e636 100644 --- a/static/index.html +++ b/static/index.html @@ -202,8 +202,8 @@ @@ -290,10 +290,7 @@ var protocol = document.querySelector('input[name="protocol"]:checked').value; var hardware = document.querySelector('input[name="hardware"]:checked').value; - if(protocol === "dry_contact"){ - alert("Dry contact support is coming soon."); - document.querySelector('input[name="protocol"][value="secplusv2"]').checked = true; - return; + if(protocol === "drycontact"){ document.querySelector("#wiring_diagram").src = "wiring_diagrams/dry_contact_diagram.png"; }else{ document.querySelector("#wiring_diagram").src = "wiring_diagrams/secplus_diagram.png"; diff --git a/static/wiring_diagrams/dry_contact_diagram.png b/static/wiring_diagrams/dry_contact_diagram.png index fef2f5f..0bf55d4 100644 Binary files a/static/wiring_diagrams/dry_contact_diagram.png and b/static/wiring_diagrams/dry_contact_diagram.png differ diff --git a/v25iboard_drycontact.yaml b/v25iboard_drycontact.yaml new file mode 100644 index 0000000..59264ae --- /dev/null +++ b/v25iboard_drycontact.yaml @@ -0,0 +1,53 @@ +--- +substitutions: + id_prefix: ratgdov25i + friendly_name: "ratgdov2.5i" + uart_tx_pin: D1 + uart_rx_pin: D2 + input_obst_pin: D7 + dry_contact_open_pin: D5 + dry_contact_close_pin: D6 + discrete_open_pin: D0 + discrete_close_pin: D8 + +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 + # remote_package: !include + # file: base_drycontact.yaml +# Sync time with Home Assistant. +time: + - platform: homeassistant + id: homeassistant_time + +api: + id: api_server + +ota: + +improv_serial: + +wifi: + ap: + +logger: + level: DEBUG \ No newline at end of file