Homekit: New supported devices (#13244)

* Fixed log message
* Added support for scripts
* Added support for lights
* Small refactoring
* Added support for humidity sensor
* Added tests
This commit is contained in:
cdce8p 2018-03-16 01:05:28 +01:00 committed by GitHub
parent de1ff1e952
commit 2350ce96a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 495 additions and 64 deletions

View File

@ -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():

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}):

View File

@ -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]})

View File

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