Add dfrobot_sen0395 mmwave radar component (#4203)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
niklasweber 2023-10-27 06:56:33 +02:00 committed by GitHub
parent 0c5d5cd623
commit 907d43827c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1265 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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<typename... Ts>
class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public:
void play(Ts... x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
};
template<typename... Ts>
class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
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<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>());
}
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<DetRangeCfgCommand>(
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<OutputLatencyCommand>(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<SensorCfgStartCommand>(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<LedModeCommand>(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<UartOutputCommand>(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<SensitivityCommand>(val));
}
}
this->parent_->enqueue(make_unique<SaveCfgCommand>());
this->parent_->enqueue(make_unique<PowerCommand>(1));
}
};
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -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))

View file

@ -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

View file

@ -0,0 +1,156 @@
#pragma once
#include <cstdint>
#include <string>
#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

View file

@ -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<ReadStateCommand>());
}
// 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<Command> 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<Command> 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<Command> CircularCommandQueue::dequeue() {
if (this->is_empty())
return nullptr;
std::unique_ptr<Command> 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

View file

@ -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<Command> cmd);
std::unique_ptr<Command> dequeue();
bool is_empty();
bool is_full();
uint8_t process(DfrobotSen0395Component *parent);
protected:
int front_{-1};
int rear_{-1};
std::unique_ptr<Command> 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<Command> 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

View file

@ -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))

View file

@ -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<PowerCommand>(state)); }
void Sen0395LedSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<LedModeCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395UartPresenceSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<UartOutputCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
void Sen0395StartAfterBootSwitch::write_state(bool state) {
bool was_active = false;
if (this->parent_->is_active()) {
was_active = true;
this->parent_->enqueue(make_unique<PowerCommand>(false));
}
this->parent_->enqueue(make_unique<SensorCfgStartCommand>(state));
this->parent_->enqueue(make_unique<SaveCfgCommand>());
if (was_active) {
this->parent_->enqueue(make_unique<PowerCommand>(true));
}
}
} // namespace dfrobot_sen0395
} // namespace esphome

View file

@ -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<DfrobotSen0395Component> {};
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

View file

@ -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: