Add faucet, shower, sprinkler, valve to HomeKit (#17145)

This commit is contained in:
cdce8p 2018-10-05 12:43:50 +02:00 committed by GitHub
parent 2e62afabdc
commit 37a47b5a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 12 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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