diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1af56a81137..52b90af7b9b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -11,7 +11,8 @@ 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) + TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry @@ -21,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) _RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$") DOMAIN = 'homekit' -REQUIREMENTS = ['HAP-python==1.1.5'] +REQUIREMENTS = ['HAP-python==1.1.7'] BRIDGE_NAME = 'Home Assistant' CONF_PIN_CODE = 'pincode' @@ -74,7 +75,8 @@ def import_types(): 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: + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, @@ -103,8 +105,7 @@ class HomeKit(): 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') + self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin) def start_driver(self, event): """Start the accessory driver.""" diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index e1a25a2c976..f2ad6067258 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,55 +1,62 @@ """Extend the basic Accessory and Bridge functions.""" +import logging + from pyhap.accessory import Accessory, Bridge, Category from .const import ( - SERVICES_ACCESSORY_INFO, MANUFACTURER, + SERV_ACCESSORY_INFO, MANUFACTURER, CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) +_LOGGER = logging.getLogger(__name__) + + +def set_accessory_info(acc, model, manufacturer=MANUFACTURER, + serial_number='0000'): + """Set the default accessory information.""" + service = acc.get_service(SERV_ACCESSORY_INFO) + service.get_characteristic(CHAR_MODEL).set_value(model) + service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) + service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) + + +def add_preload_service(acc, service, chars=None, opt_chars=None): + """Define and return a service to be available for the accessory.""" + from pyhap.loader import get_serv_loader, get_char_loader + service = get_serv_loader().get(service) + if chars: + chars = chars if isinstance(chars, list) else [chars] + for char_name in chars: + char = get_char_loader().get(char_name) + service.add_characteristic(char) + if opt_chars: + opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] + for opt_char_name in opt_chars: + opt_char = get_char_loader().get(opt_char_name) + service.add_opt_characteristic(opt_char) + acc.add_service(service) + return service + + +def override_properties(char, new_properties): + """Override characteristic property values.""" + char.properties.update(new_properties) + + class HomeAccessory(Accessory): """Class to extend the Accessory class.""" - ALL_CATEGORIES = Category - - def __init__(self, display_name): + def __init__(self, display_name, model, category='OTHER'): """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) + set_accessory_info(self, model) + self.category = getattr(Category, category, Category.OTHER) class HomeBridge(Bridge): """Class to extend the Bridge class.""" - def __init__(self, display_name, pincode): + def __init__(self, display_name, model, 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) + set_accessory_info(self, model) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 02b2e6b8f67..f514308c823 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -2,17 +2,20 @@ MANUFACTURER = 'HomeAssistant' # Service: AccessoryInfomation -SERVICES_ACCESSORY_INFO = 'AccessoryInformation' +SERV_ACCESSORY_INFO = 'AccessoryInformation' CHAR_MODEL = 'Model' CHAR_MANUFACTURER = 'Manufacturer' CHAR_SERIAL_NUMBER = 'SerialNumber' # Service: TemperatureSensor -SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor' +SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' # Service: WindowCovering -SERVICES_WINDOW_COVERING = 'WindowCovering' +SERV_WINDOW_COVERING = 'WindowCovering' CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_TARGET_POSITION = 'TargetPosition' CHAR_POSITION_STATE = 'PositionState' + +# Properties +PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/covers.py index 1068b1e0e3f..e9fc3b08d76 100644 --- a/homeassistant/components/homekit/covers.py +++ b/homeassistant/components/homekit/covers.py @@ -5,9 +5,9 @@ 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 .accessories import HomeAccessory, add_preload_service from .const import ( - SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION, + SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE) @@ -23,10 +23,7 @@ class Window(HomeAccessory): 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) + super().__init__(display_name, entity_id, 'WINDOW') self._hass = hass self._entity_id = entity_id @@ -34,12 +31,12 @@ class Window(HomeAccessory): self.current_position = None self.homekit_target = None - self.service_cover = self.get_service(SERVICES_WINDOW_COVERING) - self.char_current_position = self.service_cover. \ + self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) + self.char_current_position = self.serv_cover. \ get_characteristic(CHAR_CURRENT_POSITION) - self.char_target_position = self.service_cover. \ + self.char_target_position = self.serv_cover. \ get_characteristic(CHAR_TARGET_POSITION) - self.char_position_state = self.service_cover. \ + self.char_position_state = self.serv_cover. \ get_characteristic(CHAR_POSITION_STATE) self.char_target_position.setter_callback = self.move_cover @@ -53,7 +50,7 @@ class Window(HomeAccessory): self._hass, self._entity_id, self.update_cover_position) def move_cover(self, value): - """Move cover to value if call came from homekit.""" + """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 diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/sensors.py index db9ba2d628a..05465cd1397 100644 --- a/homeassistant/components/homekit/sensors.py +++ b/homeassistant/components/homekit/sensors.py @@ -1,38 +1,55 @@ """Class to hold all sensor accessories.""" import logging -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ( + STATE_UNKNOWN, ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) from homeassistant.helpers.event import async_track_state_change from . import TYPES -from .accessories import HomeAccessory +from .accessories import ( + HomeAccessory, add_preload_service, override_properties) from .const import ( - SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE) + SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) _LOGGER = logging.getLogger(__name__) +def calc_temperature(state, unit=TEMP_CELSIUS): + """Calculate temperature from state and unit. + + Always return temperature as Celsius value. + Conversion is handled on the device. + """ + if state == STATE_UNKNOWN: + return None + + if unit == TEMP_FAHRENHEIT: + value = round((float(state) - 32) / 1.8, 2) + else: + value = float(state) + return value + + @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. + Sensor entity must return temperature in °C, °F 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) + super().__init__(display_name, entity_id, 'SENSOR') self._hass = hass self._entity_id = entity_id - self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR) - self.char_temp = self.service_temp. \ + self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) + self.char_temp = self.serv_temp. \ get_characteristic(CHAR_CURRENT_TEMPERATURE) + override_properties(self.char_temp, PROP_CELSIUS) + self.unit = None def run(self): """Method called be object after driver is started.""" @@ -48,6 +65,9 @@ class TemperatureSensor(HomeAccessory): if new_state is None: return - temperature = new_state.state - if temperature != STATE_UNKNOWN: - self.char_temp.set_value(float(temperature)) + unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] + temperature = calc_temperature(new_state.state, unit) + if temperature is not None: + self.char_temp.set_value(temperature) + _LOGGER.debug("%s: Current temperature set to %d°C", + self._entity_id, temperature) diff --git a/requirements_all.txt b/requirements_all.txt index 920d16980fa..93710e6506e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,7 +24,7 @@ attrs==17.4.0 DoorBirdPy==0.1.2 # homeassistant.components.homekit -HAP-python==1.1.5 +HAP-python==1.1.7 # homeassistant.components.isy994 PyISY==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e443e3ad00..6c0a4d1a586 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ asynctest>=0.11.1 # homeassistant.components.homekit -HAP-python==1.1.5 +HAP-python==1.1.7 # homeassistant.components.notify.html5 PyJWT==1.5.3 diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_sensors.py index b7d3de4e90b..8e453cea0f2 100644 --- a/tests/components/homekit/test_sensors.py +++ b/tests/components/homekit/test_sensors.py @@ -1,13 +1,25 @@ """Test different accessory types: Sensors.""" import unittest -from homeassistant.components.homekit.sensors import TemperatureSensor +from homeassistant.components.homekit.sensors import ( + TemperatureSensor, calc_temperature) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) from tests.common import get_test_home_assistant +def test_calc_temperature(): + """Test if temperature in Celsius is calculated correctly.""" + assert calc_temperature(STATE_UNKNOWN) is None + + assert calc_temperature('20') == 20 + assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 + + assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 + assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 + + class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" @@ -16,7 +28,7 @@ class TestHomekitSensors(unittest.TestCase): self.hass = get_test_home_assistant() def tearDown(self): - """Stop down everthing that was started.""" + """Stop down everything that was started.""" self.hass.stop() def test_temperature_celsius(self): @@ -32,6 +44,12 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() - self.hass.states.set(temperature_sensor, '20') + self.hass.states.set(temperature_sensor, '20', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 20) + + self.hass.states.set(temperature_sensor, '75.2', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_temp.value, 24)