From 907d43827cc1e617afec7342d28c27054103e7c8 Mon Sep 17 00:00:00 2001 From: niklasweber Date: Fri, 27 Oct 2023 06:56:33 +0200 Subject: [PATCH] Add dfrobot_sen0395 mmwave radar component (#4203) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/dfrobot_sen0395/__init__.py | 208 +++++++++++ .../components/dfrobot_sen0395/automation.h | 89 +++++ .../dfrobot_sen0395/binary_sensor.py | 22 ++ .../components/dfrobot_sen0395/commands.cpp | 329 ++++++++++++++++++ esphome/components/dfrobot_sen0395/commands.h | 156 +++++++++ .../dfrobot_sen0395/dfrobot_sen0395.cpp | 142 ++++++++ .../dfrobot_sen0395/dfrobot_sen0395.h | 125 +++++++ .../dfrobot_sen0395/switch/__init__.py | 65 ++++ .../switch/dfrobot_sen0395_switch.cpp | 48 +++ .../switch/dfrobot_sen0395_switch.h | 34 ++ tests/test1.yaml | 47 ++- 12 files changed, 1265 insertions(+), 1 deletion(-) create mode 100644 esphome/components/dfrobot_sen0395/__init__.py create mode 100644 esphome/components/dfrobot_sen0395/automation.h create mode 100644 esphome/components/dfrobot_sen0395/binary_sensor.py create mode 100644 esphome/components/dfrobot_sen0395/commands.cpp create mode 100644 esphome/components/dfrobot_sen0395/commands.h create mode 100644 esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp create mode 100644 esphome/components/dfrobot_sen0395/dfrobot_sen0395.h create mode 100644 esphome/components/dfrobot_sen0395/switch/__init__.py create mode 100644 esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp create mode 100644 esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h diff --git a/CODEOWNERS b/CODEOWNERS index 2e8a82898206..9ebc1ac631f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet +esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py new file mode 100644 index 000000000000..e772db5a15a0 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -0,0 +1,208 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome import core +from esphome.automation import maybe_simple_id +from esphome.const import CONF_ID +from esphome.components import uart + +CODEOWNERS = ["@niklasweber"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395") +DfrobotSen0395Component = dfrobot_sen0395_ns.class_( + "DfrobotSen0395Component", cg.Component +) + +# Actions +DfrobotSen0395ResetAction = dfrobot_sen0395_ns.class_( + "DfrobotSen0395ResetAction", automation.Action +) +DfrobotSen0395SettingsAction = dfrobot_sen0395_ns.class_( + "DfrobotSen0395SettingsAction", automation.Action +) + +CONF_DFROBOT_SEN0395_ID = "dfrobot_sen0395_id" + +CONF_DELAY_AFTER_DETECT = "delay_after_detect" +CONF_DELAY_AFTER_DISAPPEAR = "delay_after_disappear" +CONF_DETECTION_SEGMENTS = "detection_segments" +CONF_OUTPUT_LATENCY = "output_latency" +CONF_FACTORY_RESET = "factory_reset" +CONF_SENSITIVITY = "sensitivity" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DfrobotSen0395Component), + } + ).extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + +@automation.register_action( + "dfrobot_sen0395.reset", + DfrobotSen0395ResetAction, + maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DfrobotSen0395Component), + } + ), +) +async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + return var + + +def range_segment_list(input): + """Validate input is a list of ranges which can be used to configure the dfrobot mmwave radar + + A list of segments should be provided. A minimum of one segment is required and a maximum of + four segments is allowed. A segment describes a range of distances. E.g. from 0mm to 1m. + The distances need to be defined in an ascending order and they cannot contain / intersect + each other. + """ + + # Flatten input to one dimensional list + flat_list = [] + if isinstance(input, list): + for list_item in input: + if isinstance(list_item, list): + for item in list_item: + flat_list.append(item) + else: + flat_list.append(list_item) + else: + flat_list.append(input) + + input = flat_list + + if len(input) < 2: + raise cv.Invalid( + "At least two values need to be specified (start + stop distances)" + ) + if len(input) % 2 != 0: + raise cv.Invalid( + "An even number of arguments must be specified (pairs of min + max)" + ) + if len(input) > 8: + raise cv.Invalid( + "Maximum four segments can be specified (8 values: 4 * min + max)" + ) + + largest_distance = -1 + for distance in input: + if isinstance(distance, core.Lambda): + continue + m = cv.distance(distance) + if m > 9: + raise cv.Invalid("Maximum distance is 9m") + if m < 0: + raise cv.Invalid("Minimum distance is 0m") + if m <= largest_distance: + raise cv.Invalid( + "Distances must be delared from small to large " + "and they cannot contain each other" + ) + largest_distance = m + # Replace distance object with meters float + input[input.index(distance)] = m + + return input + + +MMWAVE_SETTINGS_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(DfrobotSen0395Component), + cv.Optional(CONF_FACTORY_RESET): cv.templatable(cv.boolean), + cv.Optional(CONF_DETECTION_SEGMENTS): range_segment_list, + cv.Optional(CONF_OUTPUT_LATENCY): { + cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable( + cv.All( + cv.positive_time_period, + cv.Range(max=core.TimePeriod(seconds=1638.375)), + ) + ), + cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable( + cv.All( + cv.positive_time_period, + cv.Range(max=core.TimePeriod(seconds=1638.375)), + ) + ), + }, + cv.Optional(CONF_SENSITIVITY): cv.templatable(cv.int_range(min=0, max=9)), + } +).add_extra( + cv.has_at_least_one_key( + CONF_FACTORY_RESET, + CONF_DETECTION_SEGMENTS, + CONF_OUTPUT_LATENCY, + CONF_SENSITIVITY, + ) +) + + +@automation.register_action( + "dfrobot_sen0395.settings", + DfrobotSen0395SettingsAction, + MMWAVE_SETTINGS_SCHEMA, +) +async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if factory_reset_config := config.get(CONF_FACTORY_RESET): + template_ = await cg.templatable(factory_reset_config, args, int) + cg.add(var.set_factory_reset(template_)) + + if CONF_DETECTION_SEGMENTS in config: + segments = config[CONF_DETECTION_SEGMENTS] + + if len(segments) >= 2: + template_ = await cg.templatable(segments[0], args, float) + cg.add(var.set_det_min1(template_)) + template_ = await cg.templatable(segments[1], args, float) + cg.add(var.set_det_max1(template_)) + if len(segments) >= 4: + template_ = await cg.templatable(segments[2], args, float) + cg.add(var.set_det_min2(template_)) + template_ = await cg.templatable(segments[3], args, float) + cg.add(var.set_det_max2(template_)) + if len(segments) >= 6: + template_ = await cg.templatable(segments[4], args, float) + cg.add(var.set_det_min3(template_)) + template_ = await cg.templatable(segments[5], args, float) + cg.add(var.set_det_max3(template_)) + if len(segments) >= 8: + template_ = await cg.templatable(segments[6], args, float) + cg.add(var.set_det_min4(template_)) + template_ = await cg.templatable(segments[7], args, float) + cg.add(var.set_det_max4(template_)) + if CONF_OUTPUT_LATENCY in config: + template_ = await cg.templatable( + config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DETECT], args, float + ) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds / 1000 + cg.add(var.set_delay_after_detect(template_)) + + template_ = await cg.templatable( + config[CONF_OUTPUT_LATENCY][CONF_DELAY_AFTER_DISAPPEAR], args, float + ) + if isinstance(template_, cv.TimePeriod): + template_ = template_.total_milliseconds / 1000 + cg.add(var.set_delay_after_disappear(template_)) + if CONF_SENSITIVITY in config: + template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int) + cg.add(var.set_sensitivity(template_)) + + return var diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h new file mode 100644 index 000000000000..1f942c02e4b2 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -0,0 +1,89 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include "dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +template +class DfrobotSen0395ResetAction : public Action, public Parented { + public: + void play(Ts... x) { this->parent_->enqueue(make_unique()); } +}; + +template +class DfrobotSen0395SettingsAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int8_t, factory_reset) + TEMPLATABLE_VALUE(int8_t, start_after_power_on) + TEMPLATABLE_VALUE(int8_t, turn_on_led) + TEMPLATABLE_VALUE(int8_t, presence_via_uart) + TEMPLATABLE_VALUE(int8_t, sensitivity) + TEMPLATABLE_VALUE(float, delay_after_detect) + TEMPLATABLE_VALUE(float, delay_after_disappear) + TEMPLATABLE_VALUE(float, det_min1) + TEMPLATABLE_VALUE(float, det_max1) + TEMPLATABLE_VALUE(float, det_min2) + TEMPLATABLE_VALUE(float, det_max2) + TEMPLATABLE_VALUE(float, det_min3) + TEMPLATABLE_VALUE(float, det_max3) + TEMPLATABLE_VALUE(float, det_min4) + TEMPLATABLE_VALUE(float, det_max4) + + void play(Ts... x) { + this->parent_->enqueue(make_unique(0)); + if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { + this->parent_->enqueue(make_unique()); + } + if (this->det_min1_.has_value() && this->det_max1_.has_value()) { + if (this->det_min1_.value() >= 0 && this->det_max1_.value() >= 0) { + this->parent_->enqueue(make_unique( + this->det_min1_.value_or(-1), this->det_max1_.value_or(-1), this->det_min2_.value_or(-1), + this->det_max2_.value_or(-1), this->det_min3_.value_or(-1), this->det_max3_.value_or(-1), + this->det_min4_.value_or(-1), this->det_max4_.value_or(-1))); + } + } + if (this->delay_after_detect_.has_value() && this->delay_after_disappear_.has_value()) { + float detect = this->delay_after_detect_.value(x...); + float disappear = this->delay_after_disappear_.value(x...); + if (detect >= 0 && disappear >= 0) { + this->parent_->enqueue(make_unique(detect, disappear)); + } + } + if (this->start_after_power_on_.has_value()) { + int8_t val = this->start_after_power_on_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->turn_on_led_.has_value()) { + int8_t val = this->turn_on_led_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->presence_via_uart_.has_value()) { + int8_t val = this->presence_via_uart_.value(x...); + if (val >= 0) { + this->parent_->enqueue(make_unique(val)); + } + } + if (this->sensitivity_.has_value()) { + int8_t val = this->sensitivity_.value(x...); + if (val >= 0) { + if (val > 9) { + val = 9; + } + this->parent_->enqueue(make_unique(val)); + } + } + this->parent_->enqueue(make_unique()); + this->parent_->enqueue(make_unique(1)); + } +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/binary_sensor.py b/esphome/components/dfrobot_sen0395/binary_sensor.py new file mode 100644 index 000000000000..2fd0510476c7 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/binary_sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import DEVICE_CLASS_MOTION +from . import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component + +DEPENDENCIES = ["dfrobot_sen0395"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION +).extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) + binary_sens = await binary_sensor.new_binary_sensor(config) + + cg.add(parent.set_detected_binary_sensor(binary_sens)) diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp new file mode 100644 index 000000000000..3a89b2b71e8a --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -0,0 +1,329 @@ +#include "commands.h" + +#include "esphome/core/log.h" + +#include "dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +static const char *const TAG = "dfrobot_sen0395.commands"; + +uint8_t Command::execute(DfrobotSen0395Component *parent) { + this->parent_ = parent; + if (this->cmd_sent_) { + if (this->parent_->read_message_()) { + std::string message(this->parent_->read_buffer_); + if (message.rfind("is not recognized as a CLI command") != std::string::npos) { + ESP_LOGD(TAG, "Command not recognized properly by sensor"); + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + return 0; + } else { + this->parent_->find_prompt_(); + return 1; // Command done + } + } + uint8_t rc = on_message(message); + if (rc == 2) { + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + return 0; + } else { + this->parent_->find_prompt_(); + return 1; // Command done + } + } else if (rc == 0) { + return 0; + } else { + this->parent_->find_prompt_(); + return 1; + } + } + if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) { + ESP_LOGD(TAG, "Command timeout"); + if (this->retries_left_ > 0) { + this->retries_left_ -= 1; + this->cmd_sent_ = false; + ESP_LOGD(TAG, "Retrying..."); + } else { + return 1; // Command done + } + } + } else if (this->parent_->send_cmd_(this->cmd_.c_str(), this->cmd_duration_ms_)) { + this->cmd_sent_ = true; + } + return 0; // Command not done yet +} + +uint8_t ReadStateCommand::execute(DfrobotSen0395Component *parent) { + this->parent_ = parent; + if (this->parent_->read_message_()) { + std::string message(this->parent_->read_buffer_); + if (message.rfind("$JYBSS,0, , , *") != std::string::npos) { + this->parent_->set_detected_(false); + this->parent_->set_active(true); + return 1; // Command done + } else if (message.rfind("$JYBSS,1, , , *") != std::string::npos) { + this->parent_->set_detected_(true); + this->parent_->set_active(true); + return 1; // Command done + } + } + if (millis() - this->parent_->ts_last_cmd_sent_ > this->timeout_ms_) { + return 1; // Command done, timeout + } + return 0; // Command not done yet. +} + +uint8_t ReadStateCommand::on_message(std::string &message) { return 1; } + +uint8_t PowerCommand::on_message(std::string &message) { + if (message == "sensor stopped already") { + this->parent_->set_active(false); + ESP_LOGI(TAG, "Stopped sensor (already stopped)"); + return 1; // Command done + } else if (message == "sensor started already") { + this->parent_->set_active(true); + ESP_LOGI(TAG, "Started sensor (already started)"); + return 1; // Command done + } else if (message == "new parameter isn't save, can't startSensor") { + this->parent_->set_active(false); + ESP_LOGE(TAG, "Can't start sensor! (Use SaveCfgCommand to save config first)"); + return 1; // Command done + } else if (message == "Done") { + this->parent_->set_active(this->power_on_); + if (this->power_on_) { + ESP_LOGI(TAG, "Started sensor"); + } else { + ESP_LOGI(TAG, "Stopped sensor"); + } + return 1; // Command done + } + return 0; // Command not done yet. +} + +DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, + float min4, float max4) { + // TODO: Print warning when values are rounded + if (min1 < 0 || max1 < 0) { + this->min1_ = min1 = 0; + this->max1_ = max1 = 0; + this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = + this->max4_ = max4 = -1; + + ESP_LOGW(TAG, "DetRangeCfgCommand invalid input parameters. Using range config 0 0."); + + this->cmd_ = "detRangeCfg -1 0 0"; + } else if (min2 < 0 || max2 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = + this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); + } else if (min3 < 0 || max3 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15); + } else if (min4 < 0 || max4 < 0) { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = round(min3 / 0.15) * 0.15; + this->max3_ = max3 = round(max3 / 0.15) * 0.15; + this->min4_ = min4 = this->max4_ = max4 = -1; + + this->cmd_ = str_sprintf("detRangeCfg -1 " + "%.0f %.0f %.0f %.0f %.0f %.0f", + min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15); + } else { + this->min1_ = min1 = round(min1 / 0.15) * 0.15; + this->max1_ = max1 = round(max1 / 0.15) * 0.15; + this->min2_ = min2 = round(min2 / 0.15) * 0.15; + this->max2_ = max2 = round(max2 / 0.15) * 0.15; + this->min3_ = min3 = round(min3 / 0.15) * 0.15; + this->max3_ = max3 = round(max3 / 0.15) * 0.15; + this->min4_ = min4 = round(min4 / 0.15) * 0.15; + this->max4_ = max4 = round(max4 / 0.15) * 0.15; + + this->cmd_ = str_sprintf("detRangeCfg -1 " + "%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", + min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, + max4 / 0.15); + } + + this->min1_ = min1; + this->max1_ = max1; + this->min2_ = min2; + this->max2_ = max2; + this->min3_ = min3; + this->max3_ = max3; + this->min4_ = min4; + this->max4_ = max4; +}; + +uint8_t DetRangeCfgCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Updated detection area config:"); + ESP_LOGI(TAG, "Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); + if (this->min2_ >= 0 && this->max2_ >= 0) { + ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + } + if (this->min3_ >= 0 && this->max3_ >= 0) { + ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + } + if (this->min4_ >= 0 && this->max4_ >= 0) { + ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet. +} + +OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) { + delay_after_detection = round(delay_after_detection / 0.025) * 0.025; + delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025; + if (delay_after_detection < 0) + delay_after_detection = 0; + if (delay_after_detection > 1638.375) + delay_after_detection = 1638.375; + if (delay_after_disappear < 0) + delay_after_disappear = 0; + if (delay_after_disappear > 1638.375) + delay_after_disappear = 1638.375; + + this->delay_after_detection_ = delay_after_detection; + this->delay_after_disappear_ = delay_after_disappear; + + this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025); +}; + +uint8_t OutputLatencyCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Updated output latency config:"); + ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_); + ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_); + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SensorCfgStartCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot configure sensor startup behavior. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Updated sensor startup behavior:"); + if (startup_mode_) { + this->parent_->set_start_after_boot(true); + ESP_LOGI(TAG, "Sensor will start automatically after power-on."); + } else { + this->parent_->set_start_after_boot(false); + ESP_LOGI(TAG, "Sensor needs to be started manually after power-on."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t FactoryResetCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot factory reset. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Sensor factory reset done."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t ResetSystemCommand::on_message(std::string &message) { + if (message == "leapMMW:/>") { + ESP_LOGI(TAG, "Restarted sensor."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SaveCfgCommand::on_message(std::string &message) { + if (message == "no parameter has changed") { + ESP_LOGI(TAG, "Not saving config (no parameter changed)."); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Saved config. Saving a lot may damage the sensor."); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t LedModeCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set led mode. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set led mode done."); + if (this->active_) { + this->parent_->set_led_active(true); + ESP_LOGI(TAG, "Sensor LED will blink."); + } else { + this->parent_->set_led_active(false); + ESP_LOGI(TAG, "Turned off LED."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t UartOutputCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set uart output mode. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set uart mode done."); + if (this->active_) { + this->parent_->set_uart_presence_active(true); + ESP_LOGI(TAG, "Presence information is sent via UART and GPIO."); + } else { + this->parent_->set_uart_presence_active(false); + ESP_LOGI(TAG, "Presence information is only sent via GPIO."); + } + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +uint8_t SensitivityCommand::on_message(std::string &message) { + if (message == "sensor is not stopped") { + ESP_LOGE(TAG, "Cannot set sensitivity. Sensor is not stopped!"); + return 1; // Command done + } else if (message == "Done") { + ESP_LOGI(TAG, "Set sensitivity done. Set to value %d.", this->sensitivity_); + ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + return 1; // Command done + } + return 0; // Command not done yet +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h new file mode 100644 index 000000000000..7426d9732aac --- /dev/null +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +class DfrobotSen0395Component; + +// Use command queue and time stamps to avoid blocking. +// When component has run time, check if minimum time (1s) between +// commands has passed. After that run a command from the queue. +class Command { + public: + virtual ~Command() = default; + virtual uint8_t execute(DfrobotSen0395Component *parent); + virtual uint8_t on_message(std::string &message) = 0; + + protected: + DfrobotSen0395Component *parent_{nullptr}; + std::string cmd_; + bool cmd_sent_{false}; + int8_t retries_left_{2}; + uint32_t cmd_duration_ms_{1000}; + uint32_t timeout_ms_{1500}; +}; + +class ReadStateCommand : public Command { + public: + uint8_t execute(DfrobotSen0395Component *parent) override; + uint8_t on_message(std::string &message) override; + + protected: + uint32_t timeout_ms_{500}; +}; + +class PowerCommand : public Command { + public: + PowerCommand(bool power_on) : power_on_(power_on) { + if (power_on) { + cmd_ = "sensorStart"; + } else { + cmd_ = "sensorStop"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool power_on_; +}; + +class DetRangeCfgCommand : public Command { + public: + DetRangeCfgCommand(float min1, float max1, float min2, float max2, float min3, float max3, float min4, float max4); + uint8_t on_message(std::string &message) override; + + protected: + float min1_, max1_, min2_, max2_, min3_, max3_, min4_, max4_; + // TODO: Set min max values in component, so they can be published as sensor. +}; + +class OutputLatencyCommand : public Command { + public: + OutputLatencyCommand(float delay_after_detection, float delay_after_disappear); + uint8_t on_message(std::string &message) override; + + protected: + float delay_after_detection_; + float delay_after_disappear_; +}; + +class SensorCfgStartCommand : public Command { + public: + SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) { + char tmp_cmd[20] = {0}; + sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode); + cmd_ = std::string(tmp_cmd); + } + uint8_t on_message(std::string &message) override; + + protected: + bool startup_mode_; +}; + +class FactoryResetCommand : public Command { + public: + FactoryResetCommand() { cmd_ = "factoryReset 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; }; + uint8_t on_message(std::string &message) override; +}; + +class ResetSystemCommand : public Command { + public: + ResetSystemCommand() { cmd_ = "resetSystem"; } + uint8_t on_message(std::string &message) override; +}; + +class SaveCfgCommand : public Command { + public: + SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; } + uint8_t on_message(std::string &message) override; + + protected: + uint32_t cmd_duration_ms_{3000}; + uint32_t timeout_ms_{3500}; +}; + +class LedModeCommand : public Command { + public: + LedModeCommand(bool active) : active_(active) { + if (active) { + cmd_ = "setLedMode 1 0"; + } else { + cmd_ = "setLedMode 1 1"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool active_; +}; + +class UartOutputCommand : public Command { + public: + UartOutputCommand(bool active) : active_(active) { + if (active) { + cmd_ = "setUartOutput 1 1"; + } else { + cmd_ = "setUartOutput 1 0"; + } + }; + uint8_t on_message(std::string &message) override; + + protected: + bool active_; +}; + +class SensitivityCommand : public Command { + public: + SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) { + if (sensitivity > 9) + sensitivity_ = sensitivity = 9; + char tmp_cmd[20] = {0}; + sprintf(tmp_cmd, "setSensitivity %d", sensitivity); + cmd_ = std::string(tmp_cmd); + }; + uint8_t on_message(std::string &message) override; + + protected: + uint8_t sensitivity_; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp new file mode 100644 index 000000000000..f8ef6c713892 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -0,0 +1,142 @@ +#include "dfrobot_sen0395.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +static const char *const TAG = "dfrobot_sen0395"; +const char ASCII_CR = 0x0D; +const char ASCII_LF = 0x0A; + +void DfrobotSen0395Component::dump_config() { + ESP_LOGCONFIG(TAG, "Dfrobot Mmwave Radar:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Registered", this->detected_binary_sensor_); +#endif +#ifdef USE_SWITCH + LOG_SWITCH(" ", "Sensor Active Switch", this->sensor_active_switch_); + LOG_SWITCH(" ", "Turn on LED Switch", this->turn_on_led_switch_); + LOG_SWITCH(" ", "Presence via UART Switch", this->presence_via_uart_switch_); + LOG_SWITCH(" ", "Start after Boot Switch", this->start_after_boot_switch_); +#endif +} + +void DfrobotSen0395Component::loop() { + if (cmd_queue_.is_empty()) { + // Command queue empty. Read sensor state. + cmd_queue_.enqueue(make_unique()); + } + + // Commands are non-blocking and need to be called repeatedly. + if (cmd_queue_.process(this)) { + // Dequeue if command is done + cmd_queue_.dequeue(); + } +} + +int8_t DfrobotSen0395Component::enqueue(std::unique_ptr cmd) { + return cmd_queue_.enqueue(std::move(cmd)); // Transfer ownership using std::move +} + +uint8_t DfrobotSen0395Component::read_message_() { + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == MMWAVE_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte); + + if (byte == ASCII_CR) + continue; + if (byte >= 0x7F) + byte = '?'; // needs to be valid utf8 string for log functions. + this->read_buffer_[this->read_pos_] = byte; + + if (this->read_pos_ == 9 && byte == '>') + this->read_buffer_[++this->read_pos_] = ASCII_LF; + + if (this->read_buffer_[this->read_pos_] == ASCII_LF) { + this->read_buffer_[this->read_pos_] = 0; + this->read_pos_ = 0; + ESP_LOGV(TAG, "Message: %s", this->read_buffer_); + return 1; // Full message in buffer + } else { + this->read_pos_++; + } + } + return 0; // No full message yet +} + +uint8_t DfrobotSen0395Component::find_prompt_() { + if (this->read_message_()) { + std::string message(this->read_buffer_); + if (message.rfind("leapMMW:/>") != std::string::npos) { + return 1; // Prompt found + } + } + return 0; // Not found yet +} + +uint8_t DfrobotSen0395Component::send_cmd_(const char *cmd, uint32_t duration) { + // The interval between two commands must be larger than the specified duration (in ms). + if (millis() - ts_last_cmd_sent_ > duration) { + this->write_str(cmd); + ts_last_cmd_sent_ = millis(); + return 1; // Command sent + } + // Could not send command yet as command duration did not fully pass yet. + return 0; +} + +void DfrobotSen0395Component::set_detected_(bool detected) { + this->detected_ = detected; +#ifdef USE_BINARY_SENSOR + if (this->detected_binary_sensor_ != nullptr) + this->detected_binary_sensor_->publish_state(detected); +#endif +} + +int8_t CircularCommandQueue::enqueue(std::unique_ptr cmd) { + if (this->is_full()) { + ESP_LOGE(TAG, "Command queue is full"); + return -1; + } else if (this->is_empty()) + front_++; + rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE; + commands_[rear_] = std::move(cmd); // Transfer ownership using std::move + return 1; +} + +std::unique_ptr CircularCommandQueue::dequeue() { + if (this->is_empty()) + return nullptr; + std::unique_ptr dequeued_cmd = std::move(commands_[front_]); + if (front_ == rear_) { + front_ = -1; + rear_ = -1; + } else + front_ = (front_ + 1) % COMMAND_QUEUE_SIZE; + + return dequeued_cmd; +} + +bool CircularCommandQueue::is_empty() { return front_ == -1; } + +bool CircularCommandQueue::is_full() { return (rear_ + 1) % COMMAND_QUEUE_SIZE == front_; } + +// Run execute method of first in line command. +// Execute is non-blocking and has to be called until it returns 1. +uint8_t CircularCommandQueue::process(DfrobotSen0395Component *parent) { + if (!is_empty()) { + return commands_[front_]->execute(parent); + } else { + return 1; + } +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h new file mode 100644 index 000000000000..d3b2ecedc3c0 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.h @@ -0,0 +1,125 @@ +#pragma once + +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + +#include "commands.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +const uint8_t MMWAVE_READ_BUFFER_LENGTH = 255; + +// forward declaration due to circular dependency +class DfrobotSen0395Component; + +static const uint8_t COMMAND_QUEUE_SIZE = 20; + +class CircularCommandQueue { + public: + int8_t enqueue(std::unique_ptr cmd); + std::unique_ptr dequeue(); + bool is_empty(); + bool is_full(); + uint8_t process(DfrobotSen0395Component *parent); + + protected: + int front_{-1}; + int rear_{-1}; + std::unique_ptr commands_[COMMAND_QUEUE_SIZE]; +}; + +class DfrobotSen0395Component : public uart::UARTDevice, public Component { +#ifdef USE_SWITCH + SUB_SWITCH(sensor_active) + SUB_SWITCH(turn_on_led) + SUB_SWITCH(presence_via_uart) + SUB_SWITCH(start_after_boot) +#endif + + public: + void dump_config() override; + void loop() override; + void set_active(bool active) { + if (active != active_) { +#ifdef USE_SWITCH + if (this->sensor_active_switch_ != nullptr) + this->sensor_active_switch_->publish_state(active); +#endif + active_ = active; + } + } + bool is_active() { return active_; } + + void set_led_active(bool active) { + if (led_active_ != active) { +#ifdef USE_SWITCH + if (this->turn_on_led_switch_ != nullptr) + this->turn_on_led_switch_->publish_state(active); +#endif + led_active_ = active; + } + } + bool is_led_active() { return led_active_; } + + void set_uart_presence_active(bool active) { + uart_presence_active_ = active; +#ifdef USE_SWITCH + if (this->presence_via_uart_switch_ != nullptr) + this->presence_via_uart_switch_->publish_state(active); +#endif + } + bool is_uart_presence_active() { return uart_presence_active_; } + + void set_start_after_boot(bool start) { + start_after_boot_ = start; +#ifdef USE_SWITCH + if (this->start_after_boot_switch_ != nullptr) + this->start_after_boot_switch_->publish_state(start); +#endif + } + bool does_start_after_boot() { return start_after_boot_; } + +#ifdef USE_BINARY_SENSOR + void set_detected_binary_sensor(binary_sensor::BinarySensor *detected_binary_sensor) { + detected_binary_sensor_ = detected_binary_sensor; + } +#endif + + int8_t enqueue(std::unique_ptr cmd); + + protected: +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *detected_binary_sensor_{nullptr}; +#endif + + bool detected_{false}; + bool active_{false}; + bool led_active_{false}; + bool uart_presence_active_{false}; + bool start_after_boot_{false}; + char read_buffer_[MMWAVE_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + CircularCommandQueue cmd_queue_; + uint32_t ts_last_cmd_sent_{0}; + + uint8_t read_message_(); + uint8_t find_prompt_(); + uint8_t send_cmd_(const char *cmd, uint32_t duration); + + void set_detected_(bool detected); + + friend class Command; + friend class ReadStateCommand; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/switch/__init__.py b/esphome/components/dfrobot_sen0395/switch/__init__.py new file mode 100644 index 000000000000..b1c35d27acf3 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/__init__.py @@ -0,0 +1,65 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ENTITY_CATEGORY_CONFIG, CONF_TYPE + +from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component + + +DEPENDENCIES = ["dfrobot_sen0395"] + +dfrobot_sen0395_ns = cg.esphome_ns.namespace("dfrobot_sen0395") +DfrobotSen0395Switch = dfrobot_sen0395_ns.class_( + "DfrobotSen0395Switch", + switch.Switch, + cg.Component, + cg.Parented.template(DfrobotSen0395Component), +) + +Sen0395PowerSwitch = dfrobot_sen0395_ns.class_( + "Sen0395PowerSwitch", DfrobotSen0395Switch +) +Sen0395LedSwitch = dfrobot_sen0395_ns.class_("Sen0395LedSwitch", DfrobotSen0395Switch) +Sen0395UartPresenceSwitch = dfrobot_sen0395_ns.class_( + "Sen0395UartPresenceSwitch", DfrobotSen0395Switch +) +Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_( + "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch +) + +_SWITCH_SCHEMA = ( + switch.switch_schema( + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend( + { + cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.typed_schema( + { + "sensor_active": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} + ), + "turn_on_led": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)} + ), + "presence_via_uart": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)} + ), + "start_after_boot": _SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)} + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_DFROBOT_SEN0395_ID]) + var = await switch.new_switch(config) + await cg.register_component(var, config) + await cg.register_parented(var, parent) + cg.add(getattr(parent, f"set_{config[CONF_TYPE]}_switch")(var)) diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp new file mode 100644 index 000000000000..ca72d9453180 --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.cpp @@ -0,0 +1,48 @@ +#include "dfrobot_sen0395_switch.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +void Sen0395PowerSwitch::write_state(bool state) { this->parent_->enqueue(make_unique(state)); } + +void Sen0395LedSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395UartPresenceSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +void Sen0395StartAfterBootSwitch::write_state(bool state) { + bool was_active = false; + if (this->parent_->is_active()) { + was_active = true; + this->parent_->enqueue(make_unique(false)); + } + this->parent_->enqueue(make_unique(state)); + this->parent_->enqueue(make_unique()); + if (was_active) { + this->parent_->enqueue(make_unique(true)); + } +} + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h new file mode 100644 index 000000000000..ab32d81dd8fe --- /dev/null +++ b/esphome/components/dfrobot_sen0395/switch/dfrobot_sen0395_switch.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +#include "../dfrobot_sen0395.h" + +namespace esphome { +namespace dfrobot_sen0395 { + +class DfrobotSen0395Switch : public switch_::Switch, public Component, public Parented {}; + +class Sen0395PowerSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395LedSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395UartPresenceSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +class Sen0395StartAfterBootSwitch : public DfrobotSen0395Switch { + public: + void write_state(bool state) override; +}; + +} // namespace dfrobot_sen0395 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 0d95dd6cb81e..462b1ec197be 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -220,6 +220,10 @@ uart: baud_rate: 256000 parity: NONE stop_bits: 1 + - id: dfrobot_mmwave_uart + tx_pin: 14 + rx_pin: 27 + baud_rate: 115200 - id: gcja5_uart rx_pin: GPIO10 parity: EVEN @@ -332,6 +336,10 @@ mcp23s17: cs_pin: GPIO12 deviceaddress: 1 +dfrobot_sen0395: + - id: mmwave + uart_id: dfrobot_mmwave_uart + sensor: - platform: pmwcs3 i2c_id: i2c_bus @@ -1816,6 +1824,9 @@ binary_sensor: - platform: qwiic_pir i2c_id: i2c_bus name: "Qwiic PIR Motion Sensor" + - platform: dfrobot_sen0395 + id: mmwave_detected_uart + dfrobot_sen0395_id: mmwave pca9685: frequency: 500 @@ -2548,7 +2559,22 @@ switch: name: Haier turn_on_action: remote_transmitter.transmit_haier: - code: [0xA6, 0xDA, 0x00, 0x00, 0x40, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x05] + code: + [ + 0xA6, + 0xDA, + 0x00, + 0x00, + 0x40, + 0x40, + 0x00, + 0x80, + 0x00, + 0x00, + 0x00, + 0x00, + 0x05, + ] - platform: template name: Living Room Lights id: livingroom_lights @@ -3532,6 +3558,25 @@ button: name: Midea Power Inverse on_press: midea_ac.power_toggle: + - platform: template + name: Update Mmwave Sensor Settings + on_press: + - dfrobot_sen0395.settings: + id: mmwave + factory_reset: true + detection_segments: + - [0cm, 5m] + - 600cm + - !lambda |- + return 7; + output_latency: + delay_after_detect: 0s + delay_after_disappear: 0s + sensitivity: 6 + - platform: template + name: Reset Mmwave Sensor + on_press: + - dfrobot_sen0395.reset: - platform: template name: Poller component suspend test on_press: