feat: Security Plus v1 support (#171)
Co-authored-by: Marius Muja <mariusmuja@gmail.com>
This commit is contained in:
parent
4c2ed4e7c0
commit
f3f8f966dc
|
@ -17,26 +17,29 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
firmware:
|
firmware:
|
||||||
- file: v2board_esp8266_d1_mini_lite.yaml
|
- 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
|
manifest_filename: v2board_esp8266_d1_mini_lite-manifest.json
|
||||||
- file: v2board_esp8266_d1_mini.yaml
|
- 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
|
manifest_filename: v2board_esp8266_d1_mini-manifest.json
|
||||||
- file: v2board_esp32_d1_mini.yaml
|
- 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
|
manifest_filename: v2board_esp32_d1_mini-manifest.json
|
||||||
- file: v2board_esp32_lolin_s2_mini.yaml
|
- 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
|
manifest_filename: v2board_esp32_lolin_s2_mini-manifest.json
|
||||||
- file: v25board_esp8266_d1_mini_lite.yaml
|
- 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
|
manifest_filename: v25board_esp8266_d1_mini_lite-manifest.json
|
||||||
- file: v25board_esp32_d1_mini.yaml
|
- 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
|
manifest_filename: v25board_esp32_d1_mini-manifest.json
|
||||||
- file: v25iboard.yaml
|
- file: v25iboard.yaml
|
||||||
name: V2.5i Board
|
name: V2.5i Board Security+ 2.0
|
||||||
manifest_filename: v25iboard-manifest.json
|
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
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
|
|
15
README.md
15
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
|
||||||
|
|
||||||
- [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](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)
|
- [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)
|
||||||
- [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 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)
|
- [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)
|
||||||
- [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 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)
|
- [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)
|
||||||
- [ESPHome config for v2.5i board](https://github.com/RATGDO/esphome-ratgdo/blob/main/static/v25iboard.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/)
|
- [Web Installer](https://ratgdo.github.io/esphome-ratgdo/)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ ratgdo:
|
||||||
service: persistent_notification.create
|
service: persistent_notification.create
|
||||||
data:
|
data:
|
||||||
title: "${friendly_name} sync failed"
|
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"
|
notification_id: "esphome_ratgdo_${id_prefix}_sync_failed"
|
||||||
api:
|
api:
|
||||||
services:
|
services:
|
||||||
|
@ -263,4 +263,4 @@ button:
|
||||||
on_press:
|
on_press:
|
||||||
then:
|
then:
|
||||||
lambda: !lambda |-
|
lambda: !lambda |-
|
||||||
id($id_prefix).toggle_door();
|
id($id_prefix).door_toggle();
|
||||||
|
|
|
@ -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();
|
|
@ -1,5 +1,6 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
import voluptuous as vol
|
||||||
from esphome import automation, pins
|
from esphome import automation, pins
|
||||||
from esphome.const import CONF_ID, CONF_TRIGGER_ID
|
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_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(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
|
@ -38,14 +45,17 @@ CONFIG_SCHEMA = cv.Schema(
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_INPUT_GDO, default=DEFAULT_INPUT_GDO
|
CONF_INPUT_GDO, default=DEFAULT_INPUT_GDO
|
||||||
): pins.gpio_input_pin_schema,
|
): pins.gpio_input_pin_schema,
|
||||||
cv.Optional(
|
cv.Optional(CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST): cv.Any(
|
||||||
CONF_INPUT_OBST, default=DEFAULT_INPUT_OBST
|
cv.none, pins.gpio_input_pin_schema
|
||||||
): pins.gpio_input_pin_schema,
|
),
|
||||||
cv.Optional(CONF_ON_SYNC_FAILED): automation.validate_automation(
|
cv.Optional(CONF_ON_SYNC_FAILED): automation.validate_automation(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncFailed),
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncFailed),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_PROTOCOL, default=PROTOCOL_SECPLUSV2): vol.In(
|
||||||
|
SUPPORTED_PROTOCOLS
|
||||||
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
@ -68,8 +78,9 @@ async def to_code(config):
|
||||||
cg.add(var.set_output_gdo_pin(pin))
|
cg.add(var.set_output_gdo_pin(pin))
|
||||||
pin = await cg.gpio_pin_expression(config[CONF_INPUT_GDO])
|
pin = await cg.gpio_pin_expression(config[CONF_INPUT_GDO])
|
||||||
cg.add(var.set_input_gdo_pin(pin))
|
cg.add(var.set_input_gdo_pin(pin))
|
||||||
pin = await cg.gpio_pin_expression(config[CONF_INPUT_OBST])
|
if CONF_INPUT_OBST in config and config[CONF_INPUT_OBST]:
|
||||||
cg.add(var.set_input_obst_pin(pin))
|
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, []):
|
for conf in config.get(CONF_ON_SYNC_FAILED, []):
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
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",
|
repository="https://github.com/ratgdo/espsoftwareserial#autobaud",
|
||||||
version=None,
|
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())
|
||||||
|
|
|
@ -13,9 +13,9 @@ namespace ratgdo {
|
||||||
class OnceCallbacks<void(Ts...)> {
|
class OnceCallbacks<void(Ts...)> {
|
||||||
public:
|
public:
|
||||||
template <typename Callback>
|
template <typename Callback>
|
||||||
void then(Callback&& callback) { this->callbacks_.push_back(std::forward<Callback>(callback)); }
|
void operator()(Callback&& callback) { this->callbacks_.push_back(std::forward<Callback>(callback)); }
|
||||||
|
|
||||||
void operator()(Ts... args)
|
void trigger(Ts... args)
|
||||||
{
|
{
|
||||||
for (auto& cb : this->callbacks_)
|
for (auto& cb : this->callbacks_)
|
||||||
cb(args...);
|
cb(args...);
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define ESP_LOG1 ESP_LOGV
|
||||||
|
#define ESP_LOG2 ESP_LOGV
|
|
@ -27,6 +27,7 @@ namespace ratgdo {
|
||||||
|
|
||||||
void RATGDOCover::on_door_state(DoorState state, float position)
|
void RATGDOCover::on_door_state(DoorState state, float position)
|
||||||
{
|
{
|
||||||
|
bool save_to_flash = true;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case DoorState::OPEN:
|
case DoorState::OPEN:
|
||||||
this->position = COVER_OPEN;
|
this->position = COVER_OPEN;
|
||||||
|
@ -39,10 +40,12 @@ namespace ratgdo {
|
||||||
case DoorState::OPENING:
|
case DoorState::OPENING:
|
||||||
this->current_operation = COVER_OPERATION_OPENING;
|
this->current_operation = COVER_OPERATION_OPENING;
|
||||||
this->position = position;
|
this->position = position;
|
||||||
|
save_to_flash = false;
|
||||||
break;
|
break;
|
||||||
case DoorState::CLOSING:
|
case DoorState::CLOSING:
|
||||||
this->current_operation = COVER_OPERATION_CLOSING;
|
this->current_operation = COVER_OPERATION_CLOSING;
|
||||||
this->position = position;
|
this->position = position;
|
||||||
|
save_to_flash = false;
|
||||||
break;
|
break;
|
||||||
case DoorState::STOPPED:
|
case DoorState::STOPPED:
|
||||||
this->current_operation = COVER_OPERATION_IDLE;
|
this->current_operation = COVER_OPERATION_IDLE;
|
||||||
|
@ -55,7 +58,7 @@ namespace ratgdo {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->publish_state();
|
this->publish_state(save_to_flash);
|
||||||
}
|
}
|
||||||
|
|
||||||
CoverTraits RATGDOCover::get_traits()
|
CoverTraits RATGDOCover::get_traits()
|
||||||
|
@ -70,17 +73,17 @@ namespace ratgdo {
|
||||||
void RATGDOCover::control(const CoverCall& call)
|
void RATGDOCover::control(const CoverCall& call)
|
||||||
{
|
{
|
||||||
if (call.get_stop()) {
|
if (call.get_stop()) {
|
||||||
this->parent_->stop_door();
|
this->parent_->door_stop();
|
||||||
}
|
}
|
||||||
if (call.get_toggle()) {
|
if (call.get_toggle()) {
|
||||||
this->parent_->toggle_door();
|
this->parent_->door_toggle();
|
||||||
}
|
}
|
||||||
if (call.get_position().has_value()) {
|
if (call.get_position().has_value()) {
|
||||||
auto pos = *call.get_position();
|
auto pos = *call.get_position();
|
||||||
if (pos == COVER_OPEN) {
|
if (pos == COVER_OPEN) {
|
||||||
this->parent_->open_door();
|
this->parent_->door_open();
|
||||||
} else if (pos == COVER_CLOSED) {
|
} else if (pos == COVER_CLOSED) {
|
||||||
this->parent_->close_door();
|
this->parent_->door_close();
|
||||||
} else {
|
} else {
|
||||||
this->parent_->door_move_to_position(pos);
|
this->parent_->door_move_to_position(pos);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -31,7 +31,6 @@ namespace ratgdo {
|
||||||
|
|
||||||
void RATGDOLightOutput::set_state(esphome::ratgdo::LightState state)
|
void RATGDOLightOutput::set_state(esphome::ratgdo::LightState state)
|
||||||
{
|
{
|
||||||
|
|
||||||
bool is_on = state == LightState::ON;
|
bool is_on = state == LightState::ON;
|
||||||
this->light_state_->current_values.set_state(is_on);
|
this->light_state_->current_values.set_state(is_on);
|
||||||
this->light_state_->remote_values.set_state(is_on);
|
this->light_state_->remote_values.set_state(is_on);
|
||||||
|
|
|
@ -9,15 +9,12 @@ DEPENDENCIES = ["ratgdo"]
|
||||||
|
|
||||||
RATGDOLock = ratgdo_ns.class_("RATGDOLock", lock.Lock, cg.Component)
|
RATGDOLock = ratgdo_ns.class_("RATGDOLock", lock.Lock, cg.Component)
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
|
||||||
lock.LOCK_SCHEMA
|
{
|
||||||
.extend(
|
cv.GenerateID(): cv.declare_id(RATGDOLock),
|
||||||
{
|
}
|
||||||
cv.GenerateID(): cv.declare_id(RATGDOLock),
|
).extend(RATGDO_CLIENT_SCHMEA)
|
||||||
}
|
|
||||||
)
|
|
||||||
.extend(RATGDO_CLIENT_SCHMEA)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|
|
@ -55,3 +55,35 @@
|
||||||
return _unknown; \
|
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__) \
|
||||||
|
};
|
|
@ -5,6 +5,9 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ratgdo {
|
namespace ratgdo {
|
||||||
|
|
||||||
|
using protocol::SetClientID;
|
||||||
|
using protocol::SetRollingCodeCounter;
|
||||||
|
|
||||||
float normalize_client_id(float client_id)
|
float normalize_client_id(float client_id)
|
||||||
{
|
{
|
||||||
uint32_t int_value = static_cast<uint32_t>(client_id);
|
uint32_t int_value = static_cast<uint32_t>(client_id);
|
||||||
|
@ -84,6 +87,9 @@ namespace ratgdo {
|
||||||
|
|
||||||
void RATGDONumber::update_state(float value)
|
void RATGDONumber::update_state(float value)
|
||||||
{
|
{
|
||||||
|
if (value == this->state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this->pref_.save(&value);
|
this->pref_.save(&value);
|
||||||
this->publish_state(value);
|
this->publish_state(value);
|
||||||
}
|
}
|
||||||
|
@ -91,14 +97,14 @@ namespace ratgdo {
|
||||||
void RATGDONumber::control(float value)
|
void RATGDONumber::control(float value)
|
||||||
{
|
{
|
||||||
if (this->number_type_ == RATGDO_ROLLING_CODE_COUNTER) {
|
if (this->number_type_ == RATGDO_ROLLING_CODE_COUNTER) {
|
||||||
this->parent_->set_rolling_code_counter(value);
|
this->parent_->call_protocol(SetRollingCodeCounter { static_cast<uint32_t>(value) });
|
||||||
} else if (this->number_type_ == RATGDO_OPENING_DURATION) {
|
} else if (this->number_type_ == RATGDO_OPENING_DURATION) {
|
||||||
this->parent_->set_opening_duration(value);
|
this->parent_->set_opening_duration(value);
|
||||||
} else if (this->number_type_ == RATGDO_CLOSING_DURATION) {
|
} else if (this->number_type_ == RATGDO_CLOSING_DURATION) {
|
||||||
this->parent_->set_closing_duration(value);
|
this->parent_->set_closing_duration(value);
|
||||||
} else if (this->number_type_ == RATGDO_CLIENT_ID) {
|
} else if (this->number_type_ == RATGDO_CLIENT_ID) {
|
||||||
value = normalize_client_id(value);
|
value = normalize_client_id(value);
|
||||||
this->parent_->set_client_id(value);
|
this->parent_->call_protocol(SetClientID { static_cast<uint32_t>(value) });
|
||||||
}
|
}
|
||||||
this->update_state(value);
|
this->update_state(value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<uint32_t>* value;
|
||||||
|
};
|
||||||
|
|
||||||
|
SUM_TYPE(Result,
|
||||||
|
(RollingCodeCounter, rolling_code_counter), )
|
||||||
|
|
||||||
|
class Protocol {
|
||||||
|
public:
|
||||||
|
virtual void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
|
||||||
|
virtual void loop();
|
||||||
|
virtual void dump_config();
|
||||||
|
|
||||||
|
virtual void 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
|
File diff suppressed because it is too large
Load Diff
|
@ -12,86 +12,28 @@
|
||||||
************************************/
|
************************************/
|
||||||
|
|
||||||
#pragma once
|
#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/component.h"
|
||||||
#include "esphome/core/gpio.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
#include "callbacks.h"
|
||||||
|
#include "macros.h"
|
||||||
#include "observable.h"
|
#include "observable.h"
|
||||||
|
#include "protocol.h"
|
||||||
extern "C" {
|
|
||||||
#include "secplus.h"
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "ratgdo_state.h"
|
#include "ratgdo_state.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
class InternalGPIOPin;
|
||||||
namespace ratgdo {
|
namespace ratgdo {
|
||||||
|
|
||||||
class RATGDOComponent;
|
class RATGDOComponent;
|
||||||
typedef Parented<RATGDOComponent> RATGDOClient;
|
typedef Parented<RATGDOComponent> RATGDOClient;
|
||||||
|
|
||||||
static const uint8_t PACKET_LENGTH = 19;
|
|
||||||
typedef uint8_t WirePacket[PACKET_LENGTH];
|
|
||||||
|
|
||||||
const float DOOR_POSITION_UNKNOWN = -1.0;
|
const float DOOR_POSITION_UNKNOWN = -1.0;
|
||||||
const float DOOR_DELTA_UNKNOWN = -2.0;
|
const float DOOR_DELTA_UNKNOWN = -2.0;
|
||||||
const uint16_t PAIRED_DEVICES_UNKNOWN = 0xFF;
|
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<uint16_t>(cmd_e); }
|
|
||||||
inline bool operator==(const Command& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
|
||||||
|
|
||||||
struct RATGDOStore {
|
struct RATGDOStore {
|
||||||
int obstruction_low_count = 0; // count obstruction low pulses
|
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 {
|
class RATGDOComponent : public Component {
|
||||||
public:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
observable<uint32_t> rolling_code_counter { 0 };
|
void init_protocol();
|
||||||
|
|
||||||
|
void obstruction_loop();
|
||||||
|
|
||||||
float start_opening { -1 };
|
float start_opening { -1 };
|
||||||
observable<float> opening_duration { 0 };
|
observable<float> opening_duration { 0 };
|
||||||
|
@ -136,35 +83,38 @@ namespace ratgdo {
|
||||||
observable<MotionState> motion_state { MotionState::UNKNOWN };
|
observable<MotionState> motion_state { MotionState::UNKNOWN };
|
||||||
observable<LearnState> learn_state { LearnState::UNKNOWN };
|
observable<LearnState> learn_state { LearnState::UNKNOWN };
|
||||||
|
|
||||||
OnceCallbacks<void(DoorState)> door_state_received;
|
OnceCallbacks<void(DoorState)> on_door_state_;
|
||||||
OnceCallbacks<void()> command_sent;
|
|
||||||
|
|
||||||
observable<bool> sync_failed { false };
|
observable<bool> sync_failed { false };
|
||||||
|
|
||||||
void set_output_gdo_pin(InternalGPIOPin* pin) { this->output_gdo_pin_ = pin; }
|
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_gdo_pin(InternalGPIOPin* pin) { this->input_gdo_pin_ = pin; }
|
||||||
void set_input_obst_pin(InternalGPIOPin* pin) { this->input_obst_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();
|
Result call_protocol(Args args);
|
||||||
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<void()>&& on_sent);
|
|
||||||
bool transmit_packet();
|
|
||||||
void encode_packet(Command command, uint32_t data, bool increment, WirePacket& packet);
|
|
||||||
void print_packet(const WirePacket& packet) const;
|
|
||||||
|
|
||||||
void increment_rolling_code_counter(int delta = 1);
|
void received(const DoorState door_state);
|
||||||
void set_rolling_code_counter(uint32_t code);
|
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
|
// door
|
||||||
void door_command(uint32_t data);
|
void door_toggle();
|
||||||
void ensure_door_command(uint32_t data, uint32_t delay = 1500);
|
void door_open();
|
||||||
void toggle_door();
|
void door_close();
|
||||||
void open_door();
|
void door_stop();
|
||||||
void close_door();
|
|
||||||
void stop_door();
|
void door_action(DoorAction action);
|
||||||
|
void ensure_door_action(DoorAction action, uint32_t delay = 1500);
|
||||||
void door_move_to_position(float position);
|
void door_move_to_position(float position);
|
||||||
void set_door_position(float door_position) { this->door_position = door_position; }
|
void set_door_position(float door_position) { this->door_position = door_position; }
|
||||||
void set_opening_duration(float duration);
|
void set_opening_duration(float duration);
|
||||||
|
@ -174,13 +124,13 @@ namespace ratgdo {
|
||||||
void cancel_position_sync_callbacks();
|
void cancel_position_sync_callbacks();
|
||||||
|
|
||||||
// light
|
// light
|
||||||
void toggle_light();
|
void light_toggle();
|
||||||
void light_on();
|
void light_on();
|
||||||
void light_off();
|
void light_off();
|
||||||
LightState get_light_state() const;
|
LightState get_light_state() const;
|
||||||
|
|
||||||
// lock
|
// lock
|
||||||
void toggle_lock();
|
void lock_toggle();
|
||||||
void lock();
|
void lock();
|
||||||
void unlock();
|
void unlock();
|
||||||
|
|
||||||
|
@ -217,21 +167,13 @@ namespace ratgdo {
|
||||||
void subscribe_learn_state(std::function<void(LearnState)>&& f);
|
void subscribe_learn_state(std::function<void(LearnState)>&& f);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// tx data
|
|
||||||
bool transmit_pending_ { false };
|
|
||||||
uint32_t transmit_pending_start_ { 0 };
|
|
||||||
WirePacket tx_packet_;
|
|
||||||
|
|
||||||
RATGDOStore isr_store_ {};
|
RATGDOStore isr_store_ {};
|
||||||
SoftwareSerial sw_serial_;
|
protocol::Protocol* protocol_;
|
||||||
|
|
||||||
bool obstruction_from_status_ { false };
|
bool obstruction_from_status_ { false };
|
||||||
|
|
||||||
InternalGPIOPin* output_gdo_pin_;
|
InternalGPIOPin* output_gdo_pin_;
|
||||||
InternalGPIOPin* input_gdo_pin_;
|
InternalGPIOPin* input_gdo_pin_;
|
||||||
InternalGPIOPin* input_obst_pin_;
|
InternalGPIOPin* input_obst_pin_;
|
||||||
uint64_t client_id_ { 0x539 };
|
|
||||||
|
|
||||||
}; // RATGDOComponent
|
}; // RATGDOComponent
|
||||||
|
|
||||||
} // namespace ratgdo
|
} // namespace ratgdo
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
************************************/
|
************************************/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "enum.h"
|
#include "macros.h"
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@ -64,6 +64,11 @@ namespace ratgdo {
|
||||||
(RELEASED, 1),
|
(RELEASED, 1),
|
||||||
(UNKNOWN, 2))
|
(UNKNOWN, 2))
|
||||||
|
|
||||||
|
ENUM(BatteryState, uint8_t,
|
||||||
|
(UNKNOWN, 0),
|
||||||
|
(CHARGING, 0x6),
|
||||||
|
(FULL, 0x8))
|
||||||
|
|
||||||
/// Enum for learn states.
|
/// Enum for learn states.
|
||||||
ENUM(LearnState, uint8_t,
|
ENUM(LearnState, uint8_t,
|
||||||
(INACTIVE, 0),
|
(INACTIVE, 0),
|
||||||
|
@ -79,5 +84,39 @@ namespace ratgdo {
|
||||||
(ACCESSORY, 4),
|
(ACCESSORY, 4),
|
||||||
(UNKNOWN, 0xff))
|
(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 ratgdo
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -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<RxCommand> 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<RxCommand> 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<uint8_t>(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<uint32_t>(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<CommandType> Secplus1::pending_tx()
|
||||||
|
{
|
||||||
|
if (this->pending_tx_.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto cmd = this->pending_tx_.top();
|
||||||
|
if (cmd.time > millis()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return cmd.request;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<CommandType> 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
|
|
@ -0,0 +1,156 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#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<RxCommand> 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<RxCommand> decode_packet(const RxPacket& packet) const;
|
||||||
|
|
||||||
|
void enqueue_transmit(CommandType cmd, uint32_t time = 0);
|
||||||
|
optional<CommandType> pending_tx();
|
||||||
|
optional<CommandType> 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<void(DoorState)> 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<TxCommand, std::vector<TxCommand>, 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
|
|
@ -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<uint8_t>(action)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::lock_action(LockAction action)
|
||||||
|
{
|
||||||
|
if (action == LockAction::UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->send_command(Command(CommandType::LOCK, static_cast<uint8_t>(action)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::door_action(DoorAction action)
|
||||||
|
{
|
||||||
|
if (action == DoorAction::UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->door_command(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Secplus2::call(Args args)
|
||||||
|
{
|
||||||
|
using Tag = Args::Tag;
|
||||||
|
if (args.tag == Tag::query_status) {
|
||||||
|
this->send_command(CommandType::GET_STATUS);
|
||||||
|
} else if (args.tag == Tag::query_openings) {
|
||||||
|
this->send_command(CommandType::GET_OPENINGS);
|
||||||
|
} else if (args.tag == Tag::get_rolling_code_counter) {
|
||||||
|
return Result(RollingCodeCounter { std::addressof(this->rolling_code_counter_) });
|
||||||
|
} else if (args.tag == Tag::set_rolling_code_counter) {
|
||||||
|
this->set_rolling_code_counter(args.value.set_rolling_code_counter.counter);
|
||||||
|
} else if (args.tag == Tag::set_client_id) {
|
||||||
|
this->set_client_id(args.value.set_client_id.client_id);
|
||||||
|
} else if (args.tag == Tag::query_paired_devices) {
|
||||||
|
this->query_paired_devices(args.value.query_paired_devices.kind);
|
||||||
|
} else if (args.tag == Tag::query_paired_devices_all) {
|
||||||
|
this->query_paired_devices();
|
||||||
|
} else if (args.tag == Tag::clear_paired_devices) {
|
||||||
|
this->clear_paired_devices(args.value.clear_paired_devices.kind);
|
||||||
|
} else if (args.tag == Tag::activate_learn) {
|
||||||
|
this->activate_learn();
|
||||||
|
} else if (args.tag == Tag::inactivate_learn) {
|
||||||
|
this->inactivate_learn();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::door_command(DoorAction action)
|
||||||
|
{
|
||||||
|
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 1, 1), IncrementRollingCode::NO, [=]() {
|
||||||
|
this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] {
|
||||||
|
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 0, 1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::query_status()
|
||||||
|
{
|
||||||
|
this->send_command(CommandType::GET_STATUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::query_openings()
|
||||||
|
{
|
||||||
|
this->send_command(CommandType::GET_OPENINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Secplus2::query_paired_devices()
|
||||||
|
{
|
||||||
|
const auto kinds = {
|
||||||
|
PairedDevice::ALL,
|
||||||
|
PairedDevice::REMOTE,
|
||||||
|
PairedDevice::KEYPAD,
|
||||||
|
PairedDevice::WALL_CONTROL,
|
||||||
|
PairedDevice::ACCESSORY
|
||||||
|
};
|
||||||
|
uint32_t timeout = 0;
|
||||||
|
for (auto kind : kinds) {
|
||||||
|
timeout += 200;
|
||||||
|
this->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<uint8_t>(kind) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// wipe devices from memory based on get paired devices nibble values
|
||||||
|
void Secplus2::clear_paired_devices(PairedDevice kind)
|
||||||
|
{
|
||||||
|
if (kind == PairedDevice::UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Clear paired devices of type: %s", 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<uint8_t>(PairedDevice::REMOTE) - 1 }); }); // wireless
|
||||||
|
this->scheduler_->set_timeout(this->ratgdo_, "", 400, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::KEYPAD) - 1 }); }); // keypads
|
||||||
|
this->scheduler_->set_timeout(this->ratgdo_, "", 600, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::WALL_CONTROL) - 1 }); }); // wall controls
|
||||||
|
this->scheduler_->set_timeout(this->ratgdo_, "", 800, [=] { this->send_command(Command { CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(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<uint8_t>(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<Command> 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<Command> 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<uint16_t>((cmd.byte1 << 8) | cmd.byte2), cmd.nibble });
|
||||||
|
} else if (cmd.type == CommandType::SET_TTC) {
|
||||||
|
this->ratgdo_->received(TimeToClose { static_cast<uint16_t>((cmd.byte1 << 8) | cmd.byte2) });
|
||||||
|
} else if (cmd.type == CommandType::PAIRED_DEVICES) {
|
||||||
|
PairedDeviceCount pdc;
|
||||||
|
pdc.kind = to_PairedDevice(cmd.nibble, PairedDevice::UNKNOWN);
|
||||||
|
if (pdc.kind == PairedDevice::ALL) {
|
||||||
|
pdc.count = cmd.byte2;
|
||||||
|
} else if (pdc.kind == PairedDevice::REMOTE) {
|
||||||
|
pdc.count = cmd.byte2;
|
||||||
|
} else if (pdc.kind == PairedDevice::KEYPAD) {
|
||||||
|
pdc.count = cmd.byte2;
|
||||||
|
} else if (pdc.kind == PairedDevice::WALL_CONTROL) {
|
||||||
|
pdc.count = cmd.byte2;
|
||||||
|
} else if (pdc.kind == PairedDevice::ACCESSORY) {
|
||||||
|
pdc.count = cmd.byte2;
|
||||||
|
}
|
||||||
|
this->ratgdo_->received(pdc);
|
||||||
|
} else if (cmd.type == CommandType::BATTERY_STATUS) {
|
||||||
|
this->ratgdo_->received(to_BatteryState(cmd.byte1, BatteryState::UNKNOWN));
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOG1(TAG, "Done handle command: %s", 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<void()>&& 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<uint64_t>(command.type);
|
||||||
|
uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
|
||||||
|
uint32_t data = (static_cast<uint64_t>(command.byte2) << 24) | (static_cast<uint64_t>(command.byte1) << 16) | (static_cast<uint64_t>(command.nibble) << 8) | (cmd & 0xff);
|
||||||
|
|
||||||
|
ESP_LOG2(TAG, "[%ld] Encode for transmit rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), *this->rolling_code_counter_, fixed, data);
|
||||||
|
encode_wireline(*this->rolling_code_counter_, fixed, data, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Secplus2::transmit_packet()
|
||||||
|
{
|
||||||
|
auto now = micros();
|
||||||
|
|
||||||
|
while (micros() - now < 1300) {
|
||||||
|
if (this->rx_pin_->digital_read()) {
|
||||||
|
if (!this->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
|
|
@ -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<uint16_t>(cmd_e); }
|
||||||
|
inline bool operator==(const CommandType& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
||||||
|
|
||||||
|
enum class IncrementRollingCode {
|
||||||
|
NO,
|
||||||
|
YES,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Command {
|
||||||
|
CommandType type;
|
||||||
|
uint8_t nibble;
|
||||||
|
uint8_t byte1;
|
||||||
|
uint8_t byte2;
|
||||||
|
|
||||||
|
Command()
|
||||||
|
: type(CommandType::UNKNOWN)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
Command(CommandType type_, uint8_t nibble_ = 0, uint8_t byte1_ = 0, uint8_t byte2_ = 0)
|
||||||
|
: type(type_)
|
||||||
|
, nibble(nibble_)
|
||||||
|
, byte1(byte1_)
|
||||||
|
, byte2(byte2_)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Secplus2 : public Protocol {
|
||||||
|
public:
|
||||||
|
void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
|
||||||
|
void loop();
|
||||||
|
void dump_config();
|
||||||
|
|
||||||
|
void 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<Command> 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<void()>&& 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<Command> decode_packet(const WirePacket& packet) const;
|
||||||
|
|
||||||
|
void sync_helper(uint32_t start, uint32_t delay, uint8_t tries);
|
||||||
|
|
||||||
|
LearnState learn_state_ { LearnState::UNKNOWN };
|
||||||
|
|
||||||
|
observable<uint32_t> rolling_code_counter_ { 0 };
|
||||||
|
uint64_t client_id_ { 0x539 };
|
||||||
|
|
||||||
|
bool transmit_pending_ { false };
|
||||||
|
uint32_t transmit_pending_start_ { 0 };
|
||||||
|
WirePacket tx_packet_;
|
||||||
|
OnceCallbacks<void()> on_command_sent_;
|
||||||
|
|
||||||
|
Traits traits_;
|
||||||
|
|
||||||
|
SoftwareSerial sw_serial_;
|
||||||
|
|
||||||
|
InternalGPIOPin* tx_pin_;
|
||||||
|
InternalGPIOPin* rx_pin_;
|
||||||
|
|
||||||
|
RATGDOComponent* ratgdo_;
|
||||||
|
Scheduler* scheduler_;
|
||||||
|
};
|
||||||
|
} // namespace secplus2
|
||||||
|
} // namespace ratgdo
|
||||||
|
} // namespace esphome
|
|
@ -38,4 +38,3 @@ async def to_code(config):
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
cg.add(var.set_ratgdo_sensor_type(config[CONF_TYPE]))
|
cg.add(var.set_ratgdo_sensor_type(config[CONF_TYPE]))
|
||||||
await register_ratgdo_child(var, config)
|
await register_ratgdo_child(var, config)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace ratgdo {
|
||||||
{
|
{
|
||||||
if (this->switch_type_ == SwitchType::RATGDO_LEARN) {
|
if (this->switch_type_ == SwitchType::RATGDO_LEARN) {
|
||||||
this->parent_->subscribe_learn_state([=](LearnState state) {
|
this->parent_->subscribe_learn_state([=](LearnState state) {
|
||||||
this->publish_state(state==LearnState::ACTIVE);
|
this->publish_state(state == LearnState::ACTIVE);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,15 +154,37 @@
|
||||||
<p>
|
<p>
|
||||||
Pick your board to flash your ratgdo board with ESPhome for <a href="https://paulwieland.github.io/ratgdo/">ratgdo</a>.
|
Pick your board to flash your ratgdo board with ESPhome for <a href="https://paulwieland.github.io/ratgdo/">ratgdo</a>.
|
||||||
No programming or other software required.
|
No programming or other software required.
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>Residential overhead mounted openers
|
||||||
|
<ul>
|
||||||
|
<li>with a yellow learn button are Security + 2.0</li>
|
||||||
|
<li>with a red, purple or orange learn button are Security + “1.0”</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Residential wall mounted jackshaft openers
|
||||||
|
<ul>
|
||||||
|
<li>With model 8500<strong>W</strong> or RJ070 are Security + 2.0</li>
|
||||||
|
<li>All others are Security + 1.0</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Security + 2.0 door openers require ratgdo v2.0 control board or later</li>
|
||||||
|
<li>Security + 1.0 & Dry Contact door openers require v2.5 control board or later</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><em>Note: At the moment ESPHome only supports Security + 2.0 door openers (Yellow learn button). Support for other protocols is coming.</em></p>
|
<p>
|
||||||
|
<em>
|
||||||
|
Security + 1.0 support is experimental and may not work for all openers. Dry contact support is coming soon.
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3>v2.5i/2.52i Board</h3>
|
<h3>v2.5i/2.52i Board Security+ 2.0</h3>
|
||||||
<div class="radios">
|
<div class="radios">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v25iboard" checked />
|
<input type="radio" name="type" value="v25iboard" checked />
|
||||||
<img src="./v25iboard.png" alt="ratgdo v2.5i/2.52i board" />
|
<img src="./v25iboard.png" alt="ratgdo v2.5i/2.52i board with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -171,16 +193,24 @@
|
||||||
<li><a href="https://user-images.githubusercontent.com/4663918/277838851-e338c3bf-4eda-447a-9e79-737aa1a622a0.png">Version 2.5i Dry Contact Wiring Diagram</a></li>
|
<li><a href="https://user-images.githubusercontent.com/4663918/277838851-e338c3bf-4eda-447a-9e79-737aa1a622a0.png">Version 2.5i Dry Contact Wiring Diagram</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
<h3>v2.5i/2.52i Board Security+ 1.0</h3>
|
||||||
|
<div class="radios">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="type" value="v25iboard_secplusv1" checked />
|
||||||
|
<img src="./v25iboard_secplusv1.png" alt="ratgdo v2.5i/2.52i board with Security+ 1.0" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>v2.5 Board</h3>
|
|
||||||
|
<h3>v2.5 Board Security+ 2.0</h3>
|
||||||
<div class="radios">
|
<div class="radios">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v25board_esp8266_d1_mini_lite" />
|
<input type="radio" name="type" value="v25board_esp8266_d1_mini_lite" />
|
||||||
<img src="./v25board_esp8266_d1_mini_lite.png" alt="ratgdo v2.5 board with ESP8266 D1 Mini Lite" />
|
<img src="./v25board_esp8266_d1_mini_lite.png" alt="ratgdo v2.5 board with ESP8266 D1 Mini Lite with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v25board_esp32_d1_mini" />
|
<input type="radio" name="type" value="v25board_esp32_d1_mini" />
|
||||||
<img src="./v25board_esp32_d1_mini.png" alt="ratgdo v2.5 board with ESP32 D1 Mini" />
|
<img src="./v25board_esp32_d1_mini.png" alt="ratgdo v2.5 board with ESP32 D1 Mini with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -190,23 +220,23 @@
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>v2.0 Board</h3>
|
<h3>v2.0 Board Security+ 2.0</h3>
|
||||||
<div class="radios">
|
<div class="radios">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v2board_esp8266_d1_mini_lite" />
|
<input type="radio" name="type" value="v2board_esp8266_d1_mini_lite" />
|
||||||
<img src="./v2board_esp8266_d1_mini_lite.png" alt="ratgdo v2.0 board with ESP8266 D1 Mini Lite" />
|
<img src="./v2board_esp8266_d1_mini_lite.png" alt="ratgdo v2.0 board with ESP8266 D1 Mini Lite with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v2board_esp8266_d1_mini" />
|
<input type="radio" name="type" value="v2board_esp8266_d1_mini" />
|
||||||
<img src="./v2board_esp8266_d1_mini.png" alt="ratgdo v2.0 board with ESP8266 D1 Mini" />
|
<img src="./v2board_esp8266_d1_mini.png" alt="ratgdo v2.0 board with ESP8266 D1 Mini with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v2board_esp32_d1_mini" />
|
<input type="radio" name="type" value="v2board_esp32_d1_mini" />
|
||||||
<img src="./v2board_esp32_d1_mini.png" alt="ratgdo v2.0 board with ESP32 D1 Mini" />
|
<img src="./v2board_esp32_d1_mini.png" alt="ratgdo v2.0 board with ESP32 D1 Mini with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" name="type" value="v2board_esp32_lolin_s2_mini" />
|
<input type="radio" name="type" value="v2board_esp32_lolin_s2_mini" />
|
||||||
<img src="./v2board_esp32_lolin_s2_mini.png" alt="ratgdo v2.0 board with ESP32 lolin s2 mini" />
|
<img src="./v2board_esp32_lolin_s2_mini.png" alt="ratgdo v2.0 board with ESP32 lolin s2 mini with Security+ 2.0" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 296 KiB |
|
@ -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:
|
|
@ -0,0 +1 @@
|
||||||
|
static/v25iboard_secplusv1.yaml
|
Loading…
Reference in New Issue