mirror of
https://github.com/esphome/esphome.git
synced 2024-11-22 06:58:11 +01:00
Add absolute humidity component (#4519)
* Import Absolute Humidity component https://PigLab.ReaperLegion.net/home-automation/hass/esphome/custom-components/absolute-humidity * Fix terminology, add some docstrings * Switch from double to float https://github.com/esphome/esphome/pull/4519#pullrequestreview-1327615169 The additional precision doesn't matter in practice. * Address code review suggestions * Lint code
This commit is contained in:
parent
1b328da265
commit
b29cc58144
7 changed files with 323 additions and 0 deletions
|
@ -11,6 +11,7 @@ esphome/*.py @esphome/core
|
||||||
esphome/core/* @esphome/core
|
esphome/core/* @esphome/core
|
||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
|
esphome/components/absolute_humidity/* @DAVe3283
|
||||||
esphome/components/ac_dimmer/* @glmnet
|
esphome/components/ac_dimmer/* @glmnet
|
||||||
esphome/components/adc/* @esphome/core
|
esphome/components/adc/* @esphome/core
|
||||||
esphome/components/adc128s102/* @DeerMaximum
|
esphome/components/adc128s102/* @DeerMaximum
|
||||||
|
|
1
esphome/components/absolute_humidity/__init__.py
Normal file
1
esphome/components/absolute_humidity/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CODEOWNERS = ["@DAVe3283"]
|
182
esphome/components/absolute_humidity/absolute_humidity.cpp
Normal file
182
esphome/components/absolute_humidity/absolute_humidity.cpp
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "absolute_humidity.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace absolute_humidity {
|
||||||
|
|
||||||
|
static const char *const TAG = "absolute_humidity.sensor";
|
||||||
|
|
||||||
|
void AbsoluteHumidityComponent::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up absolute humidity '%s'...", this->get_name().c_str());
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||||
|
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||||
|
if (this->temperature_sensor_->has_state()) {
|
||||||
|
this->temperature_callback_(this->temperature_sensor_->get_state());
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
|
||||||
|
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
|
||||||
|
if (this->humidity_sensor_->has_state()) {
|
||||||
|
this->humidity_callback_(this->humidity_sensor_->get_state());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AbsoluteHumidityComponent::dump_config() {
|
||||||
|
LOG_SENSOR("", "Absolute Humidity", this);
|
||||||
|
|
||||||
|
switch (this->equation_) {
|
||||||
|
case BUCK:
|
||||||
|
ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Buck");
|
||||||
|
break;
|
||||||
|
case TETENS:
|
||||||
|
ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Tetens");
|
||||||
|
break;
|
||||||
|
case WOBUS:
|
||||||
|
ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Wobus");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, "Sources");
|
||||||
|
ESP_LOGCONFIG(TAG, " Temperature: '%s'", this->temperature_sensor_->get_name().c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Relative Humidity: '%s'", this->humidity_sensor_->get_name().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
void AbsoluteHumidityComponent::loop() {
|
||||||
|
if (!this->next_update_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->next_update_ = false;
|
||||||
|
|
||||||
|
// Ensure we have source data
|
||||||
|
const bool no_temperature = std::isnan(this->temperature_);
|
||||||
|
const bool no_humidity = std::isnan(this->humidity_);
|
||||||
|
if (no_temperature || no_humidity) {
|
||||||
|
if (no_temperature) {
|
||||||
|
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||||
|
}
|
||||||
|
if (no_humidity) {
|
||||||
|
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Unable to calculate absolute humidity.");
|
||||||
|
this->publish_state(NAN);
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to desired units
|
||||||
|
const float temperature_c = this->temperature_;
|
||||||
|
const float temperature_k = temperature_c + 273.15;
|
||||||
|
const float hr = this->humidity_ / 100;
|
||||||
|
|
||||||
|
// Calculate saturation vapor pressure
|
||||||
|
float es;
|
||||||
|
switch (this->equation_) {
|
||||||
|
case BUCK:
|
||||||
|
es = es_buck(temperature_c);
|
||||||
|
break;
|
||||||
|
case TETENS:
|
||||||
|
es = es_tetens(temperature_c);
|
||||||
|
break;
|
||||||
|
case WOBUS:
|
||||||
|
es = es_wobus(temperature_c);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!");
|
||||||
|
this->publish_state(NAN);
|
||||||
|
this->status_set_error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||||
|
|
||||||
|
// Calculate absolute humidity
|
||||||
|
const float absolute_humidity = vapor_density(es, hr, temperature_k);
|
||||||
|
|
||||||
|
// Publish absolute humidity
|
||||||
|
ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity);
|
||||||
|
this->status_clear_warning();
|
||||||
|
this->publish_state(absolute_humidity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buck equation (https://en.wikipedia.org/wiki/Arden_Buck_equation)
|
||||||
|
// More accurate than Tetens in normal meteorologic conditions
|
||||||
|
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||||
|
float a, b, c, d;
|
||||||
|
if (temperature_c >= 0) {
|
||||||
|
a = 0.61121;
|
||||||
|
b = 18.678;
|
||||||
|
c = 234.5;
|
||||||
|
d = 257.14;
|
||||||
|
} else {
|
||||||
|
a = 0.61115;
|
||||||
|
b = 18.678;
|
||||||
|
c = 233.7;
|
||||||
|
d = 279.82;
|
||||||
|
}
|
||||||
|
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
|
||||||
|
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
|
||||||
|
float a, b;
|
||||||
|
if (temperature_c >= 0) {
|
||||||
|
a = 17.27;
|
||||||
|
b = 237.3;
|
||||||
|
} else {
|
||||||
|
a = 21.875;
|
||||||
|
b = 265.5;
|
||||||
|
}
|
||||||
|
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wobus equation
|
||||||
|
// https://wahiduddin.net/calc/density_altitude.htm
|
||||||
|
// https://wahiduddin.net/calc/density_algorithms.htm
|
||||||
|
// Calculate the saturation vapor pressure (kPa)
|
||||||
|
float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||||
|
// THIS FUNCTION RETURNS THE SATURATION VAPOR PRESSURE ESW (MILLIBARS)
|
||||||
|
// OVER LIQUID WATER GIVEN THE TEMPERATURE T (CELSIUS). THE POLYNOMIAL
|
||||||
|
// APPROXIMATION BELOW IS DUE TO HERMAN WOBUS, A MATHEMATICIAN WHO
|
||||||
|
// WORKED AT THE NAVY WEATHER RESEARCH FACILITY, NORFOLK, VIRGINIA,
|
||||||
|
// BUT WHO IS NOW RETIRED. THE COEFFICIENTS OF THE POLYNOMIAL WERE
|
||||||
|
// CHOSEN TO FIT THE VALUES IN TABLE 94 ON PP. 351-353 OF THE SMITH-
|
||||||
|
// SONIAN METEOROLOGICAL TABLES BY ROLAND LIST (6TH EDITION). THE
|
||||||
|
// APPROXIMATION IS VALID FOR -50 < T < 100C.
|
||||||
|
//
|
||||||
|
// Baker, Schlatter 17-MAY-1982 Original version.
|
||||||
|
|
||||||
|
const float c0 = +0.99999683e00;
|
||||||
|
const float c1 = -0.90826951e-02;
|
||||||
|
const float c2 = +0.78736169e-04;
|
||||||
|
const float c3 = -0.61117958e-06;
|
||||||
|
const float c4 = +0.43884187e-08;
|
||||||
|
const float c5 = -0.29883885e-10;
|
||||||
|
const float c6 = +0.21874425e-12;
|
||||||
|
const float c7 = -0.17892321e-14;
|
||||||
|
const float c8 = +0.11112018e-16;
|
||||||
|
const float c9 = -0.30994571e-19;
|
||||||
|
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
|
||||||
|
return 0.61078 / pow(p, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||||
|
// H/T to https://esphome.io/cookbook/bme280_environment.html
|
||||||
|
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
||||||
|
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||||
|
// es = saturated vapor pressure (kPa)
|
||||||
|
// hr = relative humidity [0-1]
|
||||||
|
// ta = absolute temperature (K)
|
||||||
|
|
||||||
|
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
|
||||||
|
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
|
||||||
|
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
|
||||||
|
return (ea * mw) / (r * ta);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace absolute_humidity
|
||||||
|
} // namespace esphome
|
76
esphome/components/absolute_humidity/absolute_humidity.h
Normal file
76
esphome/components/absolute_humidity/absolute_humidity.h
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace absolute_humidity {
|
||||||
|
|
||||||
|
/// Enum listing all implemented saturation vapor pressure equations.
|
||||||
|
enum SaturationVaporPressureEquation {
|
||||||
|
BUCK,
|
||||||
|
TETENS,
|
||||||
|
WOBUS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||||
|
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||||
|
public:
|
||||||
|
AbsoluteHumidityComponent() = default;
|
||||||
|
|
||||||
|
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||||
|
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||||
|
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void temperature_callback_(float state) {
|
||||||
|
this->next_update_ = true;
|
||||||
|
this->temperature_ = state;
|
||||||
|
}
|
||||||
|
void humidity_callback_(float state) {
|
||||||
|
this->next_update_ = true;
|
||||||
|
this->humidity_ = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Buck equation for saturation vapor pressure in kPa.
|
||||||
|
*
|
||||||
|
* @param temperature_c Air temperature in °C.
|
||||||
|
*/
|
||||||
|
static float es_buck(float temperature_c);
|
||||||
|
/** Tetens equation for saturation vapor pressure in kPa.
|
||||||
|
*
|
||||||
|
* @param temperature_c Air temperature in °C.
|
||||||
|
*/
|
||||||
|
static float es_tetens(float temperature_c);
|
||||||
|
/** Wobus equation for saturation vapor pressure in kPa.
|
||||||
|
*
|
||||||
|
* @param temperature_c Air temperature in °C.
|
||||||
|
*/
|
||||||
|
static float es_wobus(float temperature_c);
|
||||||
|
|
||||||
|
/** Calculate vapor density (absolute humidity) in g/m³.
|
||||||
|
*
|
||||||
|
* @param es Saturation vapor pressure in kPa.
|
||||||
|
* @param hr Relative humidity 0 to 1.
|
||||||
|
* @param ta Absolute temperature in K.
|
||||||
|
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
|
||||||
|
*/
|
||||||
|
static float vapor_density(float es, float hr, float ta);
|
||||||
|
|
||||||
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
|
sensor::Sensor *humidity_sensor_{nullptr};
|
||||||
|
|
||||||
|
bool next_update_{false};
|
||||||
|
|
||||||
|
float temperature_{NAN};
|
||||||
|
float humidity_{NAN};
|
||||||
|
SaturationVaporPressureEquation equation_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace absolute_humidity
|
||||||
|
} // namespace esphome
|
56
esphome/components/absolute_humidity/sensor.py
Normal file
56
esphome/components/absolute_humidity/sensor.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import sensor
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_HUMIDITY,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
CONF_EQUATION,
|
||||||
|
ICON_WATER,
|
||||||
|
UNIT_GRAMS_PER_CUBIC_METER,
|
||||||
|
)
|
||||||
|
|
||||||
|
absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity")
|
||||||
|
AbsoluteHumidityComponent = absolute_humidity_ns.class_(
|
||||||
|
"AbsoluteHumidityComponent", sensor.Sensor, cg.Component
|
||||||
|
)
|
||||||
|
|
||||||
|
SaturationVaporPressureEquation = absolute_humidity_ns.enum(
|
||||||
|
"SaturationVaporPressureEquation"
|
||||||
|
)
|
||||||
|
EQUATION = {
|
||||||
|
"BUCK": SaturationVaporPressureEquation.BUCK,
|
||||||
|
"TETENS": SaturationVaporPressureEquation.TETENS,
|
||||||
|
"WOBUS": SaturationVaporPressureEquation.WOBUS,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER,
|
||||||
|
icon=ICON_WATER,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(AbsoluteHumidityComponent),
|
||||||
|
cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
|
||||||
|
cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
|
||||||
|
cv.Optional(CONF_EQUATION, default="WOBUS"): cv.enum(EQUATION, upper=True),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await sensor.new_sensor(config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE])
|
||||||
|
cg.add(var.set_temperature_sensor(temperature_sensor))
|
||||||
|
|
||||||
|
humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY])
|
||||||
|
cg.add(var.set_humidity_sensor(humidity_sensor))
|
||||||
|
|
||||||
|
cg.add(var.set_equation(config[CONF_EQUATION]))
|
|
@ -214,6 +214,7 @@ CONF_ENERGY = "energy"
|
||||||
CONF_ENTITY_CATEGORY = "entity_category"
|
CONF_ENTITY_CATEGORY = "entity_category"
|
||||||
CONF_ENTITY_ID = "entity_id"
|
CONF_ENTITY_ID = "entity_id"
|
||||||
CONF_ENUM_DATAPOINT = "enum_datapoint"
|
CONF_ENUM_DATAPOINT = "enum_datapoint"
|
||||||
|
CONF_EQUATION = "equation"
|
||||||
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
|
CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support"
|
||||||
CONF_ESPHOME = "esphome"
|
CONF_ESPHOME = "esphome"
|
||||||
CONF_ETHERNET = "ethernet"
|
CONF_ETHERNET = "ethernet"
|
||||||
|
@ -860,6 +861,7 @@ ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal"
|
||||||
ICON_THERMOMETER = "mdi:thermometer"
|
ICON_THERMOMETER = "mdi:thermometer"
|
||||||
ICON_TIMELAPSE = "mdi:timelapse"
|
ICON_TIMELAPSE = "mdi:timelapse"
|
||||||
ICON_TIMER = "mdi:timer-outline"
|
ICON_TIMER = "mdi:timer-outline"
|
||||||
|
ICON_WATER = "mdi:water"
|
||||||
ICON_WATER_PERCENT = "mdi:water-percent"
|
ICON_WATER_PERCENT = "mdi:water-percent"
|
||||||
ICON_WEATHER_SUNSET = "mdi:weather-sunset"
|
ICON_WEATHER_SUNSET = "mdi:weather-sunset"
|
||||||
ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down"
|
ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down"
|
||||||
|
@ -881,6 +883,7 @@ UNIT_DEGREE_PER_SECOND = "°/s"
|
||||||
UNIT_DEGREES = "°"
|
UNIT_DEGREES = "°"
|
||||||
UNIT_EMPTY = ""
|
UNIT_EMPTY = ""
|
||||||
UNIT_G = "G"
|
UNIT_G = "G"
|
||||||
|
UNIT_GRAMS_PER_CUBIC_METER = "g/m³"
|
||||||
UNIT_HECTOPASCAL = "hPa"
|
UNIT_HECTOPASCAL = "hPa"
|
||||||
UNIT_HERTZ = "Hz"
|
UNIT_HERTZ = "Hz"
|
||||||
UNIT_HOUR = "h"
|
UNIT_HOUR = "h"
|
||||||
|
|
|
@ -1229,6 +1229,10 @@ sensor:
|
||||||
model: 1005
|
model: 1005
|
||||||
update_interval: 60s
|
update_interval: 60s
|
||||||
i2c_id: i2c_bus
|
i2c_id: i2c_bus
|
||||||
|
- platform: absolute_humidity
|
||||||
|
name: DHT Absolute Humidity
|
||||||
|
temperature: dht_temperature
|
||||||
|
humidity: dht_humidity
|
||||||
|
|
||||||
esp32_touch:
|
esp32_touch:
|
||||||
setup_mode: false
|
setup_mode: false
|
||||||
|
|
Loading…
Reference in a new issue