diff --git a/CODEOWNERS b/CODEOWNERS index cce94d4ccb..122dc71b48 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,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 diff --git a/esphome/components/duty_time/__init__.py b/esphome/components/duty_time/__init__.py new file mode 100644 index 0000000000..b708cee80b --- /dev/null +++ b/esphome/components/duty_time/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@dudanov"] diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp new file mode 100644 index 0000000000..045cbcceac --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -0,0 +1,103 @@ +#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(binary_sensor::BinarySensor *const sensor) { + sensor->add_on_state_callback([this](bool state) { this->process_state_(state); }); +} + +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(this->get_object_id_hash()); + this->pref_.load(&seconds); + } + + this->set_value_(seconds); +} + +void DutyTimeSensor::set_value_(const uint32_t sec) { + this->last_time_ = 0; + if (this->last_state_) + this->last_time_ = millis(); // last time with 0 ms correction + 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 + const uint32_t tm = now - this->last_time_; + const uint32_t ms = tm % 1000; + + this->publish_and_save_(this->total_sec_ + tm / 1000, ms); + this->last_time_ = now - ms; // store time with ms correction + + if (!state) { + // falling edge + this->last_time_ = ms; // temporary store ms correction only + this->last_state_ = false; + + if (this->last_duty_time_sensor_ != nullptr) { + const uint32_t turn_on_ms = now - this->edge_time_; + this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f); + } + } + + } else if (state) { + // rising edge + this->last_time_ = now - this->last_time_; // store time with ms correction + this->edge_time_ = now; // store turn-on start time + this->last_state_ = true; + } +} + +void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) { + this->total_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_)); + LOG_SENSOR(" ", "Duty Time Sensor:", this); + LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_); +} + +} // namespace duty_time_sensor +} // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h new file mode 100644 index 0000000000..27fa383847 --- /dev/null +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -0,0 +1,88 @@ +#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 &&func) { this->func_ = func; } + void set_sensor(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 func_{nullptr}; + sensor::Sensor *last_duty_time_sensor_{nullptr}; + ESPPreferenceObject pref_; + + uint32_t total_sec_; + uint32_t last_time_; + uint32_t edge_time_; + bool last_state_{false}; + bool restore_; +}; + +template class StartAction : public Action { + public: + explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->start(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class StopAction : public Action { + public: + explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->stop(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class ResetAction : public Action { + public: + explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset(); } + + protected: + DutyTimeSensor *parent_; +}; + +template class RunningCondition : public Condition { + 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 diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py new file mode 100644 index 0000000000..5f8582d481 --- /dev/null +++ b/esphome/components/duty_time/sensor.py @@ -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) diff --git a/tests/test2.yaml b/tests/test2.yaml index 31fd840f5c..291dc240dc 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -416,6 +416,18 @@ 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 @@ -423,6 +435,17 @@ 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