mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +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