Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adds support opening closing valve entity #249

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Callable

from homeassistant.components.climate import HVACMode
from homeassistant.const import STATE_ON
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
from homeassistant.const import STATE_ON, STATE_OPEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import condition
import homeassistant.util.dt as dt_util
Expand Down Expand Up @@ -55,15 +56,26 @@ def __init__(

self._hvac_action_reason = HVACActionReason.NONE

@property
def _is_valve(self) -> bool:
state = self.hass.states.get(self.entity_id)
domain = state.domain if state else None
return domain == VALVE_DOMAIN

@property
def hvac_action_reason(self) -> HVACActionReason:
return self._hvac_action_reason

@property
def is_active(self) -> bool:
"""If the toggleable hvac device is currently active."""
on_state = STATE_OPEN if self._is_valve else STATE_ON

_LOGGER.debug(
"Checking if device is active: %s, on_state: %s", self.entity_id, on_state
)
if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_ON
self.entity_id, on_state
):
return True
return False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
import logging

from homeassistant.components.climate import HVACMode
from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN, ValveEntityFeature
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_CLOSED,
STATE_OFF,
STATE_ON,
STATE_OPEN,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
Expand Down Expand Up @@ -104,18 +109,40 @@ def set_context(self, context: Context):
def get_device_ids(self) -> list[str]:
return [self.entity_id]

@property
def _entity_state(self) -> str:
return self.hass.states.get(self.entity_id)

@property
def _is_valve(self) -> bool:
domain = self._entity_state.domain if self._entity_state else None
return domain == VALVE_DOMAIN

@property
def _entity_features(self) -> int:
return (
self.hass.states.get(self.entity_id).attributes.get("supported_features")
if self._entity_state
else 0
)

@property
def _supports_open_valve(self) -> bool:
_LOGGER.debug("entity_features: %s", self._entity_features)
return self._is_valve and self._entity_features & ValveEntityFeature.OPEN

@property
def _supports_close_valve(self) -> bool:
return self._is_valve and self._entity_features & ValveEntityFeature.CLOSE

@property
def target_env_attr(self) -> str:
return self._target_env_attr

@property
def is_active(self) -> bool:
"""If the toggleable hvac device is currently active."""
if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_ON
):
return True
return False
return self.hvac_controller.is_active

def is_below_target_env_attr(self) -> bool:
"""is too cold?"""
Expand Down Expand Up @@ -184,12 +211,12 @@ async def async_control_hvac(self, time=None, force=False):
"%s - async_control_hvac - is device active: %s, %s, strategy: %s, is opening open: %s",
self._device_type,
self.entity_id,
self.is_active,
self.hvac_controller.is_active,
self.strategy,
any_opeing_open,
)

if self.is_active:
if self.hvac_controller.is_active:
await self.hvac_controller.async_control_device_when_on(
self.strategy,
any_opeing_open,
Expand All @@ -216,35 +243,103 @@ async def async_on_startup(self):

async def _async_check_device_initial_state(self) -> None:
"""Prevent the device from keep running if HVACMode.OFF."""
if self._hvac_mode == HVACMode.OFF and self.is_active:
if self._hvac_mode == HVACMode.OFF and self.hvac_controller.is_active:
_LOGGER.warning(
"The climate mode is OFF, but the switch device is ON. Turning off device %s",
self.entity_id,
)
await self.async_turn_off()

async def async_turn_on(self):
_LOGGER.info(
"%s. Turning on or opening entity %s",
self.__class__.__name__,
self.entity_id,
)

if self.entity_id is None:
return

if self._supports_open_valve:
await self._async_open_valve_entity()
else:
await self._async_turn_on_entity()

async def async_turn_off(self):
_LOGGER.info(
"%s. Turning off or closing entity %s",
self.__class__.__name__,
self.entity_id,
)
if self.entity_id is None:
return

if self._supports_close_valve:
await self._async_close_valve_entity()
else:
await self._async_turn_off_entity()

async def _async_turn_on_entity(self) -> None:
"""Turn on the entity."""
_LOGGER.info(
"%s. Turning on entity %s", self.__class__.__name__, self.entity_id
)

_LOGGER.debug("entity_id: %s", self.entity_id)
_LOGGER.debug(
"is_state: %s", self.hass.states.is_state(self.entity_id, STATE_OFF)
)

if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_OFF
):

data = {ATTR_ENTITY_ID: self.entity_id}
await self.hass.services.async_call(
HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context
HA_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: self.entity_id},
context=self._context,
)

async def async_turn_off(self):
async def _async_turn_off_entity(self) -> None:
"""Turn off the entity."""
_LOGGER.info(
"%s. Turning off entity %s", self.__class__.__name__, self.entity_id
)

if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_ON
):
await self.hass.services.async_call(
HA_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: self.entity_id},
context=self._context,
)

async def _async_open_valve_entity(self) -> None:
"""Open the entity."""
_LOGGER.info("%s. Opening entity %s", self.__class__.__name__, self.entity_id)

if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_CLOSED
):
await self.hass.services.async_call(
HA_DOMAIN,
SERVICE_OPEN_VALVE,
{ATTR_ENTITY_ID: self.entity_id},
context=self._context,
)

data = {ATTR_ENTITY_ID: self.entity_id}
async def _async_close_valve_entity(self) -> None:
"""Close the entity."""
_LOGGER.info("%s. Closing entity %s", self.__class__.__name__, self.entity_id)

if self.entity_id is not None and self.hass.states.is_state(
self.entity_id, STATE_OPEN
):
await self.hass.services.async_call(
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
HA_DOMAIN,
SERVICE_CLOSE_VALVE,
{ATTR_ENTITY_ID: self.entity_id},
context=self._context,
)
53 changes: 52 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
STATE_ON,
HVACMode,
)
from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, UnitOfTemperature
from homeassistant.components.valve import ValveEntityFeature
from homeassistant.const import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_CLOSED,
STATE_OPEN,
UnitOfTemperature,
)
import homeassistant.core as ha
from homeassistant.core import HomeAssistant, callback
from homeassistant.setup import async_setup_component
Expand Down Expand Up @@ -61,6 +70,28 @@ async def setup_comp_heat(hass: HomeAssistant) -> None:
await hass.async_block_till_done()


@pytest.fixture
async def setup_comp_heat_valve(hass: HomeAssistant) -> None:
"""Initialize components."""
hass.config.units = METRIC_SYSTEM
assert await async_setup_component(
hass,
CLIMATE,
{
"climate": {
"platform": DOMAIN,
"name": "test",
"cold_tolerance": 2,
"hot_tolerance": 4,
"heater": common.ENT_VALVE,
"target_sensor": common.ENT_SENSOR,
"initial_hvac_mode": HVACMode.HEAT,
}
},
)
await hass.async_block_till_done()


@pytest.fixture
async def setup_comp_heat_safety_delay(hass: HomeAssistant) -> None:
"""Initialize components."""
Expand Down Expand Up @@ -1050,6 +1081,26 @@ def log_call(call) -> None:
return calls


def setup_valve(hass: HomeAssistant, is_open: bool) -> None:
"""Set up the test switch."""
hass.states.async_set(
common.ENT_VALVE,
STATE_OPEN if is_open else STATE_CLOSED,
{"supported_features": ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE},
)
calls = []

@callback
def log_call(call) -> None:
"""Log service calls."""
calls.append(call)

hass.services.async_register(ha.DOMAIN, SERVICE_OPEN_VALVE, log_call)
hass.services.async_register(ha.DOMAIN, SERVICE_CLOSE_VALVE, log_call)

return calls


def setup_heat_pump_cooling_status(hass: HomeAssistant, is_on: bool) -> None:
"""Set up the test switch."""
hass.states.async_set(
Expand Down
1 change: 1 addition & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
ENT_OPENING_SENSOR = "input_number.opneing1"
ENT_HUMIDITY_SENSOR = "input_number.humidity"
ENT_SWITCH = "switch.test"
ENT_VALVE = "valve.test"
ENT_HEATER = "input_boolean.test"
ENT_COOLER = "input_boolean.test_cooler"
ENT_FAN = "switch.test_fan"
Expand Down
35 changes: 35 additions & 0 deletions tests/test_heater_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from homeassistant.components.climate.const import ATTR_PRESET_MODE, DOMAIN as CLIMATE
from homeassistant.const import (
ATTR_TEMPERATURE,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_RELOAD,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Expand Down Expand Up @@ -67,9 +69,11 @@
setup_comp_heat_floor_opening_sensor,
setup_comp_heat_presets,
setup_comp_heat_safety_delay,
setup_comp_heat_valve,
setup_floor_sensor,
setup_sensor,
setup_switch,
setup_valve,
)

COLD_TOLERANCE = 0.5
Expand Down Expand Up @@ -665,6 +669,7 @@ async def test_set_target_temp_heater_on(
setup_sensor(hass, 25)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 30)

assert len(calls) == 1
call = calls[0]
assert call.domain == HASS_DOMAIN
Expand All @@ -687,6 +692,36 @@ async def test_set_target_temp_heater_off(
assert call.data["entity_id"] == common.ENT_SWITCH


async def test_set_target_temp_heater_valve_open(
hass: HomeAssistant, setup_comp_heat_valve # noqa: F811
) -> None:
"""Test if target temperature turn heater on."""
calls = setup_valve(hass, False)
setup_sensor(hass, 25)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 30)
assert len(calls) == 1
call = calls[0]
assert call.domain == HASS_DOMAIN
assert call.service == SERVICE_OPEN_VALVE
assert call.data["entity_id"] == common.ENT_VALVE


async def test_set_target_temp_heater_valve_close(
hass: HomeAssistant, setup_comp_heat_valve # noqa: F811
) -> None:
"""Test if target temperature turn heater off."""
calls = setup_valve(hass, True)
setup_sensor(hass, 30)
await hass.async_block_till_done()
await common.async_set_temperature(hass, 25)
assert len(calls) == 2
call = calls[0]
assert call.domain == HASS_DOMAIN
assert call.service == SERVICE_CLOSE_VALVE
assert call.data["entity_id"] == common.ENT_VALVE


async def test_temp_change_heater_on_within_tolerance(
hass: HomeAssistant, setup_comp_heat # noqa: F811
) -> None:
Expand Down