mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
Moldindicator Sensor (#1575)
* Adds MoldIndicator sensor platform This sensor may be used to get an indication for possible mold growth in rooms. It calculates the humidity at a pre-calibrated indoor point (wall, window). * Automatic conversion to Fahrenheit for mold_indicator * Minor change to critical temp label * Fixed docstrings and styles * Minor changes to MoldIndicator implementation * Added first (non-working) implementation for mold_indicator test * Small style changes * Minor improvements to mold_indicator * Completed unit test for mold indicator * Fix to moldindicator initialization * Adds missing period. Now that really matters.. * Adds test for sensor_changed function
This commit is contained in:
parent
7f0b8c5e70
commit
eaebe83429
268
homeassistant/components/sensor/mold_indicator.py
Normal file
268
homeassistant/components/sensor/mold_indicator.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
"""
|
||||||
|
Calculates mold growth indication from temperature and humidity.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.mold_indicator/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
import homeassistant.util as util
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.event import track_state_change
|
||||||
|
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = "Mold Indicator"
|
||||||
|
CONF_INDOOR_TEMP = "indoor_temp_sensor"
|
||||||
|
CONF_OUTDOOR_TEMP = "outdoor_temp_sensor"
|
||||||
|
CONF_INDOOR_HUMIDITY = "indoor_humidity_sensor"
|
||||||
|
CONF_CALIBRATION_FACTOR = "calibration_factor"
|
||||||
|
|
||||||
|
MAGNUS_K2 = 17.62
|
||||||
|
MAGNUS_K3 = 243.12
|
||||||
|
|
||||||
|
ATTR_DEWPOINT = "Dewpoint"
|
||||||
|
ATTR_CRITICAL_TEMP = "Est. Crit. Temp"
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
"""Setup MoldIndicator sensor."""
|
||||||
|
name = config.get('name', DEFAULT_NAME)
|
||||||
|
indoor_temp_sensor = config.get(CONF_INDOOR_TEMP)
|
||||||
|
outdoor_temp_sensor = config.get(CONF_OUTDOOR_TEMP)
|
||||||
|
indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY)
|
||||||
|
calib_factor = util.convert(config.get(CONF_CALIBRATION_FACTOR),
|
||||||
|
float, None)
|
||||||
|
|
||||||
|
if None in (indoor_temp_sensor,
|
||||||
|
outdoor_temp_sensor, indoor_humidity_sensor):
|
||||||
|
_LOGGER.error('Missing required key %s, %s or %s',
|
||||||
|
CONF_INDOOR_TEMP, CONF_OUTDOOR_TEMP,
|
||||||
|
CONF_INDOOR_HUMIDITY)
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices_callback([MoldIndicator(
|
||||||
|
hass, name, indoor_temp_sensor,
|
||||||
|
outdoor_temp_sensor, indoor_humidity_sensor,
|
||||||
|
calib_factor)])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class MoldIndicator(Entity):
|
||||||
|
"""Represents a MoldIndication sensor."""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(self, hass, name, indoor_temp_sensor, outdoor_temp_sensor,
|
||||||
|
indoor_humidity_sensor, calib_factor):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._state = None
|
||||||
|
self._name = name
|
||||||
|
self._indoor_temp_sensor = indoor_temp_sensor
|
||||||
|
self._indoor_humidity_sensor = indoor_humidity_sensor
|
||||||
|
self._outdoor_temp_sensor = outdoor_temp_sensor
|
||||||
|
self._calib_factor = calib_factor
|
||||||
|
self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
|
||||||
|
|
||||||
|
self._dewpoint = None
|
||||||
|
self._indoor_temp = None
|
||||||
|
self._outdoor_temp = None
|
||||||
|
self._indoor_hum = None
|
||||||
|
self._crit_temp = None
|
||||||
|
|
||||||
|
track_state_change(hass, indoor_temp_sensor, self._sensor_changed)
|
||||||
|
track_state_change(hass, outdoor_temp_sensor, self._sensor_changed)
|
||||||
|
track_state_change(hass, indoor_humidity_sensor, self._sensor_changed)
|
||||||
|
|
||||||
|
# Read initial state
|
||||||
|
indoor_temp = hass.states.get(indoor_temp_sensor)
|
||||||
|
outdoor_temp = hass.states.get(outdoor_temp_sensor)
|
||||||
|
indoor_hum = hass.states.get(indoor_humidity_sensor)
|
||||||
|
|
||||||
|
if indoor_temp:
|
||||||
|
self._indoor_temp = \
|
||||||
|
MoldIndicator._update_temp_sensor(indoor_temp)
|
||||||
|
|
||||||
|
if outdoor_temp:
|
||||||
|
self._outdoor_temp = \
|
||||||
|
MoldIndicator._update_temp_sensor(outdoor_temp)
|
||||||
|
|
||||||
|
if indoor_hum:
|
||||||
|
self._indoor_hum = \
|
||||||
|
MoldIndicator._update_hum_sensor(indoor_hum)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_temp_sensor(state):
|
||||||
|
"""Parse temperature sensor value."""
|
||||||
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
temp = util.convert(state.state, float)
|
||||||
|
|
||||||
|
if temp is None:
|
||||||
|
_LOGGER.error('Unable to parse sensor temperature: %s',
|
||||||
|
state.state)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# convert to celsius if necessary
|
||||||
|
if unit == TEMP_FAHRENHEIT:
|
||||||
|
return util.temperature.fahrenheit_to_celcius(temp)
|
||||||
|
elif unit == TEMP_CELSIUS:
|
||||||
|
return temp
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Temp sensor has unsupported unit: %s"
|
||||||
|
" (allowed: %s, %s)",
|
||||||
|
unit, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_hum_sensor(state):
|
||||||
|
"""Parse humidity sensor value."""
|
||||||
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
hum = util.convert(state.state, float)
|
||||||
|
|
||||||
|
if hum is None:
|
||||||
|
_LOGGER.error('Unable to parse sensor humidity: %s',
|
||||||
|
state.state)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# check unit
|
||||||
|
if unit != "%":
|
||||||
|
_LOGGER.error(
|
||||||
|
"Humidity sensor has unsupported unit: %s %s",
|
||||||
|
unit,
|
||||||
|
" (allowed: %)")
|
||||||
|
|
||||||
|
# check range
|
||||||
|
if hum > 100 or hum < 0:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Humidity sensor out of range: %s %s",
|
||||||
|
hum,
|
||||||
|
" (allowed: 0-100%)")
|
||||||
|
|
||||||
|
return hum
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Calculate latest state."""
|
||||||
|
# check all sensors
|
||||||
|
if None in (self._indoor_temp, self._indoor_hum, self._outdoor_temp):
|
||||||
|
return
|
||||||
|
|
||||||
|
# re-calculate dewpoint and mold indicator
|
||||||
|
self._calc_dewpoint()
|
||||||
|
self._calc_moldindicator()
|
||||||
|
|
||||||
|
def _sensor_changed(self, entity_id, old_state, new_state):
|
||||||
|
"""Called when sensor values change."""
|
||||||
|
if new_state is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if entity_id == self._indoor_temp_sensor:
|
||||||
|
# update the indoor temp sensor
|
||||||
|
self._indoor_temp = MoldIndicator._update_temp_sensor(new_state)
|
||||||
|
|
||||||
|
elif entity_id == self._outdoor_temp_sensor:
|
||||||
|
# update outdoor temp sensor
|
||||||
|
self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state)
|
||||||
|
|
||||||
|
elif entity_id == self._indoor_humidity_sensor:
|
||||||
|
# update humidity
|
||||||
|
self._indoor_hum = MoldIndicator._update_hum_sensor(new_state)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def _calc_dewpoint(self):
|
||||||
|
"""Calculate the dewpoint for the indoor air."""
|
||||||
|
# use magnus approximation to calculate the dew point
|
||||||
|
alpha = MAGNUS_K2 * self._indoor_temp / (MAGNUS_K3 + self._indoor_temp)
|
||||||
|
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._indoor_temp)
|
||||||
|
|
||||||
|
if self._indoor_hum == 0:
|
||||||
|
self._dewpoint = -50 # not defined, assume very low value
|
||||||
|
else:
|
||||||
|
self._dewpoint = \
|
||||||
|
MAGNUS_K3 * (alpha + math.log(self._indoor_hum / 100.0)) / \
|
||||||
|
(beta - math.log(self._indoor_hum / 100.0))
|
||||||
|
_LOGGER.debug("Dewpoint: %f " + TEMP_CELSIUS, self._dewpoint)
|
||||||
|
|
||||||
|
def _calc_moldindicator(self):
|
||||||
|
"""Calculate the humidity at the (cold) calibration point."""
|
||||||
|
if None in (self._dewpoint, self._calib_factor) or \
|
||||||
|
self._calib_factor == 0:
|
||||||
|
|
||||||
|
_LOGGER.debug("Invalid inputs - dewpoint: %s,"
|
||||||
|
" calibration-factor: %s",
|
||||||
|
self._dewpoint, self._calib_factor)
|
||||||
|
self._state = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# first calculate the approximate temperature at the calibration point
|
||||||
|
self._crit_temp = \
|
||||||
|
self._outdoor_temp + (self._indoor_temp - self._outdoor_temp) / \
|
||||||
|
self._calib_factor
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Estimated Critical Temperature: %f " +
|
||||||
|
TEMP_CELSIUS, self._crit_temp)
|
||||||
|
|
||||||
|
# Then calculate the humidity at this point
|
||||||
|
alpha = MAGNUS_K2 * self._crit_temp / (MAGNUS_K3 + self._crit_temp)
|
||||||
|
beta = MAGNUS_K2 * MAGNUS_K3 / (MAGNUS_K3 + self._crit_temp)
|
||||||
|
|
||||||
|
crit_humidity = \
|
||||||
|
math.exp(
|
||||||
|
(self._dewpoint * beta - MAGNUS_K3 * alpha) /
|
||||||
|
(self._dewpoint + MAGNUS_K3)) * 100.0
|
||||||
|
|
||||||
|
# check bounds and format
|
||||||
|
if crit_humidity > 100:
|
||||||
|
self._state = '100'
|
||||||
|
elif crit_humidity < 0:
|
||||||
|
self._state = '0'
|
||||||
|
else:
|
||||||
|
self._state = '{0:d}'.format(int(crit_humidity))
|
||||||
|
|
||||||
|
_LOGGER.debug('Mold indicator humidity: %s ', self._state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return "%"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
if self._is_metric:
|
||||||
|
return {
|
||||||
|
ATTR_DEWPOINT: self._dewpoint,
|
||||||
|
ATTR_CRITICAL_TEMP: self._crit_temp,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
ATTR_DEWPOINT:
|
||||||
|
util.temperature.celcius_to_fahrenheit(
|
||||||
|
self._dewpoint),
|
||||||
|
ATTR_CRITICAL_TEMP:
|
||||||
|
util.temperature.celcius_to_fahrenheit(
|
||||||
|
self._crit_temp),
|
||||||
|
}
|
131
tests/components/sensor/test_moldindicator.py
Normal file
131
tests/components/sensor/test_moldindicator.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""The tests for the MoldIndicator sensor"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import homeassistant.components.sensor as sensor
|
||||||
|
from homeassistant.components.sensor.mold_indicator import (ATTR_DEWPOINT,
|
||||||
|
ATTR_CRITICAL_TEMP)
|
||||||
|
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT,
|
||||||
|
TEMP_CELSIUS)
|
||||||
|
|
||||||
|
from tests.common import get_test_home_assistant
|
||||||
|
|
||||||
|
|
||||||
|
class TestSensorMoldIndicator(unittest.TestCase):
|
||||||
|
"""Test the MoldIndicator sensor."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup things to be run when tests are started."""
|
||||||
|
self.hass = get_test_home_assistant()
|
||||||
|
self.hass.states.set('test.indoortemp', '20',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.states.set('test.outdoortemp', '10',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.states.set('test.indoorhumidity', '50',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: '%'})
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Stop down everything that was started."""
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_setup(self):
|
||||||
|
"""Test the mold indicator sensor setup"""
|
||||||
|
self.assertTrue(sensor.setup(self.hass, {
|
||||||
|
'sensor': {
|
||||||
|
'platform': 'mold_indicator',
|
||||||
|
'indoor_temp_sensor': 'test.indoortemp',
|
||||||
|
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||||
|
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||||
|
'calibration_factor': '2.0'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||||
|
assert moldind
|
||||||
|
assert '%' == moldind.attributes.get('unit_of_measurement')
|
||||||
|
|
||||||
|
def test_invalidhum(self):
|
||||||
|
"""Test invalid sensor values"""
|
||||||
|
self.hass.states.set('test.indoortemp', '10',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.states.set('test.outdoortemp', '10',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.states.set('test.indoorhumidity', '0',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: '%'})
|
||||||
|
|
||||||
|
self.assertTrue(sensor.setup(self.hass, {
|
||||||
|
'sensor': {
|
||||||
|
'platform': 'mold_indicator',
|
||||||
|
'indoor_temp_sensor': 'test.indoortemp',
|
||||||
|
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||||
|
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||||
|
'calibration_factor': '2.0'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||||
|
assert moldind
|
||||||
|
|
||||||
|
# assert state
|
||||||
|
assert moldind.state == '0'
|
||||||
|
|
||||||
|
def test_calculation(self):
|
||||||
|
"""Test the mold indicator internal calculations"""
|
||||||
|
self.assertTrue(sensor.setup(self.hass, {
|
||||||
|
'sensor': {
|
||||||
|
'platform': 'mold_indicator',
|
||||||
|
'indoor_temp_sensor': 'test.indoortemp',
|
||||||
|
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||||
|
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||||
|
'calibration_factor': '2.0'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
moldind = self.hass.states.get('sensor.mold_indicator')
|
||||||
|
assert moldind
|
||||||
|
|
||||||
|
# assert dewpoint
|
||||||
|
dewpoint = moldind.attributes.get(ATTR_DEWPOINT)
|
||||||
|
assert dewpoint
|
||||||
|
assert dewpoint > 9.25
|
||||||
|
assert dewpoint < 9.26
|
||||||
|
|
||||||
|
# assert temperature estimation
|
||||||
|
esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP)
|
||||||
|
assert esttemp
|
||||||
|
assert esttemp > 14.9
|
||||||
|
assert esttemp < 15.1
|
||||||
|
|
||||||
|
# assert mold indicator value
|
||||||
|
state = moldind.state
|
||||||
|
assert state
|
||||||
|
assert state == '68'
|
||||||
|
|
||||||
|
def test_sensor_changed(self):
|
||||||
|
"""Test the sensor_changed function"""
|
||||||
|
self.assertTrue(sensor.setup(self.hass, {
|
||||||
|
'sensor': {
|
||||||
|
'platform': 'mold_indicator',
|
||||||
|
'indoor_temp_sensor': 'test.indoortemp',
|
||||||
|
'outdoor_temp_sensor': 'test.outdoortemp',
|
||||||
|
'indoor_humidity_sensor': 'test.indoorhumidity',
|
||||||
|
'calibration_factor': '2.0'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Change indoor temp
|
||||||
|
self.hass.states.set('test.indoortemp', '30',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
assert self.hass.states.get('sensor.mold_indicator').state == '90'
|
||||||
|
|
||||||
|
# Change outdoor temp
|
||||||
|
self.hass.states.set('test.outdoortemp', '25',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
assert self.hass.states.get('sensor.mold_indicator').state == '57'
|
||||||
|
|
||||||
|
# Change humidity
|
||||||
|
self.hass.states.set('test.indoorhumidity', '20',
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
assert self.hass.states.get('sensor.mold_indicator').state == '23'
|
Loading…
x
Reference in New Issue
Block a user