Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New 'Duty Time' sensor component #5069

Merged
merged 8 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/* @jesserockz
esphome/components/ens210/* @itn3rd77
Expand Down
1 change: 1 addition & 0 deletions esphome/components/duty_time/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CODEOWNERS = ["@dudanov"]
100 changes: 100 additions & 0 deletions esphome/components/duty_time/duty_time_sensor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#include "duty_time_sensor.h"
#include "esphome/core/hal.h"

namespace esphome {
namespace duty_time_sensor {

static const char *const TAG = "duty_time_sensor";

void DutyTimeSensor::set_sensor(const binary_sensor::BinarySensor *const sensor) {
this->func_ = [sensor]() { return sensor->state; };
dudanov marked this conversation as resolved.
Show resolved Hide resolved
}

void DutyTimeSensor::start() {
if (!this->last_state_)
this->process_state_(true);
}

void DutyTimeSensor::stop() {
if (this->last_state_)
this->process_state_(false);
}

void DutyTimeSensor::update() {
if (this->last_state_)
this->process_state_(true);
}

void DutyTimeSensor::loop() {
if (this->func_ == nullptr)
return;

const bool state = this->func_();

if (state != this->last_state_)
this->process_state_(state);
}

void DutyTimeSensor::setup() {
uint32_t seconds = 0;

if (this->restore_) {
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash());
this->pref_.load(&seconds);
}

this->set_value_(seconds);
}

void DutyTimeSensor::set_value_(const uint32_t sec) {
if (this->func_ != nullptr)
this->last_state_ = this->func_();

this->edge_ms_ = 0;
this->edge_sec_ = sec;
this->last_update_ = millis();
this->publish_and_save_(sec, 0);
}

void DutyTimeSensor::process_state_(const bool state) {
const uint32_t now = millis();

if (this->last_state_) {
// update or falling edge
this->counter_ms_ += now - this->last_update_;
this->publish_and_save_(this->counter_sec_ + this->counter_ms_ / 1000, this->counter_ms_ % 1000);

if (!state && this->last_duty_time_sensor_ != nullptr) {
// falling edge
const int32_t ms = this->counter_ms_ - this->edge_ms_;
const uint32_t sec = this->counter_sec_ - this->edge_sec_;
this->edge_ms_ = this->counter_ms_;
this->edge_sec_ = this->counter_sec_;
this->last_duty_time_sensor_->publish_state(sec + ms * 1e-3f);
}
}

this->last_update_ = now;
this->last_state_ = state;
}

void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) {
this->counter_ms_ = ms;
this->counter_sec_ = sec;
this->publish_state(sec + ms * 1e-3f);

if (this->restore_)
this->pref_.save(&sec);
}

void DutyTimeSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Duty Time:");
ESP_LOGCONFIG(TAG, " Update Interval: %dms", this->get_update_interval());
ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_));
ESP_LOGCONFIG(TAG, " Using Logical Source: %s", YESNO(this->func_ != nullptr));
LOG_SENSOR(" ", "Duty Time Sensor:", this);
LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_);
}

} // namespace duty_time_sensor
} // namespace esphome
90 changes: 90 additions & 0 deletions esphome/components/duty_time/duty_time_sensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"

namespace esphome {
namespace duty_time_sensor {

class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
public:
void setup() override;
void update() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }

void start();
void stop();
bool is_running() const { return this->last_state_; }
void reset() { this->set_value_(0); }

void set_lambda(std::function<bool()> &&func) { this->func_ = func; }
void set_sensor(const binary_sensor::BinarySensor *sensor);
void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; }
void set_restore(bool restore) { this->restore_ = restore; }

protected:
void set_value_(uint32_t sec);
void process_state_(bool state);
void publish_and_save_(uint32_t sec, uint32_t ms);

std::function<bool()> func_{nullptr};
sensor::Sensor *last_duty_time_sensor_{nullptr};
ESPPreferenceObject pref_;

uint32_t last_update_;
uint32_t counter_ms_;
uint32_t counter_sec_;
uint32_t edge_ms_;
uint32_t edge_sec_;
bool restore_;
bool last_state_{false};
};

template<typename... Ts> class StartAction : public Action<Ts...> {
public:
explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {}

void play(Ts... x) override { this->parent_->start(); }

protected:
DutyTimeSensor *parent_;
};

template<typename... Ts> class StopAction : public Action<Ts...> {
public:
explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {}

void play(Ts... x) override { this->parent_->stop(); }

protected:
DutyTimeSensor *parent_;
};

template<typename... Ts> class ResetAction : public Action<Ts...> {
public:
explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {}

void play(Ts... x) override { this->parent_->reset(); }

protected:
DutyTimeSensor *parent_;
};

template<typename... Ts> class RunningCondition : public Condition<Ts...> {
public:
explicit RunningCondition(DutyTimeSensor *parent, bool state) : parent_(parent), state_(state) {}

bool check(Ts... x) override { return this->parent_->is_running() == this->state_; }

protected:
DutyTimeSensor *parent_;
bool state_;
};

} // namespace duty_time_sensor
} // namespace esphome
121 changes: 121 additions & 0 deletions esphome/components/duty_time/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.automation import (
Action,
Condition,
maybe_simple_id,
register_action,
register_condition,
)
from esphome.components import binary_sensor, sensor
from esphome.const import (
CONF_ID,
CONF_SENSOR,
CONF_RESTORE,
CONF_LAMBDA,
UNIT_SECOND,
STATE_CLASS_TOTAL,
STATE_CLASS_TOTAL_INCREASING,
DEVICE_CLASS_DURATION,
ENTITY_CATEGORY_DIAGNOSTIC,
)

CONF_LAST_TIME = "last_time"

duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor")
DutyTimeSensor = duty_time_sensor_ns.class_(
"DutyTimeSensor", sensor.Sensor, cg.PollingComponent
)
StartAction = duty_time_sensor_ns.class_("StartAction", Action)
StopAction = duty_time_sensor_ns.class_("StopAction", Action)
ResetAction = duty_time_sensor_ns.class_("ResetAction", Action)
SetAction = duty_time_sensor_ns.class_("SetAction", Action)
RunningCondition = duty_time_sensor_ns.class_("RunningCondition", Condition)


CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
DutyTimeSensor,
unit_of_measurement=UNIT_SECOND,
icon="mdi:timer-play-outline",
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
)
.extend(
{
cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_LAMBDA): cv.lambda_,
cv.Optional(CONF_RESTORE, default=False): cv.boolean,
cv.Optional(CONF_LAST_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
icon="mdi:timer-marker-outline",
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s")),
cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA),
)


async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_restore(config[CONF_RESTORE]))
if CONF_SENSOR in config:
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_)
cg.add(var.set_lambda(lambda_))
if CONF_LAST_TIME in config:
sens = await sensor.new_sensor(config[CONF_LAST_TIME])
cg.add(var.set_last_duty_time_sensor(sens))


# AUTOMATIONS

DUTY_TIME_ID_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(DutyTimeSensor),
}
)


@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA)
async def sensor_runtime_start_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)


@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA)
async def sensor_runtime_stop_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)


@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA)
async def sensor_runtime_reset_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)


@register_condition(
"sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA
)
async def duty_time_is_running_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, True)


@register_condition(
"sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA
)
async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren, False)
23 changes: 23 additions & 0 deletions tests/test2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,36 @@ sensor:
name: Propane test distance
battery_level:
name: Propane test battery level
- platform: duty_time
id: duty_time1
name: Test Duty Time
restore: true
last_time:
name: Test Last Duty Time Sensor
sensor: ha_hello_world_binary
- platform: duty_time
id: duty_time2
name: Test Duty Time 2
restore: false
lambda: "return true;"

time:
- platform: homeassistant
on_time:
- at: "16:00:00"
then:
- logger.log: It's 16:00
- if:
condition:
- sensor.duty_time.is_running: duty_time2
then:
- sensor.duty_time.start: duty_time1
- if:
condition:
- sensor.duty_time.is_not_running: duty_time1
then:
- sensor.duty_time.stop: duty_time2
- sensor.duty_time.reset: duty_time1

esp32_touch:
setup_mode: true
Expand Down