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:
Andrey 2017-02-27 21:19:11 +02:00 committed by GitHub
parent d7db3aba36
commit 7ee75d67c5
5 changed files with 204 additions and 22 deletions

View file

@ -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

View file

@ -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

View file

@ -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__)

View file

@ -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

View 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)