diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 63013bd8fc9a22e3d76ba248ecf5a7fde38775fd..b74171b08f7a17e164e8bb297aac695c0d444cbf 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,7 +73,8 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('%s: <aid=%d config=%s>') + _LOGGER.debug('<entity_id=%s aid=%d config=%s>', + state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -87,6 +88,11 @@ def get_accessory(hass, state, aid, config): state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, state.name, aid=aid) + elif unit == '%': + _LOGGER.debug('Add "%s" as %s"', + state.entity_id, 'HumiditySensor') + return TYPES['HumiditySensor'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position @@ -114,8 +120,11 @@ def get_accessory(hass, state, aid, config): return TYPES['Thermostat'](hass, state.entity_id, state.name, support_auto, aid=aid) + elif state.domain == 'light': + return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean': + or state.domain == 'input_boolean' or state.domain == 'script': _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) @@ -175,7 +184,7 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_security_systems, type_sensors, + type_covers, type_lights, type_security_systems, type_sensors, type_switches, type_thermostats) for state in self._hass.states.all(): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 0af25bc44538362f7659dee6b0fe64afca7be024..4c4409e6dfc79831bc71837756770a37f8318883 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,6 +4,8 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver +from homeassistant.helpers.event import async_track_state_change + from .const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, @@ -49,6 +51,8 @@ def override_properties(char, properties=None, valid_values=None): class HomeAccessory(Accessory): """Adapter class for Accessory.""" + # pylint: disable=no-member + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, category='OTHER', **kwargs): """Initialize a Accessory object.""" @@ -59,6 +63,13 @@ class HomeAccessory(Accessory): def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) + def run(self): + """Method called by accessory after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_state(new_state=state) + async_track_state_change( + self._hass, self._entity_id, self.update_state) + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d2b1caffe5326d4a910820e080bf2401e89543d3..a45c8298b78a7bbc19e198ff588ffb7408aed97b 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,10 +23,18 @@ BRIDGE_MODEL = 'homekit.bridge' BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' +# #### Categories #### +CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_SENSOR = 'SENSOR' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' +SERV_HUMIDITY_SENSOR = 'HumiditySensor' +# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, +# StatusLowBattery, Name +SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' @@ -36,20 +44,24 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' +CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' +CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' -CHAR_ON = 'On' +CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' +CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0110bff318558dc2d37f2c98dcf1c7d11c2b73ac..36cfa4d635af7e314a1f95441b2ce86e6375f468 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -2,7 +2,6 @@ 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, add_preload_service @@ -22,7 +21,7 @@ class WindowCovering(HomeAccessory): """ def __init__(self, hass, entity_id, display_name, *args, **kwargs): - """Initialize a Window accessory object.""" + """Initialize a WindowCovering accessory object.""" super().__init__(display_name, entity_id, 'WINDOW_COVERING', *args, **kwargs) @@ -45,14 +44,6 @@ class WindowCovering(HomeAccessory): 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: @@ -65,8 +56,7 @@ class WindowCovering(HomeAccessory): self._hass.components.cover.set_cover_position( value, self._entity_id) - def update_cover_position(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py new file mode 100644 index 0000000000000000000000000000000000000000..107ad1db1e4ab5953dec7ab2e970890a935ac276 --- /dev/null +++ b/homeassistant/components/homekit/type_lights.py @@ -0,0 +1,209 @@ +"""Class to hold all light accessories.""" +import logging + +from homeassistant.components.light import ( + ATTR_RGB_COLOR, ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LIGHT, SERV_LIGHTBULB, + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + +_LOGGER = logging.getLogger(__name__) + +RGB_COLOR = 'rgb_color' + + +class Color: + """Class to handle color conversions.""" + + # pylint: disable=invalid-name + + def __init__(self, hue=None, saturation=None): + """Initialize a new Color object.""" + self.hue = hue # [0, 360] + self.saturation = saturation # [0, 1] + + def calc_hsv_to_rgb(self): + """Convert hsv_color value to rgb_color.""" + if not self.hue or not self.saturation: + return [None] * 3 + + i = int(self.hue / 60) + f = self.hue / 60 - i + v = 1 + p = 1 - self.saturation + q = 1 - self.saturation * f + t = 1 - self.saturation * (1 - f) + + rgb = [] + if i in [0, 6]: + rgb = [v, t, p] + elif i == 1: + rgb = [q, v, p] + elif i == 2: + rgb = [p, v, t] + elif i == 3: + rgb = [p, q, v] + elif i == 4: + rgb = [t, p, v] + elif i == 5: + rgb = [v, p, q] + + return [round(c * 255) for c in rgb] + + @classmethod + def calc_rgb_to_hsv(cls, rgb_color): + """Convert a give rgb_color back to a hsv_color.""" + rgb_color = [c / 255 for c in rgb_color] + c_max = max(rgb_color) + c_min = min(rgb_color) + c_diff = c_max - c_min + r, g, b = rgb_color + + hue, saturation = 0, 0 + if c_max == r: + hue = 60 * (0 + (g - b) / c_diff) + elif c_max == g: + hue = 60 * (2 + (b - r) / c_diff) + elif c_max == b: + hue = 60 * (4 + (r - g) / c_diff) + + hue = round(hue + 360) if hue < 0 else round(hue) + + if c_max != 0: + saturation = round((c_max - c_min) / c_max * 100) + + return (hue, saturation) + + +@TYPES.register('Light') +class Light(HomeAccessory): + """Generate a Light accessory for a light entity. + + Currently supports: state, brightness, rgb_color. + """ + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a new Light accessory object.""" + super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, + CHAR_HUE: False, CHAR_SATURATION: False, + RGB_COLOR: False} + + self.color = Color() + + self.chars = [] + self._features = self._hass.states.get(self._entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if self._features & SUPPORT_BRIGHTNESS: + self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_RGB_COLOR: + self.chars.append(CHAR_HUE) + self.chars.append(CHAR_SATURATION) + + serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.get_characteristic(CHAR_ON) + self.char_on.setter_callback = self.set_state + self.char_on.value = 0 + + if CHAR_BRIGHTNESS in self.chars: + self.char_brightness = serv_light \ + .get_characteristic(CHAR_BRIGHTNESS) + self.char_brightness.setter_callback = self.set_brightness + self.char_brightness.value = 0 + if CHAR_HUE in self.chars: + self.char_hue = serv_light.get_characteristic(CHAR_HUE) + self.char_hue.setter_callback = self.set_hue + self.char_hue.value = 0 + if CHAR_SATURATION in self.chars: + self.char_saturation = serv_light \ + .get_characteristic(CHAR_SATURATION) + self.char_saturation.setter_callback = self.set_saturation + self.char_saturation.value = 75 + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._flag[CHAR_BRIGHTNESS]: + return + + _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + self._flag[CHAR_ON] = True + + if value == 1: + self._hass.components.light.turn_on(self._entity_id) + elif value == 0: + self._hass.components.light.turn_off(self._entity_id) + + def set_brightness(self, value): + """Set brightness if call came from HomeKit.""" + _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + self._flag[CHAR_BRIGHTNESS] = True + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + + def set_saturation(self, value): + """Set saturation if call came from HomeKit.""" + _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + self._flag[CHAR_SATURATION] = True + self.color.saturation = value / 100 + self.set_color() + + def set_hue(self, value): + """Set hue if call came from HomeKit.""" + _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + self._flag[CHAR_HUE] = True + self.color.hue = value + self.set_color() + + def set_color(self): + """Set color if call came from HomeKit.""" + # Handle RGB Color + if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ + self._flag[CHAR_SATURATION]: + color = self.color.calc_hsv_to_rgb() + _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) + self._flag.update({ + CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) + self._hass.components.light.turn_on( + self._entity_id, rgb_color=color) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update light after state change.""" + if not new_state: + return + + # Handle State + state = new_state.state + if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ + self.char_on.value != (state == STATE_ON): + self.char_on.set_value(state == STATE_ON, should_callback=False) + self._flag[CHAR_ON] = False + + # Handle Brightness + if CHAR_BRIGHTNESS in self.chars: + brightness = new_state.attributes.get(ATTR_BRIGHTNESS) + if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + brightness = round(brightness / 255 * 100, 0) + if self.char_brightness.value != brightness: + self.char_brightness.set_value(brightness, + should_callback=False) + self._flag[CHAR_BRIGHTNESS] = False + + # Handle RGB Color + if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: + rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) + if not self._flag[RGB_COLOR] and \ + isinstance(rgb_color, (list, tuple)) and \ + list(rgb_color) != self.color.calc_hsv_to_rgb(): + hue, saturation = Color.calc_rgb_to_hsv(rgb_color) + self.char_hue.set_value(hue, should_callback=False) + self.char_saturation.set_value(saturation, + should_callback=False) + self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 02742acb75d83edb7e497e7fd8a7000bf1bf1440..1d47160f9d246573d973c8c18f9c488cc8f07692 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,7 +5,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -50,14 +49,6 @@ class SecuritySystem(HomeAccessory): self.char_target_state.setter_callback = self.set_security_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_security_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_security_state) - def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', @@ -69,8 +60,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} self._hass.services.call('alarm_control_panel', service, params) - def update_security_state(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 286862343f4d69b37928c9567e540e8100977904..759fda08a02a6db25d83234b30bf6cb2605f901d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -3,13 +3,13 @@ import logging from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import ( HomeAccessory, add_preload_service, override_properties) from .const import ( - SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,14 @@ def calc_temperature(state, unit=TEMP_CELSIUS): return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value +def calc_humidity(state): + """Calculate humidity from state.""" + try: + return float(state) + except ValueError: + return None + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -36,9 +44,9 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, name, *args, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -49,23 +57,42 @@ class TemperatureSensor(HomeAccessory): self.char_temp.value = 0 self.unit = None - 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): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update temperature after state changed.""" if new_state is None: return 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) + if temperature: + self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) + + +@TYPES.register('HumiditySensor') +class HumiditySensor(HomeAccessory): + """Generate a HumiditySensor accessory as humidity sensor.""" + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a HumiditySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + + serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity \ + .get_characteristic(CHAR_CURRENT_HUMIDITY) + self.char_humidity.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + humidity = calc_humidity(new_state.state) + if humidity: + self.char_humidity.set_value(humidity, should_callback=False) + _LOGGER.debug('%s: Current humidity set to %d%%', + self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 989bf4e19f51b269ed783446bb95ef2f496ab6b5..fd3291ffe23e3434cce245b9e837ac0fa76d0ef4 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -4,7 +4,6 @@ import logging from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -32,14 +31,6 @@ class Switch(HomeAccessory): self.char_on.value = False self.char_on.setter_callback = self.set_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_state) - def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 6e720c2214e397ff88a5c0b0fef3d0ca3edcf6b0..b73b492ba74d193e2b35f67e4c922bd0d240cca8 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -8,7 +8,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -96,14 +95,6 @@ class Thermostat(HomeAccessory): self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_thermostat(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_thermostat) - def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -142,8 +133,7 @@ class Thermostat(HomeAccessory): self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) - def update_thermostat(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e1c67cf2823f71aa930232734c2c1cdfec56454..e6dbe1ff729e5d6d8166aeacfcb691332d7c7004 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -53,6 +53,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) get_accessory(None, state, 2, {}) + def test_sensor_humidity(self): + """Test humidity sensor with % as unit.""" + with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): + state = State('sensor.humidity', '20', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): @@ -81,6 +88,12 @@ class TestGetAccessories(unittest.TestCase): self.assertEqual( self.mock_type.call_args[0][-1], False) # support_auto + def test_light(self): + """Test light devices.""" + with patch.dict(TYPES, {'Light': self.mock_type}): + state = State('light.test', 'on') + get_accessory(None, state, 2, {}) + def test_climate_support_auto(self): """Test climate devices with support for auto mode.""" with patch.dict(TYPES, {'Thermostat': self.mock_type}): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py new file mode 100644 index 0000000000000000000000000000000000000000..0e102c53860fe96465414260dc0bb317668c4ad2 --- /dev/null +++ b/tests/components/homekit/test_type_lights.py @@ -0,0 +1,160 @@ +"""Test different accessory types: Lights.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_lights import Light, Color +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +def test_calc_hsv_to_rgb(): + """Test conversion hsv to rgb.""" + color = Color(43, 23 / 100) + assert color.calc_hsv_to_rgb() == [255, 238, 196] + + color.hue, color.saturation = (79, 12 / 100) + assert color.calc_hsv_to_rgb() == [245, 255, 224] + + color.hue, color.saturation = (177, 2 / 100) + assert color.calc_hsv_to_rgb() == [250, 255, 255] + + color.hue, color.saturation = (212, 26 / 100) + assert color.calc_hsv_to_rgb() == [189, 220, 255] + + color.hue, color.saturation = (271, 93 / 100) + assert color.calc_hsv_to_rgb() == [140, 18, 255] + + color.hue, color.saturation = (355, 100 / 100) + assert color.calc_hsv_to_rgb() == [255, 0, 21] + + +def test_calc_rgb_to_hsv(): + """Test conversion rgb to hsv.""" + assert Color.calc_rgb_to_hsv([255, 0, 21]) == (355, 100) + assert Color.calc_rgb_to_hsv([245, 255, 224]) == (79, 12) + assert Color.calc_rgb_to_hsv([189, 220, 255]) == (212, 26) + + +class TestHomekitLights(unittest.TestCase): + """Test class for all accessory types regarding lights.""" + + 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 everything that was started.""" + self.hass.stop() + + def test_light_basic(self): + """Test light with char state.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 5) # Lightbulb + self.assertEqual(acc.char_on.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + # Remove entity + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_light_brightness(self): + """Test light with brightness.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_brightness.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 100) + + self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 40) + + # Set from HomeKit + acc.char_brightness.set_value(20) + acc.char_on.set_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + print(self.events[0].data) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + + def test_light_rgb_color(self): + """Test light with rgb_color.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, + ATTR_RGB_COLOR: (120, 20, 300)}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_hue.value, 0) + self.assertEqual(acc.char_saturation.value, 75) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_hue.value, 261) + self.assertEqual(acc.char_saturation.value, 93) + + # Set from HomeKit + acc.char_hue.set_value(145) + acc.char_saturation.set_value(75) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [64, 255, 143]}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index f9a14f6b8cf13959e0f8fc42c256223dbf3cccf8..b533c896019beb5e2fc0ea863c377801647b3510 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,7 +3,7 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, calc_temperature) + TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -22,6 +22,15 @@ def test_calc_temperature(): assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 +def test_calc_humidity(): + """Test if humidity is a integer.""" + assert calc_humidity(STATE_UNKNOWN) is None + assert calc_humidity('test') is None + + assert calc_humidity('20') == 20 + assert calc_humidity('75.2') == 75.2 + + class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" @@ -60,3 +69,23 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 24) + + def test_humidity(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' + + acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_humidity.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + self.assertEqual(acc.char_humidity.value, 20)