mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Implement pulse_meter as an improvement on pulse_counter and pulse_width for meters (#1434)
This commit is contained in:
		
							
								
								
									
										0
									
								
								esphome/components/pulse_meter/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/pulse_meter/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										87
									
								
								esphome/components/pulse_meter/pulse_meter_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								esphome/components/pulse_meter/pulse_meter_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| #include "pulse_meter_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace pulse_meter { | ||||
|  | ||||
| static const char *TAG = "pulse_meter"; | ||||
|  | ||||
| void PulseMeterSensor::setup() { | ||||
|   this->pin_->setup(); | ||||
|   this->isr_pin_ = pin_->to_isr(); | ||||
|   this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, CHANGE); | ||||
|  | ||||
|   this->last_detected_edge_us_ = 0; | ||||
|   this->last_valid_edge_us_ = 0; | ||||
| } | ||||
|  | ||||
| void PulseMeterSensor::loop() { | ||||
|   const uint32_t now = micros(); | ||||
|  | ||||
|   // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until | ||||
|   // we get at least two valid pulses. | ||||
|   const uint32_t time_since_valid_edge_us = now - this->last_valid_edge_us_; | ||||
|   if ((this->last_valid_edge_us_ != 0) && (time_since_valid_edge_us > this->timeout_us_)) { | ||||
|     ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); | ||||
|     this->last_detected_edge_us_ = 0; | ||||
|     this->last_valid_edge_us_ = 0; | ||||
|     this->pulse_width_us_ = 0; | ||||
|   } | ||||
|  | ||||
|   // We quantize our pulse widths to 1 ms to avoid unnecessary jitter | ||||
|   const uint32_t pulse_width_ms = this->pulse_width_us_ / 1000; | ||||
|   if (this->pulse_width_dedupe_.next(pulse_width_ms)) { | ||||
|     if (pulse_width_ms == 0) { | ||||
|       // Treat 0 pulse width as 0 pulses/min (normally because we've not detected any pulses for a while) | ||||
|       this->publish_state(0); | ||||
|     } else { | ||||
|       // Calculate pulses/min from the pulse width in ms | ||||
|       this->publish_state((60.0 * 1000.0) / pulse_width_ms); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->total_sensor_ != nullptr) { | ||||
|     const uint32_t total = this->total_pulses_; | ||||
|     if (this->total_dedupe_.next(total)) { | ||||
|       this->total_sensor_->publish_state(total); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void PulseMeterSensor::dump_config() { | ||||
|   LOG_SENSOR("", "Pulse Meter", this); | ||||
|   LOG_PIN("  Pin: ", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Filtering pulses shorter than %u µs", this->filter_us_); | ||||
|   ESP_LOGCONFIG(TAG, "  Assuming 0 pulses/min after not receiving a pulse for %us", this->timeout_us_ / 1000000); | ||||
| } | ||||
|  | ||||
| void ICACHE_RAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { | ||||
|   // This is an interrupt handler - we can't call any virtual method from this method | ||||
|  | ||||
|   // Get the current time before we do anything else so the measurements are consistent | ||||
|   const uint32_t now = micros(); | ||||
|  | ||||
|   // We only look at rising edges | ||||
|   if (!sensor->isr_pin_->digital_read()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Ignore the first detected pulse (we need at least two pulses to measure the width) | ||||
|   if (sensor->last_detected_edge_us_ != 0) { | ||||
|     // Check to see if we should filter this edge out | ||||
|     if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) { | ||||
|       // Don't measure the first valid pulse (we need at least two pulses to measure the width) | ||||
|       if (sensor->last_valid_edge_us_ != 0) { | ||||
|         sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_); | ||||
|       } | ||||
|  | ||||
|       sensor->total_pulses_++; | ||||
|       sensor->last_valid_edge_us_ = now; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   sensor->last_detected_edge_us_ = now; | ||||
| } | ||||
|  | ||||
| }  // namespace pulse_meter | ||||
| }  // namespace esphome | ||||
							
								
								
									
										42
									
								
								esphome/components/pulse_meter/pulse_meter_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/pulse_meter/pulse_meter_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/esphal.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace pulse_meter { | ||||
|  | ||||
| class PulseMeterSensor : public sensor::Sensor, public Component { | ||||
|  public: | ||||
|   void set_pin(GPIOPin *pin) { this->pin_ = pin; } | ||||
|   void set_filter_us(uint32_t filter) { this->filter_us_ = filter; } | ||||
|   void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; } | ||||
|   void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   static void gpio_intr(PulseMeterSensor *sensor); | ||||
|  | ||||
|   GPIOPin *pin_ = nullptr; | ||||
|   ISRInternalGPIOPin *isr_pin_; | ||||
|   uint32_t filter_us_ = 0; | ||||
|   uint32_t timeout_us_ = 1000000UL * 60UL * 5UL; | ||||
|   sensor::Sensor *total_sensor_ = nullptr; | ||||
|  | ||||
|   Deduplicator<uint32_t> pulse_width_dedupe_; | ||||
|   Deduplicator<uint32_t> total_dedupe_; | ||||
|  | ||||
|   volatile uint32_t last_detected_edge_us_ = 0; | ||||
|   volatile uint32_t last_valid_edge_us_ = 0; | ||||
|   volatile uint32_t pulse_width_us_ = 0; | ||||
|   volatile uint32_t total_pulses_ = 0; | ||||
| }; | ||||
|  | ||||
| }  // namespace pulse_meter | ||||
| }  // namespace esphome | ||||
							
								
								
									
										58
									
								
								esphome/components/pulse_meter/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/pulse_meter/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import sensor | ||||
| from esphome.const import CONF_ID, CONF_INTERNAL_FILTER, \ | ||||
|     CONF_PIN, CONF_NUMBER, CONF_TIMEOUT, CONF_TOTAL, \ | ||||
|     ICON_PULSE, UNIT_PULSES, UNIT_PULSES_PER_MINUTE | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ['@stevebaxter'] | ||||
|  | ||||
| pulse_meter_ns = cg.esphome_ns.namespace('pulse_meter') | ||||
|  | ||||
| PulseMeterSensor = pulse_meter_ns.class_('PulseMeterSensor', | ||||
|                                          sensor.Sensor, | ||||
|                                          cg.Component) | ||||
|  | ||||
|  | ||||
| def validate_internal_filter(value): | ||||
|     return cv.positive_time_period_microseconds(value) | ||||
|  | ||||
|  | ||||
| def validate_timeout(value): | ||||
|     value = cv.positive_time_period_microseconds(value) | ||||
|     if value.total_minutes > 70: | ||||
|         raise cv.Invalid("Maximum timeout is 70 minutes") | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def validate_pulse_meter_pin(value): | ||||
|     value = pins.internal_gpio_input_pin_schema(value) | ||||
|     if CORE.is_esp8266 and value[CONF_NUMBER] >= 16: | ||||
|         raise cv.Invalid("Pins GPIO16 and GPIO17 cannot be used as pulse counters on ESP8266.") | ||||
|     return value | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).extend({ | ||||
|     cv.GenerateID(): cv.declare_id(PulseMeterSensor), | ||||
|     cv.Required(CONF_PIN): validate_pulse_meter_pin, | ||||
|     cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, | ||||
|     cv.Optional(CONF_TIMEOUT, default='5min'): validate_timeout, | ||||
|     cv.Optional(CONF_TOTAL): sensor.sensor_schema(UNIT_PULSES, ICON_PULSE, 0) | ||||
| }) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield cg.register_component(var, config) | ||||
|     yield sensor.register_sensor(var, config) | ||||
|  | ||||
|     pin = yield cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     cg.add(var.set_pin(pin)) | ||||
|     cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER])) | ||||
|     cg.add(var.set_timeout_us(config[CONF_TIMEOUT])) | ||||
|  | ||||
|     if CONF_TOTAL in config: | ||||
|         sens = yield sensor.new_sensor(config[CONF_TOTAL]) | ||||
|         cg.add(var.set_total_sensor(sens)) | ||||
		Reference in New Issue
	
	Block a user