Add valve domain to HomeKit (#115901)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
f672eec515
commit
2f5ec41fa6
5 changed files with 166 additions and 28 deletions
|
@ -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,
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"),
|
||||
[
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue