Skip to content

Commit

Permalink
Add dfrobot_sen0395 mmwave radar component (esphome#4203)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Hills <[email protected]>
  • Loading branch information
niklasweber and jesserockz authored Oct 27, 2023
1 parent 0c5d5cd commit 907d438
Show file tree
Hide file tree
Showing 12 changed files with 1,265 additions and 1 deletion.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
208 changes: 208 additions & 0 deletions esphome/components/dfrobot_sen0395/__init__.py
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions esphome/components/dfrobot_sen0395/automation.h
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions esphome/components/dfrobot_sen0395/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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))
Loading

0 comments on commit 907d438

Please sign in to comment.