Add temperature support for MH-Z19 CO2 sensor. (#6169)
* Add temperature support for MH-Z19 CO2 sensor. * Remove debug printout * More tests * Minor fixes
This commit is contained in:
parent
d7db3aba36
commit
7ee75d67c5
5 changed files with 204 additions and 22 deletions
|
@ -340,7 +340,6 @@ omit =
|
|||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
|
|
|
@ -5,25 +5,40 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/sensor.mhz19/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_NAME, CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.util.temperature import celsius_to_fahrenheit
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pmsensor==0.3']
|
||||
REQUIREMENTS = ['pmsensor==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SERIAL_DEVICE = 'serial_device'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
|
||||
|
||||
DEFAULT_NAME = 'CO2 Sensor'
|
||||
|
||||
ATTR_CO2_CONCENTRATION = 'co2_concentration'
|
||||
|
||||
SENSOR_TEMPERATURE = 'temperature'
|
||||
SENSOR_CO2 = 'co2'
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_TEMPERATURE: ['Temperature', None],
|
||||
SENSOR_CO2: ['CO2', 'ppm']
|
||||
}
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_SERIAL_DEVICE): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_CO2]):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
|
@ -37,50 +52,96 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
_LOGGER.error("Could not open serial connection to %s (%s)",
|
||||
config.get(CONF_SERIAL_DEVICE), err)
|
||||
return False
|
||||
SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit
|
||||
|
||||
dev = MHZ19Sensor(config.get(CONF_SERIAL_DEVICE), config.get(CONF_NAME))
|
||||
add_devices([dev])
|
||||
data = MHZClient(co2sensor, config.get(CONF_SERIAL_DEVICE))
|
||||
dev = []
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
dev.append(
|
||||
MHZ19Sensor(data, variable, SENSOR_TYPES[variable][1], name))
|
||||
|
||||
add_devices(dev, True)
|
||||
return True
|
||||
|
||||
|
||||
class MHZ19Sensor(Entity):
|
||||
"""Representation of an CO2 sensor."""
|
||||
|
||||
def __init__(self, serial_device, name):
|
||||
def __init__(self, mhz_client, sensor_type, temp_unit, name):
|
||||
"""Initialize a new PM sensor."""
|
||||
self._mhz_client = mhz_client
|
||||
self._sensor_type = sensor_type
|
||||
self._temp_unit = temp_unit
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._serial = serial_device
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
self._ppm = None
|
||||
self._temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
return '{}: {}'.format(self._name, SENSOR_TYPES[self._sensor_type][0])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
return self._ppm if self._sensor_type == SENSOR_CO2 \
|
||||
else self._temperature
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return "ppm"
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Read from sensor and update the state."""
|
||||
from pmsensor import co2sensor
|
||||
self._mhz_client.update()
|
||||
data = self._mhz_client.data
|
||||
self._temperature = data.get(SENSOR_TEMPERATURE)
|
||||
if self._temperature is not None and \
|
||||
self._temp_unit == TEMP_FAHRENHEIT:
|
||||
self._temperature = round(
|
||||
celsius_to_fahrenheit(self._temperature), 1)
|
||||
self._ppm = data.get(SENSOR_CO2)
|
||||
|
||||
_LOGGER.debug("Reading data from CO2 sensor")
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
result = {}
|
||||
if self._sensor_type == SENSOR_TEMPERATURE and self._ppm is not None:
|
||||
result[ATTR_CO2_CONCENTRATION] = self._ppm
|
||||
if self._sensor_type == SENSOR_CO2 and self._temperature is not None:
|
||||
result[ATTR_TEMPERATURE] = self._temperature
|
||||
return result
|
||||
|
||||
|
||||
class MHZClient(object):
|
||||
"""Get the latest data from the DHT sensor."""
|
||||
|
||||
def __init__(self, co2sensor, serial):
|
||||
"""Initialize the sensor."""
|
||||
self.co2sensor = co2sensor
|
||||
self._serial = serial
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data the MH-Z19 sensor."""
|
||||
self.data = {}
|
||||
try:
|
||||
ppm = co2sensor.read_mh_z19(self._serial)
|
||||
# values from sensor can only between 0 and 5000
|
||||
if (ppm >= 0) & (ppm <= 5000):
|
||||
self._state = ppm
|
||||
result = self.co2sensor.read_mh_z19_with_temperature(self._serial)
|
||||
if result is None:
|
||||
return
|
||||
co2, temperature = result
|
||||
|
||||
except OSError as err:
|
||||
_LOGGER.error("Could not open serial connection to %s (%s)",
|
||||
self._serial, err)
|
||||
return
|
||||
|
||||
def should_poll(self):
|
||||
"""Sensor needs polling."""
|
||||
return True
|
||||
if temperature is not None:
|
||||
self.data[SENSOR_TEMPERATURE] = temperature
|
||||
if co2 is not None and 0 < co2 <= 5000:
|
||||
self.data[SENSOR_CO2] = co2
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['pmsensor==0.3']
|
||||
REQUIREMENTS = ['pmsensor==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -412,7 +412,7 @@ plexapi==2.0.2
|
|||
|
||||
# homeassistant.components.sensor.mhz19
|
||||
# homeassistant.components.sensor.serial_pm
|
||||
pmsensor==0.3
|
||||
pmsensor==0.4
|
||||
|
||||
# homeassistant.components.climate.proliphix
|
||||
proliphix==0.4.1
|
||||
|
|
122
tests/components/sensor/test_mhz19.py
Normal file
122
tests/components/sensor/test_mhz19.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
"""Tests for MH-Z19 sensor."""
|
||||
import unittest
|
||||
from unittest.mock import patch, DEFAULT, Mock
|
||||
|
||||
from homeassistant.bootstrap import setup_component
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
import homeassistant.components.sensor.mhz19 as mhz19
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
from tests.common import get_test_home_assistant, assert_setup_component
|
||||
|
||||
|
||||
class TestMHZ19Sensor(unittest.TestCase):
|
||||
"""Test the MH-Z19 sensor."""
|
||||
|
||||
hass = None
|
||||
|
||||
def setup_method(self, method):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def teardown_method(self, method):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
def test_setup_missing_config(self):
|
||||
"""Test setup with configuration missing required entries."""
|
||||
with assert_setup_component(0):
|
||||
assert setup_component(self.hass, DOMAIN, {
|
||||
'sensor': {'platform': 'mhz19'}})
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19', side_effect=OSError('test error'))
|
||||
def test_setup_failed_connect(self, mock_co2):
|
||||
"""Test setup when connection error occurs."""
|
||||
self.assertFalse(mhz19.setup_platform(self.hass, {
|
||||
'platform': 'mhz19',
|
||||
mhz19.CONF_SERIAL_DEVICE: 'test.serial',
|
||||
}, None))
|
||||
|
||||
def test_setup_connected(self):
|
||||
"""Test setup when connection succeeds."""
|
||||
with patch.multiple('pmsensor.co2sensor', read_mh_z19=DEFAULT,
|
||||
read_mh_z19_with_temperature=DEFAULT):
|
||||
from pmsensor.co2sensor import read_mh_z19_with_temperature
|
||||
read_mh_z19_with_temperature.return_value = None
|
||||
mock_add = Mock()
|
||||
self.assertTrue(mhz19.setup_platform(self.hass, {
|
||||
'platform': 'mhz19',
|
||||
'monitored_conditions': ['co2', 'temperature'],
|
||||
mhz19.CONF_SERIAL_DEVICE: 'test.serial',
|
||||
}, mock_add))
|
||||
self.assertEqual(1, mock_add.call_count)
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
side_effect=OSError('test error'))
|
||||
def test_client_update_oserror(self, mock_function):
|
||||
"""Test MHZClient when library throws OSError."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
client.update()
|
||||
self.assertEqual({}, client.data)
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
return_value=(5001, 24))
|
||||
def test_client_update_ppm_overflow(self, mock_function):
|
||||
"""Test MHZClient when ppm is too high."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
client.update()
|
||||
self.assertIsNone(client.data.get('co2'))
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
return_value=(1000, 24))
|
||||
def test_client_update_good_read(self, mock_function):
|
||||
"""Test MHZClient when ppm is too high."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
client.update()
|
||||
self.assertEqual({'temperature': 24, 'co2': 1000}, client.data)
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
return_value=(1000, 24))
|
||||
def test_co2_sensor(self, mock_function):
|
||||
"""Test CO2 sensor."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
sensor = mhz19.MHZ19Sensor(client, mhz19.SENSOR_CO2, None, 'name')
|
||||
sensor.update()
|
||||
|
||||
self.assertEqual('name: CO2', sensor.name)
|
||||
self.assertEqual(1000, sensor.state)
|
||||
self.assertEqual('ppm', sensor.unit_of_measurement)
|
||||
self.assertTrue(sensor.should_poll)
|
||||
self.assertEqual({'temperature': 24}, sensor.device_state_attributes)
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
return_value=(1000, 24))
|
||||
def test_temperature_sensor(self, mock_function):
|
||||
"""Test temperature sensor."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
sensor = mhz19.MHZ19Sensor(
|
||||
client, mhz19.SENSOR_TEMPERATURE, None, 'name')
|
||||
sensor.update()
|
||||
|
||||
self.assertEqual('name: Temperature', sensor.name)
|
||||
self.assertEqual(24, sensor.state)
|
||||
self.assertEqual('°C', sensor.unit_of_measurement)
|
||||
self.assertTrue(sensor.should_poll)
|
||||
self.assertEqual(
|
||||
{'co2_concentration': 1000}, sensor.device_state_attributes)
|
||||
|
||||
@patch('pmsensor.co2sensor.read_mh_z19_with_temperature',
|
||||
return_value=(1000, 24))
|
||||
def test_temperature_sensor_f(self, mock_function):
|
||||
"""Test temperature sensor."""
|
||||
from pmsensor import co2sensor
|
||||
client = mhz19.MHZClient(co2sensor, 'test.serial')
|
||||
sensor = mhz19.MHZ19Sensor(
|
||||
client, mhz19.SENSOR_TEMPERATURE, TEMP_FAHRENHEIT, 'name')
|
||||
sensor.update()
|
||||
|
||||
self.assertEqual(75.2, sensor.state)
|
Loading…
Add table
Reference in a new issue