mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add valve entity support for ESPHome (#115341)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
8fb551430d
commit
14515b77bb
@ -36,6 +36,7 @@ from aioesphomeapi import (
|
|||||||
TextSensorInfo,
|
TextSensorInfo,
|
||||||
TimeInfo,
|
TimeInfo,
|
||||||
UserService,
|
UserService,
|
||||||
|
ValveInfo,
|
||||||
build_unique_id,
|
build_unique_id,
|
||||||
)
|
)
|
||||||
from aioesphomeapi.model import ButtonInfo
|
from aioesphomeapi.model import ButtonInfo
|
||||||
@ -78,6 +79,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
|||||||
TextInfo: Platform.TEXT,
|
TextInfo: Platform.TEXT,
|
||||||
TextSensorInfo: Platform.SENSOR,
|
TextSensorInfo: Platform.SENSOR,
|
||||||
TimeInfo: Platform.TIME,
|
TimeInfo: Platform.TIME,
|
||||||
|
ValveInfo: Platform.VALVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
103
homeassistant/components/esphome/valve.py
Normal file
103
homeassistant/components/esphome/valve.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""Support for ESPHome valves."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aioesphomeapi import EntityInfo, ValveInfo, ValveOperation, ValveState
|
||||||
|
|
||||||
|
from homeassistant.components.valve import (
|
||||||
|
ValveDeviceClass,
|
||||||
|
ValveEntity,
|
||||||
|
ValveEntityFeature,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
|
from .entity import (
|
||||||
|
EsphomeEntity,
|
||||||
|
convert_api_error_ha_error,
|
||||||
|
esphome_state_property,
|
||||||
|
platform_async_setup_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
"""Set up ESPHome valves based on a config entry."""
|
||||||
|
await platform_async_setup_entry(
|
||||||
|
hass,
|
||||||
|
entry,
|
||||||
|
async_add_entities,
|
||||||
|
info_type=ValveInfo,
|
||||||
|
entity_type=EsphomeValve,
|
||||||
|
state_type=ValveState,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
|
||||||
|
"""A valve implementation for ESPHome."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||||
|
"""Set attrs from static info."""
|
||||||
|
super()._on_static_info_update(static_info)
|
||||||
|
static_info = self._static_info
|
||||||
|
flags = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
|
||||||
|
if static_info.supports_stop:
|
||||||
|
flags |= ValveEntityFeature.STOP
|
||||||
|
if static_info.supports_position:
|
||||||
|
flags |= ValveEntityFeature.SET_POSITION
|
||||||
|
self._attr_supported_features = flags
|
||||||
|
self._attr_device_class = try_parse_enum(
|
||||||
|
ValveDeviceClass, static_info.device_class
|
||||||
|
)
|
||||||
|
self._attr_assumed_state = static_info.assumed_state
|
||||||
|
self._attr_reports_position = static_info.supports_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def is_closed(self) -> bool:
|
||||||
|
"""Return if the valve is closed or not."""
|
||||||
|
return self._state.position == 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def is_opening(self) -> bool:
|
||||||
|
"""Return if the valve is opening or not."""
|
||||||
|
return self._state.current_operation is ValveOperation.IS_OPENING
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def is_closing(self) -> bool:
|
||||||
|
"""Return if the valve is closing or not."""
|
||||||
|
return self._state.current_operation is ValveOperation.IS_CLOSING
|
||||||
|
|
||||||
|
@property
|
||||||
|
@esphome_state_property
|
||||||
|
def current_valve_position(self) -> int | None:
|
||||||
|
"""Return current position of valve. 0 is closed, 100 is open."""
|
||||||
|
return round(self._state.position * 100.0)
|
||||||
|
|
||||||
|
@convert_api_error_ha_error
|
||||||
|
async def async_open_valve(self, **kwargs: Any) -> None:
|
||||||
|
"""Open the valve."""
|
||||||
|
self._client.valve_command(key=self._key, position=1.0)
|
||||||
|
|
||||||
|
@convert_api_error_ha_error
|
||||||
|
async def async_close_valve(self, **kwargs: Any) -> None:
|
||||||
|
"""Close valve."""
|
||||||
|
self._client.valve_command(key=self._key, position=0.0)
|
||||||
|
|
||||||
|
@convert_api_error_ha_error
|
||||||
|
async def async_stop_valve(self, **kwargs: Any) -> None:
|
||||||
|
"""Stop the valve."""
|
||||||
|
self._client.valve_command(key=self._key, stop=True)
|
||||||
|
|
||||||
|
@convert_api_error_ha_error
|
||||||
|
async def async_set_valve_position(self, position: float) -> None:
|
||||||
|
"""Move the valve to a specific position."""
|
||||||
|
self._client.valve_command(key=self._key, position=position / 100)
|
196
tests/components/esphome/test_valve.py
Normal file
196
tests/components/esphome/test_valve.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
"""Test ESPHome valves."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
from aioesphomeapi import (
|
||||||
|
APIClient,
|
||||||
|
EntityInfo,
|
||||||
|
EntityState,
|
||||||
|
UserService,
|
||||||
|
ValveInfo,
|
||||||
|
ValveOperation,
|
||||||
|
ValveState,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.valve import (
|
||||||
|
ATTR_CURRENT_POSITION,
|
||||||
|
ATTR_POSITION,
|
||||||
|
DOMAIN as VALVE_DOMAIN,
|
||||||
|
SERVICE_CLOSE_VALVE,
|
||||||
|
SERVICE_OPEN_VALVE,
|
||||||
|
SERVICE_SET_VALVE_POSITION,
|
||||||
|
SERVICE_STOP_VALVE,
|
||||||
|
STATE_CLOSED,
|
||||||
|
STATE_CLOSING,
|
||||||
|
STATE_OPEN,
|
||||||
|
STATE_OPENING,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import MockESPHomeDevice
|
||||||
|
|
||||||
|
|
||||||
|
async def test_valve_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: Callable[
|
||||||
|
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||||
|
Awaitable[MockESPHomeDevice],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test a generic valve entity."""
|
||||||
|
entity_info = [
|
||||||
|
ValveInfo(
|
||||||
|
object_id="myvalve",
|
||||||
|
key=1,
|
||||||
|
name="my valve",
|
||||||
|
unique_id="my_valve",
|
||||||
|
supports_position=True,
|
||||||
|
supports_stop=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
states = [
|
||||||
|
ValveState(
|
||||||
|
key=1,
|
||||||
|
position=0.5,
|
||||||
|
current_operation=ValveOperation.IS_OPENING,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
user_service = []
|
||||||
|
mock_device = await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=entity_info,
|
||||||
|
user_service=user_service,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_OPENING
|
||||||
|
assert state.attributes[ATTR_CURRENT_POSITION] == 50
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_CLOSE_VALVE,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_OPEN_VALVE,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_SET_VALVE_POSITION,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve", ATTR_POSITION: 50},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, position=0.5)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_STOP_VALVE,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, stop=True)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
mock_device.set_state(
|
||||||
|
ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_CLOSED
|
||||||
|
|
||||||
|
mock_device.set_state(
|
||||||
|
ValveState(key=1, position=0.5, current_operation=ValveOperation.IS_CLOSING)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_CLOSING
|
||||||
|
|
||||||
|
mock_device.set_state(
|
||||||
|
ValveState(key=1, position=1.0, current_operation=ValveOperation.IDLE)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_OPEN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_valve_entity_without_position(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_client: APIClient,
|
||||||
|
mock_esphome_device: Callable[
|
||||||
|
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
|
||||||
|
Awaitable[MockESPHomeDevice],
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test a generic valve entity without position or stop."""
|
||||||
|
entity_info = [
|
||||||
|
ValveInfo(
|
||||||
|
object_id="myvalve",
|
||||||
|
key=1,
|
||||||
|
name="my valve",
|
||||||
|
unique_id="my_valve",
|
||||||
|
supports_position=False,
|
||||||
|
supports_stop=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
states = [
|
||||||
|
ValveState(
|
||||||
|
key=1,
|
||||||
|
position=0.5,
|
||||||
|
current_operation=ValveOperation.IS_OPENING,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
user_service = []
|
||||||
|
mock_device = await mock_esphome_device(
|
||||||
|
mock_client=mock_client,
|
||||||
|
entity_info=entity_info,
|
||||||
|
user_service=user_service,
|
||||||
|
states=states,
|
||||||
|
)
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_OPENING
|
||||||
|
assert ATTR_CURRENT_POSITION not in state.attributes
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_CLOSE_VALVE,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, position=0.0)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
VALVE_DOMAIN,
|
||||||
|
SERVICE_OPEN_VALVE,
|
||||||
|
{ATTR_ENTITY_ID: "valve.test_myvalve"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_client.valve_command.assert_has_calls([call(key=1, position=1.0)])
|
||||||
|
mock_client.valve_command.reset_mock()
|
||||||
|
|
||||||
|
mock_device.set_state(
|
||||||
|
ValveState(key=1, position=0.0, current_operation=ValveOperation.IDLE)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("valve.test_myvalve")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_CLOSED
|
Loading…
x
Reference in New Issue
Block a user