#include "esphome/core/log.h" #include "absolute_humidity.h" namespace esphome::absolute_humidity { static const char *const TAG{"absolute_humidity.sensor"}; void AbsoluteHumidityComponent::setup() { this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_ = state; this->enable_loop(); }); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); // Get initial value if (this->temperature_sensor_->has_state()) { this->temperature_ = this->temperature_sensor_->get_state(); } this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_ = state; this->enable_loop(); }); ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); // Get initial value if (this->humidity_sensor_->has_state()) { this->humidity_ = 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\n" " Temperature: '%s'\n" " Relative Humidity: '%s'", this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); } void AbsoluteHumidityComponent::loop() { // Only run once this->disable_loop(); // 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 humidity sensor!"); } this->publish_state(NAN); this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); return; } // Convert to desired units const float temperature_c{this->temperature_}; const float temperature_k{temperature_c + 273.15f}; const float hr{this->humidity_ / 100.0f}; // 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: this->publish_state(NAN); this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } // Calculate absolute humidity const float absolute_humidity{vapor_density(es, hr, temperature_k)}; ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish 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.0f) { a = 0.61121f; b = 18.678f; c = 234.5f; d = 257.14f; } else { a = 0.61115f; b = 18.678f; c = 233.7f; d = 279.82f; } 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.0f) { a = 17.27f; b = 237.3f; } else { a = 21.875f; b = 265.5f; } return 0.61078f * 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. constexpr float c0{+0.99999683e+00f}; constexpr float c1{-0.90826951e-02f}; constexpr float c2{+0.78736169e-04f}; constexpr float c3{-0.61117958e-06f}; constexpr float c4{+0.43884187e-08f}; constexpr float c5{-0.29883885e-10f}; constexpr float c6{+0.21874425e-12f}; constexpr float c7{-0.17892321e-14f}; constexpr float c8{+0.11112018e-16f}; constexpr float c9{-0.30994571e-19f}; const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))}; return 0.61078f / powf(p, 8.0f); } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ // H/T to https://esphome.io/cookbook/bme280_environment/ // 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.0f}; // vapor pressure of the air (Pa) const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹) const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹) return (ea * mw) / (r * ta); } } // namespace esphome::absolute_humidity