Support number entities for HomeKit valve durations (#177410)

Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
This commit is contained in:
louwii17
2026-09-09 15:03:06 +03:00
committed by GitHub
co-authored by Markus Tuominen
parent 45d351fe7f
commit 95b1211180
4 changed files with 104 additions and 20 deletions
@@ -20,7 +20,6 @@ from homeassistant.components.input_number import (
CONF_MAX as INPUT_NUMBER_CONF_MAX,
CONF_MIN as INPUT_NUMBER_CONF_MIN,
CONF_STEP as INPUT_NUMBER_CONF_STEP,
DOMAIN as INPUT_NUMBER_DOMAIN,
SERVICE_SET_VALUE as INPUT_NUMBER_SERVICE_SET_VALUE,
)
from homeassistant.components.input_select import SERVICE_SELECT_OPTION
@@ -404,8 +403,9 @@ class ValveBase(HomeAccessory):
def set_duration(self, value: int) -> None:
"""Set default duration for how long the valve should remain open."""
_LOGGER.debug("%s: Set default run time to %s", self.entity_id, value)
assert self.linked_duration_entity
self.async_call_service(
INPUT_NUMBER_DOMAIN,
split_entity_id(self.linked_duration_entity)[0],
INPUT_NUMBER_SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: self.linked_duration_entity,
+7 -2
View File
@@ -31,6 +31,7 @@ from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntityFeature,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN, RemoteEntityFeature
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@@ -290,7 +291,9 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend(
)
),
),
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(INPUT_NUMBER_DOMAIN),
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(
[INPUT_NUMBER_DOMAIN, NUMBER_DOMAIN]
),
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(SENSOR_DOMAIN),
}
)
@@ -308,7 +311,9 @@ VALVE_SCHEMA = BASIC_INFO_SCHEMA.extend(
cv.string,
vol.In((TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE)),
),
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(INPUT_NUMBER_DOMAIN),
vol.Optional(CONF_LINKED_VALVE_DURATION): cv.entity_domain(
[INPUT_NUMBER_DOMAIN, NUMBER_DOMAIN]
),
vol.Optional(CONF_LINKED_VALVE_END_TIME): cv.entity_domain(SENSOR_DOMAIN),
}
)
+58 -12
View File
@@ -36,6 +36,7 @@ from homeassistant.components.lawn_mower import (
LawnMowerActivity,
LawnMowerEntityFeature,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.select import ATTR_OPTIONS
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.vacuum import (
@@ -935,33 +936,78 @@ async def test_valve_switch_with_duration_characteristics(
assert acc.get_duration() == 0
@pytest.mark.parametrize(
("entity_id", "state", "accessory_class", "config"),
[
pytest.param(
"switch.sprinkler",
STATE_OFF,
ValveSwitch,
{
"type": "sprinkler",
"linked_valve_duration": "number.valve_duration",
"linked_valve_end_time": "sensor.valve_end_time",
},
id="switch",
),
pytest.param(
"valve.sprinkler",
STATE_CLOSED,
Valve,
{
"linked_valve_duration": "number.valve_duration",
"linked_valve_end_time": "sensor.valve_end_time",
},
id="valve",
),
],
)
async def test_valve_with_duration_characteristics(
hass: HomeAssistant, hk_driver, events: list[Event]
hass: HomeAssistant,
hk_driver: HomeDriver,
events: list[Event],
entity_id: str,
state: str,
accessory_class: type[Valve | ValveSwitch],
config: dict[str, str],
) -> None:
"""Test valve with set duration and remaining duration characteristics."""
entity_id = "switch.sprinkler"
# Test with duration and end time entities linked
hass.states.async_set(entity_id, STATE_OFF)
hass.states.async_set("input_number.valve_duration", "900")
hass.states.async_set(entity_id, state)
hass.states.async_set(
"number.valve_duration",
"900",
{"min": 60, "max": 10800, "step": 60},
)
hass.states.async_set("sensor.valve_end_time", dt_util.utcnow().isoformat())
await hass.async_block_till_done()
# Using Valve instead of ValveSwitch
acc = Valve(
acc = accessory_class(
hass,
hk_driver,
"Valve",
"Sprinkler",
entity_id,
5,
{
"linked_valve_duration": "input_number.valve_duration",
"linked_valve_end_time": "sensor.valve_end_time",
},
config,
)
acc.run()
await hass.async_block_till_done()
assert acc.char_set_duration.value == 900
assert acc.char_set_duration.properties["minValue"] == 60
assert acc.char_set_duration.properties["maxValue"] == 10800
assert acc.char_set_duration.properties["minStep"] == 60
call_set_value = async_mock_service(
hass, NUMBER_DOMAIN, INPUT_NUMBER_SERVICE_SET_VALUE
)
acc.char_set_duration.client_update_value(300)
await hass.async_block_till_done()
assert call_set_value[0].data == {
"entity_id": "number.valve_duration",
"value": 300,
}
with freeze_time(dt_util.utcnow()):
hass.states.async_set(
"sensor.valve_end_time",
+37 -4
View File
@@ -135,20 +135,29 @@ def test_validate_entity_config() -> None:
{
"switch.test": {
CONF_TYPE: "sprinkler",
# Must be input_number entity
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
# Must be input_number or number entity
CONF_LINKED_VALVE_DURATION: "sensor.valve_duration",
}
},
{
"switch.test": {
CONF_TYPE: "sprinkler",
# Must be sensor (timestamp) entity
CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time",
}
},
{"fan.test": {CONF_TYPE: "invalid_type"}},
{"climate.test": {CONF_TYPE: "invalid_type"}},
{
"valve.test": {
# Must be input_number or number entity
CONF_LINKED_VALVE_DURATION: "sensor.valve_duration",
}
},
{
"valve.test": {
# Must be sensor (timestamp) entity
CONF_LINKED_VALVE_END_TIME: "datetime.valve_end_time",
# Must be input_number
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
}
},
{"valve.test": {CONF_TYPE: "invalid_type"}},
@@ -254,6 +263,17 @@ def test_validate_entity_config() -> None:
CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD,
}
}
config = {
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time",
}
assert vec({"valve.sprinkler": config}) == {
"valve.sprinkler": {
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time",
CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD,
}
}
assert vec({"sensor.co": {CONF_THRESHOLD_CO: 500}}) == {
"sensor.co": {CONF_THRESHOLD_CO: 500, CONF_LOW_BATTERY_THRESHOLD: 20}
}
@@ -299,6 +319,19 @@ def test_validate_entity_config() -> None:
CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD,
}
}
config = {
CONF_TYPE: TYPE_SPRINKLER,
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time",
}
assert vec({"switch.sprinkler": config}) == {
"switch.sprinkler": {
CONF_TYPE: TYPE_SPRINKLER,
CONF_LINKED_VALVE_DURATION: "number.valve_duration",
CONF_LINKED_VALVE_END_TIME: "sensor.valve_end_time",
CONF_LOW_BATTERY_THRESHOLD: DEFAULT_LOW_BATTERY_THRESHOLD,
}
}
def test_validate_media_player_features() -> None: