diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 583b2ab..47c7bfd 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -91,6 +91,48 @@ input_number: icon: mdi:thermostat unit_of_measurement: °C mode: box + fake_offset_calibration1: + name: Sonoff offset calibration 1 + min: -12 + max: 12 + icon: mdi:tune + unit_of_measurement: °C + mode: box + fake_opening_degree1: + name: Sonoff Opening degree 1 + min: 0 + max: 100 + icon: mdi:valve-open + unit_of_measurement: "%" + mode: box + fake_closing_degree1: + name: Sonoff Closing degree 1 + min: 0 + max: 100 + icon: mdi:valve-closed + unit_of_measurement: "%" + mode: box + fake_offset_calibration2: + name: Sonoff offset calibration 2 + min: -12 + max: 12 + icon: mdi:tune + unit_of_measurement: °C + mode: box + fake_opening_degree2: + name: Sonoff Opening degree 2 + min: 0 + max: 100 + icon: mdi:valve-open + unit_of_measurement: "%" + mode: box + fake_closing_degree2: + name: Sonoff Closing degree 2 + min: 0 + max: 100 + icon: mdi:valve-closed + unit_of_measurement: "%" + mode: box input_boolean: # input_boolean to simulate the windows entity. Only for development environment. @@ -142,6 +184,12 @@ input_boolean: fake_presence_sensor1: name: Presence Sensor 1 icon: mdi:home + fake_valve_sonoff_trvzb1: + name: Valve Sonoff TRVZB1 + icon: mdi:valve + fake_valve_sonoff_trvzb2: + name: Valve Sonoff TRVZB2 + icon: mdi:valve climate: - platform: generic_thermostat @@ -185,6 +233,16 @@ climate: name: Underlying thermostat9 heater: input_boolean.fake_heater_switch3 target_sensor: input_number.fake_temperature_sensor1 + - platform: generic_thermostat + name: Underlying Sonoff TRVZB1 + heater: input_boolean.fake_valve_sonoff_trvzb1 + target_sensor: input_number.fake_temperature_sensor1 + ac_mode: false + - platform: generic_thermostat + name: Underlying Sonoff TRVZB2 + heater: input_boolean.fake_valve_sonoff_trvzb2 + target_sensor: input_number.fake_temperature_sensor1 + ac_mode: false input_datetime: fake_last_seen: @@ -238,14 +296,14 @@ switch: friendly_name: "Pilote chauffage SDB RDC" value_template: "{{ is_state_attr('switch_seche_serviettes_sdb_rdc', 'sensor_state', 'on') }}" turn_on: - service: select.select_option + action: select.select_option data: option: comfort target: entity_id: select.seche_serviettes_sdb_rdc_cable_outlet_mode turn_off: - service: select.select_option + action: select.select_option data: option: comfort-2 target: diff --git a/custom_components/versatile_thermostat/base_thermostat.py b/custom_components/versatile_thermostat/base_thermostat.py index 02fadb8..d95feaf 100644 --- a/custom_components/versatile_thermostat/base_thermostat.py +++ b/custom_components/versatile_thermostat/base_thermostat.py @@ -87,10 +87,6 @@ def get_tz(hass: HomeAssistant): return dt_util.get_time_zone(hass.config.time_zone) -_LOGGER_ENERGY = logging.getLogger( - "custom_components.versatile_thermostat.energy_debug" -) - class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): """Representation of a base class for all Versatile Thermostat device.""" @@ -205,7 +201,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._attr_translation_key = "versatile_thermostat" self._total_energy = None - _LOGGER_ENERGY.debug("%s - _init_ resetting energy to None", self) + _LOGGER.debug("%s - _init_ resetting energy to None", self) # because energy of climate is calculated in the thermostat we have to keep that here and not in underlying entity self._underlying_climate_start_hvac_action_date = None @@ -478,7 +474,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): self._presence_state = None self._total_energy = None - _LOGGER_ENERGY.debug("%s - post_init_ resetting energy to None", self) + _LOGGER.debug("%s - post_init_ resetting energy to None", self) # Read the parameter from configuration.yaml if it exists short_ema_params = DEFAULT_SHORT_EMA_PARAMS @@ -596,7 +592,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): async def async_will_remove_from_hass(self): """Try to force backup of entity""" - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - force write before remove. Energy is %s", self, self.total_energy ) # Force dump in background @@ -823,7 +819,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): old_total_energy = old_state.attributes.get(ATTR_TOTAL_ENERGY) self._total_energy = old_total_energy if old_total_energy is not None else 0 - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - get_my_previous_state restored energy is %s", self, self._total_energy, @@ -841,7 +837,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "No previously saved temperature, setting to %s", self._target_temp ) self._total_energy = 0 - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - get_my_previous_state no previous state energy is %s", self, self._total_energy, @@ -2668,7 +2664,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): "hvac_off_reason": self.hvac_off_reason, } - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - update_custom_attributes saved energy is %s", self, self.total_energy, @@ -2677,7 +2673,7 @@ class BaseThermostat(ClimateEntity, RestoreEntity, Generic[T]): @overrides def async_write_ha_state(self): """overrides to have log""" - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - async_write_ha_state written state energy is %s", self, self._total_energy, diff --git a/custom_components/versatile_thermostat/thermostat_climate.py b/custom_components/versatile_thermostat/thermostat_climate.py index 66faeb1..c903473 100644 --- a/custom_components/versatile_thermostat/thermostat_climate.py +++ b/custom_components/versatile_thermostat/thermostat_climate.py @@ -1,5 +1,5 @@ # pylint: disable=line-too-long, too-many-lines -""" A climate over switch classe """ +""" A climate over climate classe """ import logging from datetime import timedelta, datetime @@ -23,7 +23,7 @@ from .pi_algorithm import PITemperatureRegulator from .const import * # pylint: disable=wildcard-import, unused-wildcard-import from .vtherm_api import VersatileThermostatAPI -from .underlyings import UnderlyingClimate, UnderlyingSonoffTRVZB +from .underlyings import UnderlyingClimate from .auto_start_stop_algorithm import ( AutoStartStopDetectionAlgorithm, AUTO_START_STOP_ACTION_OFF, @@ -31,10 +31,6 @@ from .auto_start_stop_algorithm import ( ) _LOGGER = logging.getLogger(__name__) -_LOGGER_ENERGY = logging.getLogger( - "custom_components.versatile_thermostat.energy_debug" -) - HVAC_ACTION_ON = [ # pylint: disable=invalid-name HVACAction.COOLING, @@ -104,25 +100,12 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): super().post_init(config_entry) - for idx, climate in enumerate(config_entry.get(CONF_UNDERLYING_LIST)): - if config_entry.get(CONF_SONOFF_TRZB_MODE) is True: - offset = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)[idx] - opening = config_entry.get(CONF_OPENING_DEGREE_LIST)[idx] - closing = config_entry.get(CONF_CLOSING_DEGREE_LIST)[idx] - under = UnderlyingSonoffTRVZB( - hass=self._hass, - thermostat=self, - climate_entity_id=climate, - offset_calibration=offset, - opening_degree=opening, - closing_degree=closing, - ) - else: - under = UnderlyingClimate( - hass=self._hass, - thermostat=self, - climate_entity_id=climate, - ) + for climate in config_entry.get(CONF_UNDERLYING_LIST): + under = UnderlyingClimate( + hass=self._hass, + thermostat=self, + climate_entity_id=climate, + ) self._underlyings.append(under) self.choose_auto_regulation_mode( @@ -618,14 +601,14 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): if self._total_energy is None: self._total_energy = added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - incremente_energy set energy is %s", self, self._total_energy, ) else: self._total_energy += added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - incremente_energy incremented energy is %s", self, self._total_energy, @@ -1131,7 +1114,7 @@ class ThermostatOverClimate(BaseThermostat[UnderlyingClimate]): def current_humidity(self) -> float | None: """Return the humidity.""" if self.underlying_entity(0): - return self.underlying_entity(0).humidity + return self.underlying_entity(0).current_humidity return None diff --git a/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py b/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py new file mode 100644 index 0000000..22bacf2 --- /dev/null +++ b/custom_components/versatile_thermostat/thermostat_sonoff_trvzb.py @@ -0,0 +1,142 @@ +# pylint: disable=line-too-long, too-many-lines +""" A climate over Sonoff TRVZB classe """ + +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.components.climate import HVACMode + +from .underlyings import UnderlyingSonoffTRVZB + +# from .commons import NowClass, round_to_nearest +from .base_thermostat import BaseThermostat, ConfigData +from .prop_algorithm import PropAlgorithm + +from .const import * # pylint: disable=wildcard-import, unused-wildcard-import + +# from .vtherm_api import VersatileThermostatAPI + +_LOGGER = logging.getLogger(__name__) + + +class ThermostatOverSonoffTRVZB(BaseThermostat[UnderlyingSonoffTRVZB]): + """This class represent a VTherm over a Sonoff TRVZB climate""" + + _entity_component_unrecorded_attributes = ( + BaseThermostat._entity_component_unrecorded_attributes.union( + frozenset( + { + "is_over_climate", + "is_over_sonoff_trvzb", + "underlying_entities", + "on_time_sec", + "off_time_sec", + "cycle_min", + "function", + "tpi_coef_int", + "tpi_coef_ext", + "power_percent", + } + ) + ) + ) + + def __init__( + self, hass: HomeAssistant, unique_id: str, name: str, entry_infos: ConfigData + ): + """Initialize the ThermostatOverSonoffTRVZB class""" + _LOGGER.debug("%s - creating a ThermostatOverSonoffTRVZB VTherm", name) + super().__init__(hass, unique_id, name, entry_infos) + + @overrides + def post_init(self, config_entry: ConfigData): + """Initialize the Thermostat""" + + super().post_init(config_entry) + + for idx, _ in enumerate(config_entry.get(CONF_UNDERLYING_LIST)): + offset = config_entry.get(CONF_OFFSET_CALIBRATION_LIST)[idx] + opening = config_entry.get(CONF_OPENING_DEGREE_LIST)[idx] + closing = config_entry.get(CONF_CLOSING_DEGREE_LIST)[idx] + under = UnderlyingSonoffTRVZB( + hass=self._hass, + thermostat=self, + offset_calibration_entity_id=offset, + opening_degree_entity_id=opening, + closing_degree_entity_id=closing, + ) + self._underlyings.append(under) + + # Initialization of the TPI algo + self._prop_algorithm = PropAlgorithm( + self._proportional_function, + self._tpi_coef_int, + self._tpi_coef_ext, + self._cycle_min, + self._minimal_activation_delay, + self.name, + ) + + @overrides + def update_custom_attributes(self): + """Custom attributes""" + super().update_custom_attributes() + + under0: UnderlyingSonoffTRVZB = self._underlyings[0] + self._attr_extra_state_attributes["is_over_sonoff_trvzb"] = ( + self.is_over_sonoff_trvzb + ) + self._attr_extra_state_attributes["keep_alive_sec"] = under0.keep_alive_sec + + self._attr_extra_state_attributes["underlying_entities"] = [ + underlying.entity_id for underlying in self._underlyings + ] + + self._attr_extra_state_attributes["on_percent"] = ( + self._prop_algorithm.on_percent + ) + self._attr_extra_state_attributes["power_percent"] = self.power_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, + ) + + @overrides + 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 or HVACMode.OFF, + ) + self.update_custom_attributes() + + @property + def is_over_sonoff_trvzb(self) -> bool: + """True if the Thermostat is over_sonoff_trvzb""" + return True + + @property + def power_percent(self) -> float | None: + """Get the current on_percent value""" + if self._prop_algorithm: + return round(self._prop_algorithm.on_percent * 100, 0) + else: + return None diff --git a/custom_components/versatile_thermostat/thermostat_switch.py b/custom_components/versatile_thermostat/thermostat_switch.py index 4e07b8d..328656f 100644 --- a/custom_components/versatile_thermostat/thermostat_switch.py +++ b/custom_components/versatile_thermostat/thermostat_switch.py @@ -21,9 +21,6 @@ from .underlyings import UnderlyingSwitch from .prop_algorithm import PropAlgorithm _LOGGER = logging.getLogger(__name__) -_LOGGER_ENERGY = logging.getLogger( - "custom_components.versatile_thermostat.energy_debug" -) class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): """Representation of a base class for a Versatile Thermostat over a switch.""" @@ -185,14 +182,14 @@ class ThermostatOverSwitch(BaseThermostat[UnderlyingSwitch]): if self._total_energy is None: self._total_energy = added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - incremente_energy set energy is %s", self, self._total_energy, ) else: self._total_energy += added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - incremente_energy increment energy is %s", self, self._total_energy, diff --git a/custom_components/versatile_thermostat/thermostat_valve.py b/custom_components/versatile_thermostat/thermostat_valve.py index d9324d0..2ff6ccc 100644 --- a/custom_components/versatile_thermostat/thermostat_valve.py +++ b/custom_components/versatile_thermostat/thermostat_valve.py @@ -25,9 +25,6 @@ from .const import ( from .underlyings import UnderlyingValve _LOGGER = logging.getLogger(__name__) -_LOGGER_ENERGY = logging.getLogger( - "custom_components.versatile_thermostat.energy_debug" -) class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=abstract-method """Representation of a class for a Versatile Thermostat over a Valve""" @@ -267,14 +264,14 @@ class ThermostatOverValve(BaseThermostat[UnderlyingValve]): # pylint: disable=a if self._total_energy is None: self._total_energy = added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - incremente_energy set energy is %s", self, self._total_energy, ) else: self._total_energy += added_energy - _LOGGER_ENERGY.debug( + _LOGGER.debug( "%s - get_my_previous_state increment energy is %s", self, self._total_energy, diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index 437b15e..1598c10 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -53,6 +53,9 @@ class UnderlyingEntityType(StrEnum): # a valve VALVE = "valve" + # a Sonoff TRVZB + SONOFF_TRVZB = "sonoff_trvzb" + class UnderlyingEntity: """Represent a underlying device which could be a switch or a climate""" @@ -713,6 +716,13 @@ class UnderlyingClimate(UnderlyingEntity): return [] return self._underlying_climate.hvac_modes + @property + def current_humidity(self) -> float | None: + """Get the humidity""" + if not self.is_initialized: + return None + return self._underlying_climate.current_humidity + @property def fan_modes(self) -> list[str]: """Get the fan_modes""" @@ -851,7 +861,7 @@ class UnderlyingValve(UnderlyingEntity): def __init__( self, hass: HomeAssistant, thermostat: Any, valve_entity_id: str ) -> None: - """Initialize the underlying switch""" + """Initialize the underlying valve""" super().__init__( hass=hass, @@ -988,3 +998,40 @@ class UnderlyingValve(UnderlyingEntity): def remove_entity(self): """Remove the entity after stopping its cycle""" self._cancel_cycle() + + +class UnderlyingSonoffTRVZB(UnderlyingValve): + """A specific underlying class for Sonoff TRVZB TRV""" + + _offset_calibration_entity_id: str + _opening_degree_entity_id: str + _closing_degree_entity_id: str + + def __init__( + self, + hass: HomeAssistant, + thermostat: Any, + offset_calibration_entity_id: str, + opening_degree_entity_id: str, + closing_degree_entity_id: str, + ) -> None: + """Initialize the underlying Sonoff TRV""" + super().__init__(hass, thermostat, opening_degree_entity_id) + self._offset_calibration_entity_id = offset_calibration_entity_id + self._opening_degree_entity_id = opening_degree_entity_id + self._closing_degree_entity_id = closing_degree_entity_id + + @property + def offset_calibration_entity_id(self) -> str: + """The offset_calibration_entity_id""" + return self._offset_calibration_entity_id + + @property + def opening_degree_entity_id(self) -> str: + """The offset_calibration_entity_id""" + return self._opening_degree_entity_id + + @property + def closing_degree_entity_id(self) -> str: + """The offset_calibration_entity_id""" + return self._closing_degree_entity_id