Add valve domain to HomeKit (#115901)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Sander Peterse 2024-06-30 19:04:20 +02:00 committed by GitHub
parent f672eec515
commit 2f5ec41fa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 28 deletions

View file

@ -104,12 +104,12 @@ from .util import (
_LOGGER = logging.getLogger(__name__)
SWITCH_TYPES = {
TYPE_FAUCET: "Valve",
TYPE_FAUCET: "ValveSwitch",
TYPE_OUTLET: "Outlet",
TYPE_SHOWER: "Valve",
TYPE_SPRINKLER: "Valve",
TYPE_SHOWER: "ValveSwitch",
TYPE_SPRINKLER: "ValveSwitch",
TYPE_SWITCH: "Switch",
TYPE_VALVE: "Valve",
TYPE_VALVE: "ValveSwitch",
}
TYPES: Registry[str, type[HomeAccessory]] = Registry()
@ -244,6 +244,9 @@ def get_accessory( # noqa: C901
else:
a_type = "Switch"
elif state.domain == "valve":
a_type = "Valve"
elif state.domain == "vacuum":
a_type = "Vacuum"
@ -289,7 +292,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
name: str,
entity_id: str,
aid: int,
config: dict,
config: dict[str, Any],
*args: Any,
category: int = CATEGORY_OTHER,
device_id: str | None = None,

View file

@ -17,6 +17,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
from homeassistant.config_entries import (
SOURCE_IMPORT,
ConfigEntry,
@ -105,6 +106,7 @@ SUPPORTED_DOMAINS = [
"switch",
"vacuum",
"water_heater",
VALVE_DOMAIN,
]
DEFAULT_DOMAINS = [

View file

@ -3,7 +3,7 @@
from __future__ import annotations
import logging
from typing import Any, NamedTuple
from typing import Any, Final, NamedTuple
from pyhap.characteristic import Characteristic
from pyhap.const import (
@ -28,14 +28,19 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_TYPE,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_CLOSING,
STATE_ON,
STATE_OPEN,
STATE_OPENING,
)
from homeassistant.core import State, callback, split_entity_id
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
from homeassistant.helpers.event import async_call_later
from .accessories import TYPES, HomeAccessory
from .accessories import TYPES, HomeAccessory, HomeDriver
from .const import (
CHAR_ACTIVE,
CHAR_IN_USE,
@ -55,6 +60,8 @@ from .util import cleanup_name_for_homekit
_LOGGER = logging.getLogger(__name__)
VALVE_OPEN_STATES: Final = {STATE_OPEN, STATE_OPENING, STATE_CLOSING}
class ValveInfo(NamedTuple):
"""Category and type information for valve."""
@ -211,18 +218,28 @@ class Vacuum(Switch):
self.char_on.set_value(current_state)
@TYPES.register("Valve")
class Valve(HomeAccessory):
"""Generate a Valve accessory."""
class ValveBase(HomeAccessory):
"""Valve base class."""
def __init__(self, *args: Any) -> None:
def __init__(
self,
valve_type: str,
open_states: set[str],
on_service: str,
off_service: str,
*args: Any,
**kwargs: Any,
) -> None:
"""Initialize a Valve accessory object."""
super().__init__(*args)
super().__init__(*args, **kwargs)
self.domain = split_entity_id(self.entity_id)[0]
state = self.hass.states.get(self.entity_id)
assert state
valve_type = self.config[CONF_TYPE]
self.category = VALVE_TYPE[valve_type].category
self.open_states = open_states
self.on_service = on_service
self.off_service = off_service
serv_valve = self.add_preload_service(SERV_VALVE)
self.char_active = serv_valve.configure_char(
@ -241,19 +258,64 @@ class Valve(HomeAccessory):
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
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.async_call_service(DOMAIN, service, params)
service = self.on_service if value else self.off_service
self.async_call_service(self.domain, service, params)
@callback
def async_update_state(self, new_state: State) -> None:
"""Update switch state after state changed."""
current_state = 1 if new_state.state == STATE_ON else 0
current_state = 1 if new_state.state in self.open_states else 0
_LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
self.char_active.set_value(current_state)
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
self.char_in_use.set_value(current_state)
@TYPES.register("ValveSwitch")
class ValveSwitch(ValveBase):
"""Generate a Valve accessory from a HomeAssistant switch."""
def __init__(
self,
hass: HomeAssistant,
driver: HomeDriver,
name: str,
entity_id: str,
aid: int,
config: dict[str, Any],
*args: Any,
) -> None:
"""Initialize a Valve accessory object."""
super().__init__(
config[CONF_TYPE],
{STATE_ON},
SERVICE_TURN_ON,
SERVICE_TURN_OFF,
hass,
driver,
name,
entity_id,
aid,
config,
*args,
)
@TYPES.register("Valve")
class Valve(ValveBase):
"""Generate a Valve accessory from a HomeAssistant valve."""
def __init__(self, *args: Any) -> None:
"""Initialize a Valve accessory object."""
super().__init__(
TYPE_VALVE,
VALVE_OPEN_STATES,
SERVICE_OPEN_VALVE,
SERVICE_CLOSE_VALVE,
*args,
)
@TYPES.register("SelectSwitch")
class SelectSwitch(HomeAccessory):
"""Generate a Switch accessory that contains multiple switches."""

View file

@ -335,10 +335,10 @@ def test_type_sensors(type_name, entity_id, state, attrs) -> None:
("SelectSwitch", "select.test", "option1", {}, {}),
("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}),
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}),
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}),
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}),
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}),
],
)
def test_type_switches(type_name, entity_id, state, attrs, config) -> None:
@ -350,6 +350,21 @@ def test_type_switches(type_name, entity_id, state, attrs, config) -> None:
assert mock_type.called
@pytest.mark.parametrize(
("type_name", "entity_id", "state", "attrs"),
[
("Valve", "valve.test", "on", {}),
],
)
def test_type_valve(type_name, entity_id, state, attrs) -> None:
"""Test if valve types are associated correctly."""
mock_type = Mock()
with patch.dict(TYPES, {type_name: mock_type}):
entity_state = State(entity_id, state, attrs)
get_accessory(None, None, entity_state, 2, {})
assert mock_type.called
@pytest.mark.parametrize(
("type_name", "entity_id", "state", "attrs"),
[

View file

@ -17,6 +17,7 @@ from homeassistant.components.homekit.type_switches import (
Switch,
Vacuum,
Valve,
ValveSwitch,
)
from homeassistant.components.select import ATTR_OPTIONS
from homeassistant.components.vacuum import (
@ -33,9 +34,13 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_TYPE,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_SELECT_OPTION,
STATE_CLOSED,
STATE_OFF,
STATE_ON,
STATE_OPEN,
)
from homeassistant.core import HomeAssistant, split_entity_id
import homeassistant.util.dt as dt_util
@ -140,32 +145,34 @@ async def test_switch_set_state(
assert events[-1].data[ATTR_VALUE] is None
async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None:
async def test_valve_switch_set_state(hass: HomeAssistant, hk_driver, events) -> None:
"""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})
acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 2, {CONF_TYPE: TYPE_FAUCET})
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, 3, {CONF_TYPE: TYPE_SHOWER})
acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 3, {CONF_TYPE: TYPE_SHOWER})
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, 4, {CONF_TYPE: TYPE_SPRINKLER})
acc = ValveSwitch(
hass, hk_driver, "Valve", entity_id, 4, {CONF_TYPE: TYPE_SPRINKLER}
)
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, 5, {CONF_TYPE: TYPE_VALVE})
acc = ValveSwitch(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE})
acc.run()
await hass.async_block_till_done()
@ -187,8 +194,57 @@ async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None:
assert acc.char_in_use.value == 0
# Set from HomeKit
call_turn_on = async_mock_service(hass, "switch", "turn_on")
call_turn_off = async_mock_service(hass, "switch", "turn_off")
call_turn_on = async_mock_service(hass, "switch", SERVICE_TURN_ON)
call_turn_off = async_mock_service(hass, "switch", SERVICE_TURN_OFF)
acc.char_active.client_update_value(1)
await hass.async_block_till_done()
assert acc.char_in_use.value == 1
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None
acc.char_active.client_update_value(0)
await hass.async_block_till_done()
assert acc.char_in_use.value == 0
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] is None
async def test_valve_set_state(hass: HomeAssistant, hk_driver, events) -> None:
"""Test if Valve accessory and HA are updated accordingly."""
entity_id = "valve.valve_test"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = Valve(hass, hk_driver, "Valve", entity_id, 5, {CONF_TYPE: TYPE_VALVE})
acc.run()
await hass.async_block_till_done()
assert acc.aid == 5
assert acc.category == 29 # Faucet
assert acc.char_active.value == 0
assert acc.char_in_use.value == 0
assert acc.char_valve_type.value == 0 # Generic Valve
hass.states.async_set(entity_id, STATE_OPEN)
await hass.async_block_till_done()
assert acc.char_active.value == 1
assert acc.char_in_use.value == 1
hass.states.async_set(entity_id, STATE_CLOSED)
await hass.async_block_till_done()
assert acc.char_active.value == 0
assert acc.char_in_use.value == 0
# Set from HomeKit
call_turn_on = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE)
call_turn_off = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE)
acc.char_active.client_update_value(1)
await hass.async_block_till_done()