Skip to content

Commit

Permalink
Add template polygons
Browse files Browse the repository at this point in the history
  • Loading branch information
TillFleisch committed Jun 10, 2024
1 parent e96a3e7 commit 5ed53c3
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 9 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ A valid configuration may look like [this](examples/target_sensors.yaml) or this
The name of zone sub-sensor will be prefixed with the zone name. For instance, if the zone is called `Dining Table` and a sub-sensor of this zone is named `Occupancy`, the actual name of the Sensor will be `Dining Table Occupancy`.

- **name**(**Required**, string): The name of the zone. This name will be used as a prefix for sub-sensors.
- **polygon**(**Required**, polygon): A simple convex polygon with at least 3 vertices. See [Polygon](#polygon).
- **polygon**(**Required**, polygon): A simple convex polygon with at least 3 vertices or a template polygon. See [Polygon](#polygon).
- **margin**(**Optional**, distance): The margin that is added to the zone. Targets that are already being tracked, will still be tracked within the additional margin. This prevents on-off-flickering of related sensors. Defaults to `25cm`.
- **target_timeout**(**Optional**, time): The time after which a target within the zone is considered absent. This helps with continuous detection of non-moving targets. Targets which leave the zone via polygon boundaries are still detected as absent form the zone immediately. Defaults to `5s`.
- **occupancy**(**Optional**, binary sensor): A binary sensor, that will be triggered if at least one target is tracked inside the zone. `id` or `name` required. The default name is empty, which results in the sensor being named after the zone. All options from [Binary Sensor](https://esphome.io/components/binary_sensor/#config-binary-sensor).
Expand Down Expand Up @@ -165,6 +165,35 @@ A valid configuration may look like [this](examples/zones.yaml) or this:
y: 0m
```

Alternatively, a polygon can also be defined via a template expression like this:

```yaml
# Template polygon which updated every 2 seconds
polygon:
lambda: !lambda |-
return {ld2450::Point(1500,10), ld2450::Point(6000,10), ld2450::Point(6000,2600), ld2450::Point(-1500,2600)};
update_interval: 2s
```

- **lambda**(**Required**, `return std::vector<ld2450::Point>;`): List of Points which make up a convex polygon. The expression is evaluated every `update_interval`, if the provided polygon is invalid (i.e. not convex or too small) the previously used polygon is kept.
- **update_interval**(**Optional**, time): Interval in which the template polygon is evaluated. Set to `0s` to disable. Defaults to `1s`.

### LD2450.zone.update_polygon

The polygon used by a zone can be updated with the `zone.update_polygon` action.
The change is only effective, if the provided polygon is valid.

```yaml
on_...:
- LD2450.zone.update_polygon:
id: z4
polygon: !lambda |-
return {ld2450::Point(1500,10), ld2450::Point(6000,10), ld2450::Point(6000,2600), ld2450::Point(-1500,2600)};
```

- **id**(**Required**, id): Id of the zone which should be updated
- **polygon**(**Required**, `return std::vector<ld2450::Point>;`): List of Points which make up a convex polygon. The new polygon is only used if it's valid.

## Troubleshooting

When using Dupont connectors make sure they make proper contact. The very short pins on the LD2450 Sensor can easily go loose or break.
Expand Down
68 changes: 61 additions & 7 deletions components/LD2450/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import (
binary_sensor,
button,
Expand All @@ -13,10 +14,12 @@
from esphome.const import (
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
CONF_NAME,
CONF_RESTORE_VALUE,
CONF_STEP,
CONF_UNIT_OF_MEASUREMENT,
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_RESTART,
Expand Down Expand Up @@ -81,11 +84,14 @@
MaxDistanceNumber = ld2450_ns.class_("LimitNumber", cg.Component)
PollingSensor = ld2450_ns.class_("PollingSensor", cg.PollingComponent)
Zone = ld2450_ns.class_("Zone")
Point = ld2450_ns.class_("Point")
EmptyButton = ld2450_ns.class_("EmptyButton", button.Button, cg.Component)
TrackingModeSwitch = ld2450_ns.class_("TrackingModeSwitch", switch.Switch, cg.Component)
BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch, cg.Component)
BaudRateSelect = ld2450_ns.class_("BaudRateSelect", select.Select, cg.Component)
LimitTypeEnum = ld2450_ns.enum("LimitType")
UpdatePolygonAction = ld2450_ns.class_("UpdatePolygonAction", automation.Action)


DISTANCE_SENSOR_SCHEMA = (
sensor.sensor_schema(
Expand Down Expand Up @@ -226,6 +232,9 @@ def is_convex(points):
def validate_polygon(config):
"""Assert that the provided polygon is convex."""

if CONF_LAMBDA in config.get(CONF_POLYGON, []):
return config

points = []
for point_config in config[CONF_POLYGON]:
point_config = point_config[CONF_POINT]
Expand Down Expand Up @@ -277,8 +286,18 @@ def validate_min_max_angle(config):
cv.Optional(
CONF_TARGET_TIMEOUT, default="5s"
): cv.positive_time_period_milliseconds,
cv.Required(CONF_POLYGON): cv.All(
cv.ensure_list(POLYGON_SCHEMA), cv.Length(min=3)
cv.Required(CONF_POLYGON): cv.Any(
cv.All(cv.ensure_list(POLYGON_SCHEMA), cv.Length(min=3)),
cv.Schema(
{
cv.Required(CONF_LAMBDA): cv.templatable(
cv.ensure_list(Point)
),
cv.Optional(
CONF_UPDATE_INTERVAL, default="1s"
): cv.positive_time_period_milliseconds,
}
),
),
cv.Optional(CONF_OCCUPANCY): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
Expand Down Expand Up @@ -606,14 +625,27 @@ def zone_to_code(config):
cg.add(zone.set_target_timeout(config[CONF_TARGET_TIMEOUT]))

# Add points to the polygon of the zone object
for point_config in config[CONF_POLYGON]:
point_config = point_config[CONF_POINT]

if CONF_LAMBDA in config.get(CONF_POLYGON, []):
template_ = yield cg.process_lambda(
config[CONF_POLYGON][CONF_LAMBDA],
[],
return_type=cg.std_vector.template(Point),
)
cg.add(zone.set_template_polygon(template_))
cg.add(
zone.append_point(
(float(point_config[CONF_X])), float(point_config[CONF_Y])
zone.set_template_evaluation_interval(
config[CONF_POLYGON][CONF_UPDATE_INTERVAL]
)
)
else:
for point_config in config[CONF_POLYGON]:
point_config = point_config[CONF_POINT]

cg.add(
zone.append_point(
(float(point_config[CONF_X])), float(point_config[CONF_Y])
)
)

# Add binary occupancy sensor if present
if occupancy_config := config.get(CONF_OCCUPANCY):
Expand All @@ -638,3 +670,25 @@ def zone_to_code(config):
cg.add(zone.set_target_count_sensor(target_count_sensor))

return zone


@automation.register_action(
"LD2450.zone.update_polygon",
UpdatePolygonAction,
cv.All(
{
cv.Required(CONF_ID): cv.use_id(Zone),
cv.Required(CONF_POLYGON): cv.templatable(cv.ensure_list(Point)),
}
),
)
async def update_polygon_to_code(config, action_id, template_arg, args):
"""Code generation for the update (template) polygon action."""
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)

template_ = await cg.templatable(
config[CONF_POLYGON], args, cg.std_vector.template(Point)
)
cg.add(var.set_polygon(template_))
return var
26 changes: 26 additions & 0 deletions components/LD2450/zone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ namespace esphome::ld2450
int dy_1 = polygon[(i + 1) % size].y - polygon[i % size].y;
int dx_2 = polygon[(i + 2) % size].x - polygon[(i + 1) % size].x;
int dy_2 = polygon[(i + 2) % size].y - polygon[(i + 1) % size].y;

// Reject duplicate points
if ((dx_1 == 0 && dy_1 == 0) || (dx_2 == 0 && dy_2 == 0))
return false;

float cross_product = dx_1 * dy_2 - dy_1 * dx_2;
if (!std::isnan(last_cross_product) && ((cross_product > 0 && last_cross_product < 0) || (cross_product > 0 && last_cross_product < 0)))
return false;
Expand All @@ -30,6 +35,11 @@ namespace esphome::ld2450
ESP_LOGCONFIG(TAG, "Zone: %s", name_);
ESP_LOGCONFIG(TAG, " polygon_size: %i", polygon_.size());
ESP_LOGCONFIG(TAG, " polygon valid: %s", is_convex(polygon_) ? "true" : "false");
if (template_polygon_ != nullptr)
{
ESP_LOGCONFIG(TAG, " template polygon defined");
ESP_LOGCONFIG(TAG, " template polygon update interval: %i", template_evaluation_interval_);
}
ESP_LOGCONFIG(TAG, " target_timeout: %i", target_timeout_);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "OccupancyBinarySensor", occupancy_binary_sensor_);
Expand All @@ -41,6 +51,13 @@ namespace esphome::ld2450

void Zone::update(std::vector<Target *> &targets, bool sensor_available)
{
// evaluate custom template polygon at given interval
if (template_evaluation_interval_ != 0 && millis() - last_template_evaluation_ > template_evaluation_interval_)
{
last_template_evaluation_ = millis();
evaluate_template_polygon();
}

if (!sensor_available)
{
#ifdef USE_BINARY_SENSOR
Expand Down Expand Up @@ -172,4 +189,13 @@ namespace esphome::ld2450
}
return true;
}

bool Zone::evaluate_template_polygon()
{
if (template_polygon_ == nullptr)
return false;

std::vector<Point> val = (template_polygon_)();
return update_polygon(val);
}
} // namespace esphome::ld2450
63 changes: 62 additions & 1 deletion components/LD2450/zone.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,38 @@ namespace esphome::ld2450
if (!is_convex(polygon))
return false;
polygon_ = polygon;
return true;
}

/**
* @brief Defines a template polygon which will be evaluated regularly
*/
void set_template_polygon(std::function<std::vector<Point>()> &&template_polygon)
{
this->template_polygon_ = template_polygon;
}

/**
* @brief Sets the interval at which template polygons are evaluated
*/
void set_template_evaluation_interval(uint32_t interval)
{
template_evaluation_interval_ = interval;
}

/**
* @brief Evaluates the template polygon which is configured for this zone
* @return false if the polygon is not defined or invalid, true otherwise
*/
bool evaluate_template_polygon();

/**
* @brief Retrieves the currently used polygon
* @return list of points which make up the current polygon
*/
std::vector<Point> get_polygon()
{
return polygon_;
}

protected:
Expand All @@ -136,13 +168,42 @@ namespace esphome::ld2450
/// @brief List of points which make up a convex polygon
std::vector<Point> polygon_{};

/// @brief Margin around the polygon, which still in mm
/// @brief Margin around the polygon in mm, in which existing targets are tracked further
uint16_t margin_ = 250;

/// @brief timeout after which a target within the is considered absent
int target_timeout_ = 5000;

/// @brief Map of targets which are currently tracked inside of this polygon with their last seen timestamp
std::map<Target *, uint32_t> tracked_targets_{};

/// @brief Template polygon function
std::function<std::vector<Point>()> template_polygon_ = nullptr;

/// @brief timestamp of the last template evaluation
uint32_t last_template_evaluation_ = 0;

/// @brief interval in which the polygon template is evaluated (time in ms); 0 for no updates
uint32_t template_evaluation_interval_ = 1000;
};

template <typename... Ts>
class UpdatePolygonAction : public Action<Ts...>
{
public:
UpdatePolygonAction(Zone *parent)
: parent_(parent)
{
}

TEMPLATABLE_VALUE(std::vector<Point>, polygon)

void play(Ts... x) override
{
std::vector<Point> polygon = this->polygon_.value(x...);
this->parent_->update_polygon(polygon);
}

Zone *parent_;
};
} // namespace esphome::ld2450
35 changes: 35 additions & 0 deletions tests/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,38 @@ LD2450:
id: z2_occupancy
target_count:
id: z2_target_count

- zone:
name: "Template1"
margin: 0.4m
polygon:
lambda: !lambda |-
return {ld2450::Point(1500,10), ld2450::Point(6000,10), ld2450::Point(6000,2600), ld2450::Point(-1500,2600)};
update_interval: 2s
occupancy:
id: z3_occupancy

- zone:
name: "Template2"
polygon:
lambda: !lambda |-
return {ld2450::Point(1500,10), ld2450::Point(6000,10), ld2450::Point(6000,2600), ld2450::Point(-1500,2600)};
- zone:
name: "Template3"
id: zone_template_3
polygon:
lambda: !lambda |-
return {};
update_interval: 0s
occupancy:
id: z5_occupancy

button:
- platform: template
name: Update polygon
on_press:
- LD2450.zone.update_polygon:
id: zone_template_3
polygon: !lambda |-
return {ld2450::Point(1500,10), ld2450::Point(6000,10), ld2450::Point(6000,2600), ld2450::Point(-1500,2600)};

0 comments on commit 5ed53c3

Please sign in to comment.