diff --git a/CODEOWNERS b/CODEOWNERS index ff07940d9cb..846eb20b3fe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ homeassistant/components/*/axis.py @kane610 homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline +homeassistant/components/homekit/* @cdce8p homeassistant/components/*/deconz.py @kane610 homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/velux.py @Julius2342 diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py new file mode 100644 index 00000000000..021c682466e --- /dev/null +++ b/homeassistant/components/homekit/__init__.py @@ -0,0 +1,133 @@ +"""Support for Apple Homekit. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/homekit/ +""" +import asyncio +import logging +import re + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, + TEMP_CELSIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.util import get_local_ip +from homeassistant.util.decorator import Registry + +TYPES = Registry() +_LOGGER = logging.getLogger(__name__) + +_RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$") + +DOMAIN = 'homekit' +REQUIREMENTS = ['HAP-python==1.1.5'] + +BRIDGE_NAME = 'Home Assistant' +CONF_PIN_CODE = 'pincode' + +HOMEKIT_FILE = '.homekit.state' + + +def valid_pin(value): + """Validate pincode value.""" + match = _RE_VALID_PINCODE.findall(value.strip()) + if match == []: + raise vol.Invalid("Pin must be in the format: '123-45-678'") + return match[0] + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All({ + vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), + vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the homekit component.""" + _LOGGER.debug("Begin setup homekit") + + conf = config[DOMAIN] + port = conf.get(CONF_PORT) + pin = str.encode(conf.get(CONF_PIN_CODE)) + + homekit = Homekit(hass, port) + homekit.setup_bridge(pin) + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, homekit.start_driver) + return True + + +def import_types(): + """Import all types from files in the homekit dir.""" + _LOGGER.debug("Import type files.") + # pylint: disable=unused-variable + from .covers import Window # noqa F401 + # pylint: disable=unused-variable + from .sensors import TemperatureSensor # noqa F401 + + +def get_accessory(hass, state): + """Take state and return an accessory object if supported.""" + if state.domain == 'sensor': + if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS: + _LOGGER.debug("Add \"%s\" as \"%s\"", + state.entity_id, 'TemperatureSensor') + return TYPES['TemperatureSensor'](hass, state.entity_id, + state.name) + + elif state.domain == 'cover': + # Only add covers that support set_cover_position + if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: + _LOGGER.debug("Add \"%s\" as \"%s\"", + state.entity_id, 'Window') + return TYPES['Window'](hass, state.entity_id, state.name) + + return None + + +class Homekit(): + """Class to handle all actions between homekit and Home Assistant.""" + + def __init__(self, hass, port): + """Initialize a homekit object.""" + self._hass = hass + self._port = port + self.bridge = None + self.driver = None + + def setup_bridge(self, pin): + """Setup the bridge component to track all accessories.""" + from .accessories import HomeBridge + self.bridge = HomeBridge(BRIDGE_NAME, pincode=pin) + self.bridge.set_accessory_info('homekit.bridge') + + def start_driver(self, event): + """Start the accessory driver.""" + from pyhap.accessory_driver import AccessoryDriver + self._hass.bus.listen_once( + EVENT_HOMEASSISTANT_STOP, self.stop_driver) + + import_types() + _LOGGER.debug("Start adding accessories.") + for state in self._hass.states.all(): + acc = get_accessory(self._hass, state) + if acc is not None: + self.bridge.add_accessory(acc) + + ip_address = get_local_ip() + path = self._hass.config.path(HOMEKIT_FILE) + self.driver = AccessoryDriver(self.bridge, self._port, + ip_address, path) + _LOGGER.debug("Driver started") + self.driver.start() + + def stop_driver(self, event): + """Stop the accessory driver.""" + _LOGGER.debug("Driver stop") + if self.driver is not None: + self.driver.stop() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py new file mode 100644 index 00000000000..e1a25a2c976 --- /dev/null +++ b/homeassistant/components/homekit/accessories.py @@ -0,0 +1,55 @@ +"""Extend the basic Accessory and Bridge functions.""" +from pyhap.accessory import Accessory, Bridge, Category + +from .const import ( + SERVICES_ACCESSORY_INFO, MANUFACTURER, + CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) + + +class HomeAccessory(Accessory): + """Class to extend the Accessory class.""" + + ALL_CATEGORIES = Category + + def __init__(self, display_name): + """Initialize a Accessory object.""" + super().__init__(display_name) + + def set_category(self, category): + """Set the category of the accessory.""" + self.category = category + + def add_preload_service(self, service): + """Define the services to be available for the accessory.""" + from pyhap.loader import get_serv_loader + self.add_service(get_serv_loader().get(service)) + + def set_accessory_info(self, model, manufacturer=MANUFACTURER, + serial_number='0000'): + """Set the default accessory information.""" + service_info = self.get_service(SERVICES_ACCESSORY_INFO) + service_info.get_characteristic(CHAR_MODEL) \ + .set_value(model) + service_info.get_characteristic(CHAR_MANUFACTURER) \ + .set_value(manufacturer) + service_info.get_characteristic(CHAR_SERIAL_NUMBER) \ + .set_value(serial_number) + + +class HomeBridge(Bridge): + """Class to extend the Bridge class.""" + + def __init__(self, display_name, pincode): + """Initialize a Bridge object.""" + super().__init__(display_name, pincode=pincode) + + def set_accessory_info(self, model, manufacturer=MANUFACTURER, + serial_number='0000'): + """Set the default accessory information.""" + service_info = self.get_service(SERVICES_ACCESSORY_INFO) + service_info.get_characteristic(CHAR_MODEL) \ + .set_value(model) + service_info.get_characteristic(CHAR_MANUFACTURER) \ + .set_value(manufacturer) + service_info.get_characteristic(CHAR_SERIAL_NUMBER) \ + .set_value(serial_number) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py new file mode 100644 index 00000000000..6c58b7fe45f --- /dev/null +++ b/homeassistant/components/homekit/const.py @@ -0,0 +1,18 @@ +"""Constants used be the homekit component.""" +MANUFACTURER = 'HomeAssistant' + +# Service: AccessoryInfomation +SERVICES_ACCESSORY_INFO = 'AccessoryInformation' +CHAR_MODEL = 'Model' +CHAR_MANUFACTURER = 'Manufacturer' +CHAR_SERIAL_NUMBER = 'SerialNumber' + +# Service: TemperatureSensor +SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor' +CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' + +# Service: WindowCovering +SERVICES_WINDOW_COVERING = 'WindowCovering' +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_TARGET_POSITION = 'TargetPosition' +CHAR_POSITION_STATE = 'PositionState' diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/covers.py new file mode 100644 index 00000000000..1068b1e0e3f --- /dev/null +++ b/homeassistant/components/homekit/covers.py @@ -0,0 +1,84 @@ +"""Class to hold all cover accessories.""" +import logging + +from homeassistant.components.cover import ATTR_CURRENT_POSITION +from homeassistant.helpers.event import async_track_state_change + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION, + CHAR_TARGET_POSITION, CHAR_POSITION_STATE) + + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('Window') +class Window(HomeAccessory): + """Generate a Window accessory for a cover entity. + + The cover entity must support: set_cover_position. + """ + + def __init__(self, hass, entity_id, display_name): + """Initialize a Window accessory object.""" + super().__init__(display_name) + self.set_category(self.ALL_CATEGORIES.WINDOW) + self.set_accessory_info(entity_id) + self.add_preload_service(SERVICES_WINDOW_COVERING) + + self._hass = hass + self._entity_id = entity_id + + self.current_position = None + self.homekit_target = None + + self.service_cover = self.get_service(SERVICES_WINDOW_COVERING) + self.char_current_position = self.service_cover. \ + get_characteristic(CHAR_CURRENT_POSITION) + self.char_target_position = self.service_cover. \ + get_characteristic(CHAR_TARGET_POSITION) + self.char_position_state = self.service_cover. \ + get_characteristic(CHAR_POSITION_STATE) + + self.char_target_position.setter_callback = self.move_cover + + def run(self): + """Method called be object after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_cover_position(new_state=state) + + async_track_state_change( + self._hass, self._entity_id, self.update_cover_position) + + def move_cover(self, value): + """Move cover to value if call came from homekit.""" + if value != self.current_position: + _LOGGER.debug("%s: Set position to %d", self._entity_id, value) + self.homekit_target = value + if value > self.current_position: + self.char_position_state.set_value(1) + elif value < self.current_position: + self.char_position_state.set_value(0) + self._hass.services.call( + 'cover', 'set_cover_position', + {'entity_id': self._entity_id, 'position': value}) + + def update_cover_position(self, entity_id=None, old_state=None, + new_state=None): + """Update cover position after state changed.""" + if new_state is None: + return + + current_position = new_state.attributes[ATTR_CURRENT_POSITION] + if current_position is None: + return + self.current_position = int(current_position) + self.char_current_position.set_value(self.current_position) + + if self.homekit_target is None or \ + abs(self.current_position - self.homekit_target) < 6: + self.char_target_position.set_value(self.current_position) + self.char_position_state.set_value(2) + self.homekit_target = None diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/sensors.py new file mode 100644 index 00000000000..dcbb25c9e15 --- /dev/null +++ b/homeassistant/components/homekit/sensors.py @@ -0,0 +1,50 @@ +"""Class to hold all sensor accessories.""" +import logging + +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.event import async_track_state_change + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE) + + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('TemperatureSensor') +class TemperatureSensor(HomeAccessory): + """Generate a TemperatureSensor accessory for a temperature sensor. + + Sensor entity must return either temperature in °C or STATE_UNKNOWN. + """ + + def __init__(self, hass, entity_id, display_name): + """Initialize a TemperatureSensor accessory object.""" + super().__init__(display_name) + self.set_category(self.ALL_CATEGORIES.SENSOR) + self.set_accessory_info(entity_id) + self.add_preload_service(SERVICES_TEMPERATURE_SENSOR) + + self._hass = hass + self._entity_id = entity_id + + self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR) + self.char_temp = self.service_temp. \ + get_characteristic(CHAR_CURRENT_TEMPERATURE) + + def run(self): + """Method called be object after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_temperature(new_state=state) + + async_track_state_change( + self._hass, self._entity_id, self.update_temperature) + + def update_temperature(self, entity_id=None, old_state=None, + new_state=None): + """Update temperature after state changed.""" + temperature = new_state.state + if temperature != STATE_UNKNOWN: + self.char_temp.set_value(float(temperature)) diff --git a/requirements_all.txt b/requirements_all.txt index b18f38dffd4..abf4dea14e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,6 +23,9 @@ attrs==17.4.0 # homeassistant.components.doorbird DoorBirdPy==0.1.2 +# homeassistant.components.homekit +HAP-python==1.1.5 + # homeassistant.components.isy994 PyISY==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1d776c110d..9b054da50d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,6 +18,9 @@ flake8-docstrings==1.0.3 asynctest>=0.11.1 +# homeassistant.components.homekit +HAP-python==1.1.5 + # homeassistant.components.notify.html5 PyJWT==1.5.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 42acee96206..317cecfb164 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -47,6 +47,7 @@ TEST_REQUIREMENTS = ( 'evohomeclient', 'feedparser', 'gTTS-token', + 'HAP-python', 'ha-ffmpeg', 'haversine', 'hbmqtt', @@ -92,6 +93,9 @@ TEST_REQUIREMENTS = ( IGNORE_PACKAGES = ( 'homeassistant.components.recorder.models', + 'homeassistant.components.homekit.accessories', + 'homeassistant.components.homekit.covers', + 'homeassistant.components.homekit.sensors' ) IGNORE_PIN = ('colorlog>2.1,<3', 'keyring>=9.3,<10.0', 'urllib3') diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py new file mode 100644 index 00000000000..61a60cee2ac --- /dev/null +++ b/tests/components/homekit/__init__.py @@ -0,0 +1 @@ +"""The tests for the homekit component.""" diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_covers.py new file mode 100644 index 00000000000..8810beb6210 --- /dev/null +++ b/tests/components/homekit/test_covers.py @@ -0,0 +1,83 @@ +"""Test different accessory types: Covers.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_CURRENT_POSITION) +from homeassistant.components.homekit.covers import Window +from homeassistant.const import ( + STATE_UNKNOWN, STATE_OPEN, + ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) + +from tests.common import get_test_home_assistant + + +class TestHomekitSensors(unittest.TestCase): + """Test class for all accessory types regarding covers.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everthing that was started.""" + self.hass.stop() + + def test_window_set_cover_position(self): + """Test if accessory and HA are updated accordingly.""" + window_cover = 'cover.window' + + acc = Window(self.hass, window_cover, 'Cover') + acc.run() + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 0) + + self.hass.states.set(window_cover, STATE_UNKNOWN, + {ATTR_CURRENT_POSITION: None}) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 0) + + self.hass.states.set(window_cover, STATE_OPEN, + {ATTR_CURRENT_POSITION: 50}) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_position.value, 50) + self.assertEqual(acc.char_target_position.value, 50) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.set_value(25) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'set_cover_position') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25) + + self.assertEqual(acc.char_current_position.value, 50) + self.assertEqual(acc.char_target_position.value, 25) + self.assertEqual(acc.char_position_state.value, 0) + + # Set from HomeKit + acc.char_target_position.set_value(75) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'set_cover_position') + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75) + + self.assertEqual(acc.char_current_position.value, 50) + self.assertEqual(acc.char_target_position.value, 75) + self.assertEqual(acc.char_position_state.value, 1) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py new file mode 100644 index 00000000000..e20e87871b8 --- /dev/null +++ b/tests/components/homekit/test_get_accessories.py @@ -0,0 +1,46 @@ +"""Package to test the get_accessory method.""" +from unittest.mock import patch, MagicMock + +from homeassistant.core import State +from homeassistant.components.homekit import ( + TYPES, get_accessory, import_types) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, + TEMP_CELSIUS, STATE_UNKNOWN) + + +def test_import_types(): + """Test if all type files are imported correctly.""" + try: + import_types() + assert True + # pylint: disable=broad-except + except Exception: + assert False + + +def test_component_not_supported(): + """Test with unsupported component.""" + state = State('demo.unsupported', STATE_UNKNOWN) + + assert True if get_accessory(None, state) is None else False + + +def test_sensor_temperatur_celsius(): + """Test temperature sensor with celsius as unit.""" + mock_type = MagicMock() + with patch.dict(TYPES, {'TemperatureSensor': mock_type}): + state = State('sensor.temperatur', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + get_accessory(None, state) + assert len(mock_type.mock_calls) == 1 + + +def test_cover_set_position(): + """Test cover with support for set_cover_position.""" + mock_type = MagicMock() + with patch.dict(TYPES, {'Window': mock_type}): + state = State('cover.setposition', 'open', + {ATTR_SUPPORTED_FEATURES: 4}) + get_accessory(None, state) + assert len(mock_type.mock_calls) == 1 diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py new file mode 100644 index 00000000000..7182fd6f7d9 --- /dev/null +++ b/tests/components/homekit/test_homekit.py @@ -0,0 +1,124 @@ +"""Tests for the homekit component.""" + +import unittest +from unittest.mock import patch + +import voluptuous as vol + +from homeassistant import setup +from homeassistant.core import Event +from homeassistant.components.homekit import ( + CONF_PIN_CODE, BRIDGE_NAME, Homekit, valid_pin) +from homeassistant.components.homekit.covers import Window +from homeassistant.components.homekit.sensors import TemperatureSensor +from homeassistant.const import ( + CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +from tests.common import get_test_home_assistant + +HOMEKIT_PATH = 'homeassistant.components.homekit' + +CONFIG_MIN = {'homekit': {}} +CONFIG = { + 'homekit': { + CONF_PORT: 11111, + CONF_PIN_CODE: '987-65-432', + } +} + + +class TestHomekit(unittest.TestCase): + """Test the Multicover component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everthing that was started.""" + self.hass.stop() + + @patch(HOMEKIT_PATH + '.Homekit.start_driver') + @patch(HOMEKIT_PATH + '.Homekit.setup_bridge') + @patch(HOMEKIT_PATH + '.Homekit.__init__') + def test_setup_min(self, mock_homekit, mock_setup_bridge, + mock_start_driver): + """Test async_setup with minimal config option.""" + mock_homekit.return_value = None + + self.assertTrue(setup.setup_component( + self.hass, 'homekit', CONFIG_MIN)) + + mock_homekit.assert_called_once_with(self.hass, 51826) + mock_setup_bridge.assert_called_with(b'123-45-678') + mock_start_driver.assert_not_called() + + self.hass.start() + self.hass.block_till_done() + mock_start_driver.assert_called_once() + + @patch(HOMEKIT_PATH + '.Homekit.start_driver') + @patch(HOMEKIT_PATH + '.Homekit.setup_bridge') + @patch(HOMEKIT_PATH + '.Homekit.__init__') + def test_setup_parameters(self, mock_homekit, mock_setup_bridge, + mock_start_driver): + """Test async_setup with full config option.""" + mock_homekit.return_value = None + + self.assertTrue(setup.setup_component( + self.hass, 'homekit', CONFIG)) + + mock_homekit.assert_called_once_with(self.hass, 11111) + mock_setup_bridge.assert_called_with(b'987-65-432') + + def test_validate_pincode(self): + """Test async_setup with invalid config option.""" + schema = vol.Schema(valid_pin) + + for value in ('', '123-456-78', 'a23-45-678', '12345678'): + with self.assertRaises(vol.MultipleInvalid): + schema(value) + + for value in ('123-45-678', '234-56-789'): + self.assertTrue(schema(value)) + + @patch('pyhap.accessory_driver.AccessoryDriver.persist') + @patch('pyhap.accessory_driver.AccessoryDriver.stop') + @patch('pyhap.accessory_driver.AccessoryDriver.start') + @patch(HOMEKIT_PATH + '.import_types') + @patch(HOMEKIT_PATH + '.get_accessory') + def test_homekit_pyhap_interaction( + self, mock_get_accessory, mock_import_types, + mock_driver_start, mock_driver_stop, mock_file_persist): + """Test the interaction between the homekit class and pyhap.""" + acc1 = TemperatureSensor(self.hass, 'sensor.temp', 'Temperature') + acc2 = Window(self.hass, 'cover.hall_window', 'Cover') + mock_get_accessory.side_effect = [acc1, acc2] + + homekit = Homekit(self.hass, 51826) + homekit.setup_bridge(b'123-45-678') + + self.assertEqual(homekit.bridge.display_name, BRIDGE_NAME) + + self.hass.states.set('demo.demo1', 'on') + self.hass.states.set('demo.demo2', 'off') + + self.hass.start() + self.hass.block_till_done() + + homekit.start_driver(Event(EVENT_HOMEASSISTANT_START)) + + self.assertEqual(mock_get_accessory.call_count, 2) + mock_import_types.assert_called_once() + mock_driver_start.assert_called_once() + + accessories = homekit.bridge.accessories + self.assertEqual(accessories[2], acc1) + self.assertEqual(accessories[3], acc2) + + mock_driver_stop.assert_not_called() + + self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) + self.hass.block_till_done() + + mock_driver_stop.assert_called_once() diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_sensors.py new file mode 100644 index 00000000000..cebbd4f5aea --- /dev/null +++ b/tests/components/homekit/test_sensors.py @@ -0,0 +1,37 @@ +"""Test different accessory types: Sensors.""" +import unittest + +from homeassistant.components.homekit.sensors import TemperatureSensor +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +class TestHomekitSensors(unittest.TestCase): + """Test class for all accessory types regarding sensors.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everthing that was started.""" + self.hass.stop() + + def test_temperature_celsius(self): + """Test if accessory is updated after state change.""" + temperature_sensor = 'sensor.temperature' + + self.hass.states.set(temperature_sensor, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + + acc = TemperatureSensor(self.hass, temperature_sensor, 'Temperature') + acc.run() + + self.assertEqual(acc.char_temp.value, 0.0) + + self.hass.states.set(temperature_sensor, '20') + self.hass.block_till_done() + self.assertEqual(acc.char_temp.value, 20)