1 """Calculates mold growth indication from temperature and humidity."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
8 from typing
import TYPE_CHECKING, Any
10 import voluptuous
as vol
12 from homeassistant
import util
14 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
21 ATTR_UNIT_OF_MEASUREMENT,
32 EventStateChangedData,
46 CONF_CALIBRATION_FACTOR,
53 _LOGGER = logging.getLogger(__name__)
55 ATTR_CRITICAL_TEMP =
"estimated_critical_temp"
56 ATTR_DEWPOINT =
"dewpoint"
62 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
64 vol.Required(CONF_INDOOR_TEMP): cv.entity_id,
65 vol.Required(CONF_OUTDOOR_TEMP): cv.entity_id,
66 vol.Required(CONF_INDOOR_HUMIDITY): cv.entity_id,
67 vol.Optional(CONF_CALIBRATION_FACTOR): vol.Coerce(float),
68 vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
69 vol.Optional(CONF_UNIQUE_ID): cv.string,
77 async_add_entities: AddEntitiesCallback,
78 discovery_info: DiscoveryInfoType |
None =
None,
80 """Set up MoldIndicator sensor."""
81 name: str = config.get(CONF_NAME, DEFAULT_NAME)
82 indoor_temp_sensor: str = config[CONF_INDOOR_TEMP]
83 outdoor_temp_sensor: str = config[CONF_OUTDOOR_TEMP]
84 indoor_humidity_sensor: str = config[CONF_INDOOR_HUMIDITY]
85 calib_factor: float = config[CONF_CALIBRATION_FACTOR]
86 unique_id: str |
None = config.get(CONF_UNIQUE_ID)
93 hass.config.units
is METRIC_SYSTEM,
96 indoor_humidity_sensor,
108 async_add_entities: AddEntitiesCallback,
110 """Set up the Mold indicator sensor entry."""
111 name: str = entry.options[CONF_NAME]
112 indoor_temp_sensor: str = entry.options[CONF_INDOOR_TEMP]
113 outdoor_temp_sensor: str = entry.options[CONF_OUTDOOR_TEMP]
114 indoor_humidity_sensor: str = entry.options[CONF_INDOOR_HUMIDITY]
115 calib_factor: float = entry.options[CONF_CALIBRATION_FACTOR]
122 hass.config.units
is METRIC_SYSTEM,
125 indoor_humidity_sensor,
135 """Represents a MoldIndication sensor."""
137 _attr_should_poll =
False
138 _attr_native_unit_of_measurement = PERCENTAGE
139 _attr_device_class = SensorDeviceClass.HUMIDITY
140 _attr_state_class = SensorStateClass.MEASUREMENT
147 indoor_temp_sensor: str,
148 outdoor_temp_sensor: str,
149 indoor_humidity_sensor: str,
151 unique_id: str |
None,
153 """Initialize the sensor."""
164 indoor_humidity_sensor,
167 self.
_dewpoint_dewpoint: float |
None =
None
171 self.
_crit_temp_crit_temp: float |
None =
None
172 if indoor_humidity_sensor:
175 indoor_humidity_sensor,
177 self.
_preview_callback_preview_callback: Callable[[str, Mapping[str, Any]],
None] |
None =
None
182 preview_callback: Callable[[str, Mapping[str, Any]],
None],
184 """Render a preview."""
194 preview_callback(calculated_state.state, calculated_state.attributes)
203 """Run when entity about to be added to hass."""
208 """Set up the sensor and start tracking state changes."""
211 def mold_indicator_sensors_state_listener(
212 event: Event[EventStateChangedData],
214 """Handle for state changes for dependent sensors."""
215 new_state = event.data[
"new_state"]
216 old_state = event.data[
"old_state"]
217 entity = event.data[
"entity_id"]
219 "Sensor state change for %s that had old state %s and new state %s",
225 if self.
_update_sensor_update_sensor(entity, old_state, new_state):
229 calculated_state.state, calculated_state.attributes
236 def mold_indicator_startup() -> None:
237 """Add listeners and get 1st state."""
238 _LOGGER.debug(
"Startup for %s", self.
entity_identity_id)
241 self.
hasshass,
list(self.
_entities_entities), mold_indicator_sensors_state_listener
281 calculated_state.state, calculated_state.attributes
284 mold_indicator_startup()
287 self, entity: str, old_state: State |
None, new_state: State |
None
289 """Update information based on new sensor states."""
290 _LOGGER.debug(
"Sensor update for %s", entity)
291 if new_state
is None:
296 if old_state
is None and new_state.state == STATE_UNKNOWN:
310 """Parse temperature sensor value."""
311 _LOGGER.debug(
"Updating temp sensor with value %s", state.state)
314 if state.state
in (STATE_UNKNOWN, STATE_UNAVAILABLE):
316 "Unable to parse temperature sensor %s with state: %s",
322 if (temp := util.convert(state.state, float))
is None:
324 "Unable to parse temperature sensor %s with state: %s",
332 unit := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
333 )
in UnitOfTemperature:
334 return TemperatureConverter.convert(temp, unit, UnitOfTemperature.CELSIUS)
336 "Temp sensor %s has unsupported unit: %s (allowed: %s, %s)",
339 UnitOfTemperature.CELSIUS,
340 UnitOfTemperature.FAHRENHEIT,
347 """Parse humidity sensor value."""
348 _LOGGER.debug(
"Updating humidity sensor with value %s", state.state)
351 if state.state
in (STATE_UNKNOWN, STATE_UNAVAILABLE):
353 "Unable to parse humidity sensor %s, state: %s",
359 if (hum := util.convert(state.state, float))
is None:
361 "Unable to parse humidity sensor %s, state: %s",
367 if (unit := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) != PERCENTAGE:
369 "Humidity sensor %s has unsupported unit: %s (allowed: %s)",
376 if hum > 100
or hum < 0:
378 "Humidity sensor %s is out of range: %s (allowed: 0-100)",
387 """Calculate latest state."""
388 _LOGGER.debug(
"Update state for %s", self.
entity_identity_id)
407 """Calculate the dewpoint for the indoor air."""
412 beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self.
_indoor_temp_indoor_temp)
419 * (alpha + math.log(self.
_indoor_hum_indoor_hum / 100.0))
420 / (beta - math.log(self.
_indoor_hum_indoor_hum / 100.0))
422 _LOGGER.debug(
"Dewpoint: %f %s", self.
_dewpoint_dewpoint, UnitOfTemperature.CELSIUS)
425 """Calculate the humidity at the (cold) calibration point."""
431 "Invalid inputs - dewpoint: %s, calibration-factor: %s",
447 "Estimated Critical Temperature: %f %s",
449 UnitOfTemperature.CELSIUS,
454 beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self.
_crit_temp_crit_temp)
458 (self.
_dewpoint_dewpoint * beta - MAGNUS_K3 * alpha)
465 if crit_humidity > 100:
467 elif crit_humidity < 0:
472 _LOGGER.debug(
"Mold indicator humidity: %s", self.
native_valuenative_value)
476 """Return the state attributes."""
478 convert_to = UnitOfTemperature.CELSIUS
480 convert_to = UnitOfTemperature.FAHRENHEIT
483 TemperatureConverter.convert(
484 self.
_dewpoint_dewpoint, UnitOfTemperature.CELSIUS, convert_to
491 TemperatureConverter.convert(
492 self.
_crit_temp_crit_temp, UnitOfTemperature.CELSIUS, convert_to
499 ATTR_DEWPOINT: round(dewpoint, 2)
if dewpoint
else None,
500 ATTR_CRITICAL_TEMP: round(crit_temp, 2)
if crit_temp
else None,
float|None _update_hum_sensor(State state)
bool _update_sensor(self, str entity, State|None old_state, State|None new_state)
None _async_setup_sensor(self)
CALLBACK_TYPE async_start_preview(self, Callable[[str, Mapping[str, Any]], None] preview_callback)
float|None _update_temp_sensor(State state)
dict[str, Any] extra_state_attributes(self)
None _calc_moldindicator(self)
None _calc_dewpoint(self)
None async_added_to_hass(self)
None __init__(self, HomeAssistant hass, str name, bool is_metric, str indoor_temp_sensor, str outdoor_temp_sensor, str indoor_humidity_sensor, float calib_factor, str|None unique_id)
StateType|date|datetime|Decimal native_value(self)
None async_schedule_update_ha_state(self, bool force_refresh=False)
CalculatedState _async_calculate_state(self)
None _call_on_remove_callbacks(self)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
dr.DeviceInfo|None async_device_info_to_link_from_entity(HomeAssistant hass, str entity_id_or_uuid)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)