Integration tests ok

This commit is contained in:
Jean-Marc Collin
2023-10-27 21:52:39 +00:00
parent fe694048af
commit 8dee0dd8d5
8 changed files with 228 additions and 87 deletions

View File

@@ -44,6 +44,12 @@ input_number:
step: 10
icon: mdi:flash
unit_of_measurement: kW
fake_valve1:
name: The valve 1
min: 0
max: 100
icon: mdi:pipe-valve
unit_of_measurement: percentage
input_boolean:
# input_boolean to simulate the windows entity. Only for development environment.

View File

@@ -852,12 +852,14 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
Need to be one of CURRENT_HVAC_*.
"""
if self._hvac_mode == HVACMode.OFF:
return HVACAction.OFF
if not self.is_device_active:
return HVACAction.IDLE
if self._hvac_mode == HVACMode.COOL:
return HVACAction.COOLING
return HVACAction.HEATING
action = HVACAction.OFF
elif not self.is_device_active:
action = HVACAction.IDLE
elif self._hvac_mode == HVACMode.COOL:
action = HVACAction.COOLING
else:
action = HVACAction.HEATING
return action
@property
def target_temperature(self):
@@ -2329,11 +2331,11 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
self._total_energy,
)
# TODO implement this with overrides
def update_custom_attributes(self):
"""Update the custom extra attributes for the entity"""
self._attr_extra_state_attributes: dict(str, str) = {
"hvac_action": self.hvac_action,
"hvac_mode": self.hvac_mode,
"preset_mode": self.preset_mode,
"type": self._thermostat_type,
@@ -2392,56 +2394,6 @@ class BaseThermostat(ClimateEntity, RestoreEntity):
"power_sensor_entity_id": self._power_sensor_entity_id,
"max_power_sensor_entity_id": self._max_power_sensor_entity_id,
}
if self.is_over_climate:
self._attr_extra_state_attributes[
"underlying_climate_0"
] = self._underlyings[0].entity_id
self._attr_extra_state_attributes["underlying_climate_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_climate_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_climate_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes[
"start_hvac_action_date"
] = self._underlying_climate_start_hvac_action_date
else:
self._attr_extra_state_attributes[
"underlying_switch_1"
] = self._underlyings[0].entity_id
self._attr_extra_state_attributes["underlying_switch_2"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_switch_3"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_switch_4"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes[
"on_percent"
] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[
"on_time_sec"
] = self._prop_algorithm.on_time_sec
self._attr_extra_state_attributes[
"off_time_sec"
] = self._prop_algorithm.off_time_sec
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
@callback
def async_registry_entry_updated(self):

View File

@@ -1,3 +1,4 @@
# pylint: disable=unused-argument
""" Implements the VersatileThermostat sensors component """
import logging
import math
@@ -22,6 +23,7 @@ from .const import (
CONF_PROP_FUNCTION,
PROPORTIONAL_FUNCTION_TPI,
CONF_THERMOSTAT_SWITCH,
CONF_THERMOSTAT_VALVE,
CONF_THERMOSTAT_TYPE,
)
@@ -50,7 +52,7 @@ async def async_setup_entry(
]
if entry.data.get(CONF_DEVICE_POWER):
entities.append(EnergySensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_SWITCH:
if entry.data.get(CONF_THERMOSTAT_TYPE) in [CONF_THERMOSTAT_SWITCH, CONF_THERMOSTAT_VALVE]:
entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI:
@@ -58,6 +60,9 @@ async def async_setup_entry(
entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
entities.append(OffTimeSensor(hass, unique_id, name, entry.data))
if entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE:
entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))
async_add_entities(entities, True)
@@ -224,6 +229,47 @@ class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Return the suggested number of decimal digits for display."""
return 1
class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on percent sensor which exposes the on_percent in a cycle"""
def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
"""Initialize the energy sensor"""
super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
self._attr_name = "Vave open percent"
self._attr_unique_id = f"{self._device_name}_valve_open_percent"
@callback
async def async_my_climate_changed(self, event: Event = None):
"""Called when my climate have change"""
_LOGGER.debug("%s - climate state change", self._attr_unique_id)
old_state = self._attr_native_value
self._attr_native_value = self.my_climate.valve_open_percent
if old_state != self._attr_native_value:
self.async_write_ha_state()
return
@property
def icon(self) -> str | None:
return "mdi:pipe-valve"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.POWER_FACTOR
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return PERCENTAGE
@property
def suggested_display_precision(self) -> int | None:
"""Return the suggested number of decimal digits for display."""
return 0
class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
"""Representation of a on time sensor which exposes the on_time_sec in a cycle"""

View File

@@ -97,3 +97,37 @@ class ThermostatOverClimate(BaseThermostat):
interval=timedelta(minutes=self._cycle_min),
)
)
def update_custom_attributes(self):
""" Custom attributes """
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_climate"] = self.is_over_climate
self._attr_extra_state_attributes["start_hvac_action_date"] = (
self._underlying_climate_start_hvac_action_date)
self._attr_extra_state_attributes["underlying_climate_0"] = (
self._underlyings[0].entity_id)
self._attr_extra_state_attributes["underlying_climate_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_climate_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_climate_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self.update_custom_attributes()
self.async_write_ha_state()

View File

@@ -2,6 +2,7 @@
import logging
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.components.climate import HVACMode
from .const import (
CONF_HEATER,
@@ -66,3 +67,55 @@ class ThermostatOverSwitch(BaseThermostat):
)
self.hass.create_task(self.async_control_heating())
def update_custom_attributes(self):
""" Custom attributes """
super().update_custom_attributes()
self._attr_extra_state_attributes["is_over_switch"] = self.is_over_switch
self._attr_extra_state_attributes["underlying_switch_0"] = (
self._underlyings[0].entity_id)
self._attr_extra_state_attributes["underlying_switch_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_switch_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_switch_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes[
"on_percent"
] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[
"on_time_sec"
] = self._prop_algorithm.on_time_sec
self._attr_extra_state_attributes[
"off_time_sec"
] = self._prop_algorithm.off_time_sec
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
)
self.update_custom_attributes()
self.async_write_ha_state()

View File

@@ -81,7 +81,6 @@ class ThermostatOverValve(BaseThermostat):
)
)
@callback
async def _async_valve_changed(self, event):
"""Handle unerdlying valve state changes.
@@ -252,3 +251,61 @@ class ThermostatOverValve(BaseThermostat):
changes = True
await end_climate_changed(changes)
def update_custom_attributes(self):
""" Custom attributes """
super().update_custom_attributes()
self._attr_extra_state_attributes["valve_open_percent"] = self.valve_open_percent
self._attr_extra_state_attributes["is_over_valve"] = self.is_over_valve
self._attr_extra_state_attributes["underlying_valve_0"] = (
self._underlyings[0].entity_id)
self._attr_extra_state_attributes["underlying_valve_1"] = (
self._underlyings[1].entity_id if len(self._underlyings) > 1 else None
)
self._attr_extra_state_attributes["underlying_valve_2"] = (
self._underlyings[2].entity_id if len(self._underlyings) > 2 else None
)
self._attr_extra_state_attributes["underlying_valve_3"] = (
self._underlyings[3].entity_id if len(self._underlyings) > 3 else None
)
self._attr_extra_state_attributes[
"on_percent"
] = self._prop_algorithm.on_percent
self._attr_extra_state_attributes[
"on_time_sec"
] = self._prop_algorithm.on_time_sec
self._attr_extra_state_attributes[
"off_time_sec"
] = self._prop_algorithm.off_time_sec
self._attr_extra_state_attributes["cycle_min"] = self._cycle_min
self._attr_extra_state_attributes["function"] = self._proportional_function
self._attr_extra_state_attributes["tpi_coef_int"] = self._tpi_coef_int
self._attr_extra_state_attributes["tpi_coef_ext"] = self._tpi_coef_ext
self.async_write_ha_state()
_LOGGER.debug(
"%s - Calling update_custom_attributes: %s",
self,
self._attr_extra_state_attributes,
)
def recalculate(self):
"""A utility function to force the calculation of a the algo and
update the custom attributes and write the state
"""
_LOGGER.debug("%s - recalculate all", self)
self._prop_algorithm.calculate(
self._target_temp,
self._cur_temp,
self._cur_ext_temp,
self._hvac_mode == HVACMode.COOL,
)
for under in self._underlyings:
under.set_valve_open_percent(
self._prop_algorithm.on_percent
)
self.update_custom_attributes()
self.async_write_ha_state()

View File

@@ -662,9 +662,10 @@ class UnderlyingValve(UnderlyingEntity):
""" Send the percent open to the underlying valve """
# This may fails if called after shutdown
try:
data = { "value": self._percent_open }
data = { ATTR_ENTITY_ID: self._entity_id, "value": self._percent_open }
domain = self._entity_id.split('.')[0]
await self._hass.services.async_call(
HA_DOMAIN,
domain,
SERVICE_SET_VALUE,
data,
)
@@ -697,7 +698,10 @@ class UnderlyingValve(UnderlyingEntity):
def is_device_active(self):
"""If the toggleable device is currently active."""
try:
return float(self._hass.states.get(self._entity_id)) > 0
return self._percent_open > 0
# To test if real device is open but this is causing some side effect
# because the activation can be deferred -
# or float(self._hass.states.get(self._entity_id).state) > 0
except Exception: # pylint: disable=broad-exception-caught
return False
@@ -711,20 +715,22 @@ class UnderlyingValve(UnderlyingEntity):
force=False,
):
"""We use this function to change the on_percent"""
if force:
await self.send_percent_open()
def set_valve_open_percent(self, percent):
""" Update the valve open percent """
caped_val = self._thermostat.valve_open_percent
if not force and self._percent_open == caped_val:
if self._percent_open == caped_val:
# No changes
return
self._percent_open = caped_val
# Send the new command to valve via a service call
try:
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
await self.send_percent_open()
except ServiceNotFound as err:
_LOGGER.error(err)
_LOGGER.info("%s - Setting valve ouverture percent to %s", self, self._percent_open)
# Send the change to the valve, in background
self._hass.create_task(self.send_percent_open())
def remove_entity(self):
"""Remove the entity after stopping its cycle"""

View File

@@ -17,7 +17,6 @@ from custom_components.versatile_thermostat.thermostat_valve import ThermostatOv
from .commons import * # pylint: disable=wildcard-import, unused-wildcard-import
@pytest.mark.parametrize("expected_lingering_tasks", [True])
@pytest.mark.parametrize("expected_lingering_timers", [True])
async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_state): # pylint: disable=unused-argument
"""Test the normal full start of a thermostat in thermostat_over_switch type"""
@@ -160,7 +159,7 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st
) as mock_send_event, patch(
"homeassistant.core.ServiceRegistry.async_call"
) as mock_service_call, patch(
"homeassistant.core.StateMachine.get", return_value=90
"homeassistant.core.StateMachine.get", return_value=State(entity_id="number.mock_valve", state="90")
):
# Change temperature
event_timestamp = now - timedelta(minutes=10)
@@ -174,20 +173,8 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st
assert mock_service_call.call_count == 2
mock_service_call.assert_has_calls([
call.async_call(
HA_DOMAIN,
SERVICE_SET_VALUE,
{
"value": 90
}
),
call.async_call(
HA_DOMAIN,
SERVICE_SET_VALUE,
{
"value": 98
}
)
call.async_call('number', 'set_value', {'entity_id': 'number.mock_valve', 'value': 90}),
call.async_call('number', 'set_value', {'entity_id': 'number.mock_valve', 'value': 98})
])
assert mock_send_event.call_count == 0
@@ -238,8 +225,8 @@ async def test_over_valve_full_start(hass: HomeAssistant, skip_hass_states_is_st
assert entity.presence_state == STATE_OFF # pylint: disable=protected-access
assert entity.valve_open_percent == 10
assert entity.target_temperature == 17.1 # eco_away
assert entity.is_device_active is False
assert entity.hvac_action == HVACAction.IDLE
assert entity.is_device_active is True
assert entity.hvac_action == HVACAction.HEATING
# Open a window
with patch(