mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
Add valve domain to HomeKit (#115901)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
f672eec515
commit
2f5ec41fa6
@ -104,12 +104,12 @@ from .util import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SWITCH_TYPES = {
|
SWITCH_TYPES = {
|
||||||
TYPE_FAUCET: "Valve",
|
TYPE_FAUCET: "ValveSwitch",
|
||||||
TYPE_OUTLET: "Outlet",
|
TYPE_OUTLET: "Outlet",
|
||||||
TYPE_SHOWER: "Valve",
|
TYPE_SHOWER: "ValveSwitch",
|
||||||
TYPE_SPRINKLER: "Valve",
|
TYPE_SPRINKLER: "ValveSwitch",
|
||||||
TYPE_SWITCH: "Switch",
|
TYPE_SWITCH: "Switch",
|
||||||
TYPE_VALVE: "Valve",
|
TYPE_VALVE: "ValveSwitch",
|
||||||
}
|
}
|
||||||
TYPES: Registry[str, type[HomeAccessory]] = Registry()
|
TYPES: Registry[str, type[HomeAccessory]] = Registry()
|
||||||
|
|
||||||
@ -244,6 +244,9 @@ def get_accessory( # noqa: C901
|
|||||||
else:
|
else:
|
||||||
a_type = "Switch"
|
a_type = "Switch"
|
||||||
|
|
||||||
|
elif state.domain == "valve":
|
||||||
|
a_type = "Valve"
|
||||||
|
|
||||||
elif state.domain == "vacuum":
|
elif state.domain == "vacuum":
|
||||||
a_type = "Vacuum"
|
a_type = "Vacuum"
|
||||||
|
|
||||||
@ -289,7 +292,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
|||||||
name: str,
|
name: str,
|
||||||
entity_id: str,
|
entity_id: str,
|
||||||
aid: int,
|
aid: int,
|
||||||
config: dict,
|
config: dict[str, Any],
|
||||||
*args: Any,
|
*args: Any,
|
||||||
category: int = CATEGORY_OTHER,
|
category: int = CATEGORY_OTHER,
|
||||||
device_id: str | None = None,
|
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.lock import DOMAIN as LOCK_DOMAIN
|
||||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||||
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
|
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
|
||||||
|
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
SOURCE_IMPORT,
|
SOURCE_IMPORT,
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
@ -105,6 +106,7 @@ SUPPORTED_DOMAINS = [
|
|||||||
"switch",
|
"switch",
|
||||||
"vacuum",
|
"vacuum",
|
||||||
"water_heater",
|
"water_heater",
|
||||||
|
VALVE_DOMAIN,
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_DOMAINS = [
|
DEFAULT_DOMAINS = [
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, Final, NamedTuple
|
||||||
|
|
||||||
from pyhap.characteristic import Characteristic
|
from pyhap.characteristic import Characteristic
|
||||||
from pyhap.const import (
|
from pyhap.const import (
|
||||||
@ -28,14 +28,19 @@ from homeassistant.const import (
|
|||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
|
SERVICE_CLOSE_VALVE,
|
||||||
|
SERVICE_OPEN_VALVE,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
|
STATE_CLOSING,
|
||||||
STATE_ON,
|
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 homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .accessories import TYPES, HomeAccessory
|
from .accessories import TYPES, HomeAccessory, HomeDriver
|
||||||
from .const import (
|
from .const import (
|
||||||
CHAR_ACTIVE,
|
CHAR_ACTIVE,
|
||||||
CHAR_IN_USE,
|
CHAR_IN_USE,
|
||||||
@ -55,6 +60,8 @@ from .util import cleanup_name_for_homekit
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VALVE_OPEN_STATES: Final = {STATE_OPEN, STATE_OPENING, STATE_CLOSING}
|
||||||
|
|
||||||
|
|
||||||
class ValveInfo(NamedTuple):
|
class ValveInfo(NamedTuple):
|
||||||
"""Category and type information for valve."""
|
"""Category and type information for valve."""
|
||||||
@ -211,18 +218,28 @@ class Vacuum(Switch):
|
|||||||
self.char_on.set_value(current_state)
|
self.char_on.set_value(current_state)
|
||||||
|
|
||||||
|
|
||||||
@TYPES.register("Valve")
|
class ValveBase(HomeAccessory):
|
||||||
class Valve(HomeAccessory):
|
"""Valve base class."""
|
||||||
"""Generate a Valve accessory."""
|
|
||||||
|
|
||||||
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."""
|
"""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)
|
state = self.hass.states.get(self.entity_id)
|
||||||
assert state
|
assert state
|
||||||
|
|
||||||
valve_type = self.config[CONF_TYPE]
|
|
||||||
self.category = VALVE_TYPE[valve_type].category
|
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)
|
serv_valve = self.add_preload_service(SERV_VALVE)
|
||||||
self.char_active = serv_valve.configure_char(
|
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)
|
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
|
||||||
self.char_in_use.set_value(value)
|
self.char_in_use.set_value(value)
|
||||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
service = self.on_service if value else self.off_service
|
||||||
self.async_call_service(DOMAIN, service, params)
|
self.async_call_service(self.domain, service, params)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_state(self, new_state: State) -> None:
|
def async_update_state(self, new_state: State) -> None:
|
||||||
"""Update switch state after state changed."""
|
"""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)
|
_LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
|
||||||
self.char_active.set_value(current_state)
|
self.char_active.set_value(current_state)
|
||||||
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
|
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
|
||||||
self.char_in_use.set_value(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")
|
@TYPES.register("SelectSwitch")
|
||||||
class SelectSwitch(HomeAccessory):
|
class SelectSwitch(HomeAccessory):
|
||||||
"""Generate a Switch accessory that contains multiple switches."""
|
"""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", {}, {}),
|
("SelectSwitch", "select.test", "option1", {}, {}),
|
||||||
("Switch", "switch.test", "on", {}, {}),
|
("Switch", "switch.test", "on", {}, {}),
|
||||||
("Switch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SWITCH}),
|
("Switch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SWITCH}),
|
||||||
("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}),
|
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_FAUCET}),
|
||||||
("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}),
|
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_VALVE}),
|
||||||
("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}),
|
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SHOWER}),
|
||||||
("Valve", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}),
|
("ValveSwitch", "switch.test", "on", {}, {CONF_TYPE: TYPE_SPRINKLER}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_type_switches(type_name, entity_id, state, attrs, config) -> None:
|
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
|
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(
|
@pytest.mark.parametrize(
|
||||||
("type_name", "entity_id", "state", "attrs"),
|
("type_name", "entity_id", "state", "attrs"),
|
||||||
[
|
[
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.components.homekit.type_switches import (
|
|||||||
Switch,
|
Switch,
|
||||||
Vacuum,
|
Vacuum,
|
||||||
Valve,
|
Valve,
|
||||||
|
ValveSwitch,
|
||||||
)
|
)
|
||||||
from homeassistant.components.select import ATTR_OPTIONS
|
from homeassistant.components.select import ATTR_OPTIONS
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
@ -33,9 +34,13 @@ from homeassistant.const import (
|
|||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
|
SERVICE_CLOSE_VALVE,
|
||||||
|
SERVICE_OPEN_VALVE,
|
||||||
SERVICE_SELECT_OPTION,
|
SERVICE_SELECT_OPTION,
|
||||||
|
STATE_CLOSED,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
STATE_OPEN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, split_entity_id
|
from homeassistant.core import HomeAssistant, split_entity_id
|
||||||
import homeassistant.util.dt as dt_util
|
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
|
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."""
|
"""Test if Valve accessory and HA are updated accordingly."""
|
||||||
entity_id = "switch.valve_test"
|
entity_id = "switch.valve_test"
|
||||||
|
|
||||||
hass.states.async_set(entity_id, None)
|
hass.states.async_set(entity_id, None)
|
||||||
await hass.async_block_till_done()
|
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()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert acc.category == 29 # Faucet
|
assert acc.category == 29 # Faucet
|
||||||
assert acc.char_valve_type.value == 3 # Water 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()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert acc.category == 30 # Shower
|
assert acc.category == 30 # Shower
|
||||||
assert acc.char_valve_type.value == 2 # Shower head
|
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()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert acc.category == 28 # Sprinkler
|
assert acc.category == 28 # Sprinkler
|
||||||
assert acc.char_valve_type.value == 1 # Irrigation
|
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()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
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
|
assert acc.char_in_use.value == 0
|
||||||
|
|
||||||
# Set from HomeKit
|
# Set from HomeKit
|
||||||
call_turn_on = async_mock_service(hass, "switch", "turn_on")
|
call_turn_on = async_mock_service(hass, "switch", SERVICE_TURN_ON)
|
||||||
call_turn_off = async_mock_service(hass, "switch", "turn_off")
|
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)
|
acc.char_active.client_update_value(1)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user