From 37a47b5a59ec3ba5bd05fb3525874c0572a152ec Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 5 Oct 2018 12:43:50 +0200 Subject: [PATCH] Add faucet, shower, sprinkler, valve to HomeKit (#17145) --- homeassistant/components/homekit/__init__.py | 12 ++- homeassistant/components/homekit/const.py | 7 ++ .../components/homekit/type_switches.py | 58 ++++++++++++++- homeassistant/components/homekit/util.py | 8 +- .../homekit/test_get_accessories.py | 7 +- .../components/homekit/test_type_switches.py | 74 ++++++++++++++++++- tests/components/homekit/test_util.py | 14 +++- 7 files changed, 168 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bd79a0a7ca7..5d7733584be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -24,7 +24,8 @@ from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -41,8 +42,13 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 -SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', - TYPE_SWITCH: 'Switch'} +SWITCH_TYPES = { + TYPE_FAUCET: 'Valve', + TYPE_OUTLET: 'Outlet', + TYPE_SHOWER: 'Valve', + TYPE_SPRINKLER: 'Valve', + TYPE_SWITCH: 'Switch', + TYPE_VALVE: 'Valve'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index df488d4a73a..617dd3f4f22 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -32,8 +32,12 @@ BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' # #### Switch Types #### +TYPE_FAUCET = 'faucet' TYPE_OUTLET = 'outlet' +TYPE_SHOWER = 'shower' +TYPE_SPRINKLER = 'sprinkler' TYPE_SWITCH = 'switch' +TYPE_VALVE = 'valve' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' @@ -57,6 +61,7 @@ SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' +SERV_VALVE = 'Valve' SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### @@ -85,6 +90,7 @@ CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' +CHAR_IN_USE = 'InUse' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -109,6 +115,7 @@ CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' +CHAR_VALVE_TYPE = 'ValveType' # #### Properties #### PROP_MAX_VALUE = 'maxValue' diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index a5724057eee..82a5d68d644 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -5,15 +5,29 @@ from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH +from .const import ( + CHAR_ACTIVE, CHAR_IN_USE, CHAR_ON, CHAR_OUTLET_IN_USE, CHAR_VALVE_TYPE, + SERV_OUTLET, SERV_SWITCH, SERV_VALVE, TYPE_FAUCET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) +CATEGORY_SPRINKLER = 28 +CATEGORY_FAUCET = 29 +CATEGORY_SHOWER_HEAD = 30 + +VALVE_TYPE = { + TYPE_FAUCET: (CATEGORY_FAUCET, 3), + TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), + TYPE_SPRINKLER: (CATEGORY_SPRINKLER, 1), + TYPE_VALVE: (CATEGORY_FAUCET, 0), +} + @TYPES.register('Outlet') class Outlet(HomeAccessory): @@ -80,3 +94,43 @@ class Switch(HomeAccessory): self.entity_id, current_state) self.char_on.set_value(current_state) self.flag_target_state = False + + +@TYPES.register('Valve') +class Valve(HomeAccessory): + """Generate a Valve accessory.""" + + def __init__(self, *args): + """Initialize a Valve accessory object.""" + super().__init__(*args) + self.flag_target_state = False + valve_type = self.config[CONF_TYPE] + self.category = VALVE_TYPE[valve_type][0] + + serv_valve = self.add_preload_service(SERV_VALVE) + self.char_active = serv_valve.configure_char( + CHAR_ACTIVE, value=False, setter_callback=self.set_state) + self.char_in_use = serv_valve.configure_char( + CHAR_IN_USE, value=False) + self.char_valve_type = serv_valve.configure_char( + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1]) + + def set_state(self, value): + """Move value state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state to %s', + self.entity_id, value) + self.flag_target_state = True + self.char_in_use.set_value(value) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = (new_state.state == STATE_ON) + if not self.flag_target_state: + _LOGGER.debug('%s: Set current state to %s', + self.entity_id, current_state) + self.char_active.set_value(current_state) + self.char_in_use.set_value(current_state) + self.flag_target_state = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 33a4bec321f..4dd7396cf8d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -11,8 +11,8 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, - TYPE_SWITCH) + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET, + TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,9 @@ MEDIA_PLAYER_SCHEMA = vol.Schema({ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( - cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), + cv.string, vol.In(( + TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE))), }) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 5b76618d460..d5552cce82c 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,7 +9,8 @@ import homeassistant.components.climate as climate import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, @@ -140,6 +141,10 @@ def test_type_sensors(type_name, entity_id, state, attrs): ('Switch', 'script.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_FAUCET}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_VALVE}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SHOWER}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SPRINKLER}), ]) def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index c2b80226508..bc44a93884a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,8 +1,11 @@ """Test different accessory types: Switches.""" import pytest -from homeassistant.components.homekit.type_switches import Outlet, Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.components.homekit.const import ( + TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) +from homeassistant.components.homekit.type_switches import ( + Outlet, Switch, Valve) +from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id from tests.common import async_mock_service @@ -90,3 +93,70 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_valve_set_state(hass, hk_driver): + """Test if Valve accessory and HA are updated accordingly.""" + entity_id = 'switch.valve_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_FAUCET}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 29 # Faucet + assert acc.char_valve_type.value == 3 # Water faucet + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SHOWER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 30 # Shower + assert acc.char_valve_type.value == 2 # Shower head + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SPRINKLER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 28 # Sprinkler + assert acc.char_valve_type.value == 1 # Irrigation + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_VALVE}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 29 # Faucet + + assert acc.char_active.value is False + assert acc.char_in_use.value is False + assert acc.char_valve_type.value == 0 # Generic Valve + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_active.value is True + assert acc.char_in_use.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_active.value is False + assert acc.char_in_use.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_active.client_update_value, True) + await hass.async_block_till_done() + assert acc.char_in_use.value is True + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_active.client_update_value, False) + await hass.async_block_till_done() + assert acc.char_in_use.value is False + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 6ae16ee8f33..0368dfa642e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,7 +4,8 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, TYPE_OUTLET) + FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -58,8 +59,19 @@ def test_validate_entity_config(): assert vec({'media_player.demo': config}) == \ {'media_player.demo': {CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + + assert vec({'switch.demo': {CONF_TYPE: TYPE_FAUCET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_FAUCET}} assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SHOWER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SHOWER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SWITCH}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SWITCH}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_VALVE}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_VALVE}} def test_validate_media_player_features():