diff --git a/.coveragerc b/.coveragerc index ae71a10f73d33e158ec2b77841a1e441b69d3c09..4db8323c3a1157b64ad006840854b6f46a499695 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/sensor/mhz19.py b/homeassistant/components/sensor/mhz19.py index 2ca15898b1878d98e97c2de0213678eea78bd079..816b7465f8fb161706e4c5f993449b64128f5bdb 100644 --- a/homeassistant/components/sensor/mhz19.py +++ b/homeassistant/components/sensor/mhz19.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 + + 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)) - dev = MHZ19Sensor(config.get(CONF_SERIAL_DEVICE), config.get(CONF_NAME)) - add_devices([dev]) + 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 diff --git a/homeassistant/components/sensor/serial_pm.py b/homeassistant/components/sensor/serial_pm.py index 9704991e9595cccbcdd5c19b93d07488bedc79ee..a031f9cbd5647e87285fee3eb6cdd961ab6f377e 100644 --- a/homeassistant/components/sensor/serial_pm.py +++ b/homeassistant/components/sensor/serial_pm.py @@ -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__) diff --git a/requirements_all.txt b/requirements_all.txt index 32f5cb57a88279753c576afcd96214a62fe026cc..26169f838eb37b00c3324abf7e977aabbdd89471 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/tests/components/sensor/test_mhz19.py b/tests/components/sensor/test_mhz19.py new file mode 100644 index 0000000000000000000000000000000000000000..4311493ac97bcdce21d6b273377133e44e8f507a --- /dev/null +++ b/tests/components/sensor/test_mhz19.py @@ -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)