Skip to content

Commit

Permalink
fix: adds support opening closing valve entity
Browse files Browse the repository at this point in the history
Fixes #240
  • Loading branch information
= committed Jul 24, 2024
1 parent 309753e commit 1c6f685
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 17 deletions.
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

0 comments on commit 1c6f685

Please sign in to comment.