mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	New 'Duty Time' sensor component (#5069)
This commit is contained in:
		| @@ -78,6 +78,7 @@ esphome/components/display_menu_base/* @numo68 | |||||||
| esphome/components/dps310/* @kbx81 | esphome/components/dps310/* @kbx81 | ||||||
| esphome/components/ds1307/* @badbadc0ffee | esphome/components/ds1307/* @badbadc0ffee | ||||||
| esphome/components/dsmr/* @glmnet @zuidwijk | esphome/components/dsmr/* @glmnet @zuidwijk | ||||||
|  | esphome/components/duty_time/* @dudanov | ||||||
| esphome/components/ee895/* @Stock-M | esphome/components/ee895/* @Stock-M | ||||||
| esphome/components/ektf2232/* @jesserockz | esphome/components/ektf2232/* @jesserockz | ||||||
| esphome/components/ens210/* @itn3rd77 | esphome/components/ens210/* @itn3rd77 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/duty_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/duty_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@dudanov"] | ||||||
							
								
								
									
										103
									
								
								esphome/components/duty_time/duty_time_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								esphome/components/duty_time/duty_time_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<uint32_t>(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 | ||||||
							
								
								
									
										88
									
								
								esphome/components/duty_time/duty_time_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/duty_time/duty_time_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<bool()> &&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<bool()> 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<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
									
								
								esphome/components/duty_time/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/duty_time/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
| @@ -416,6 +416,18 @@ sensor: | |||||||
|       name: Propane test distance |       name: Propane test distance | ||||||
|     battery_level: |     battery_level: | ||||||
|       name: Propane test 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: | time: | ||||||
|   - platform: homeassistant |   - platform: homeassistant | ||||||
| @@ -423,6 +435,17 @@ time: | |||||||
|       - at: "16:00:00" |       - at: "16:00:00" | ||||||
|         then: |         then: | ||||||
|           - logger.log: It's 16:00 |           - 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: | esp32_touch: | ||||||
|   setup_mode: true |   setup_mode: true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user