From 14b5ff94043e08790b47c06d2a28cc751f4f1d9e Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 25 Jul 2024 13:10:03 +0000 Subject: [PATCH] feat: provides power level and power pwercent Fixes #196 --- .../dual_smart_thermostat/climate.py | 41 +++- .../dual_smart_thermostat/const.py | 8 + .../hvac_device/cooler_device.py | 5 + .../hvac_device/dryer_device.py | 5 + .../hvac_device/fan_device.py | 30 ++- .../hvac_device/generic_hvac_device.py | 8 +- .../hvac_device/heat_pump_device.py | 29 ++- .../hvac_device/heater_device.py | 5 + .../hvac_device/hvac_device_factory.py | 18 +- .../managers/environment_manager.py | 15 ++ .../managers/feature_manager.py | 17 +- .../managers/hvac_power_manager.py | 184 ++++++++++++++++++ 12 files changed, 341 insertions(+), 24 deletions(-) create mode 100644 custom_components/dual_smart_thermostat/managers/hvac_power_manager.py diff --git a/custom_components/dual_smart_thermostat/climate.py b/custom_components/dual_smart_thermostat/climate.py index a4e7e88..5560048 100644 --- a/custom_components/dual_smart_thermostat/climate.py +++ b/custom_components/dual_smart_thermostat/climate.py @@ -71,6 +71,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningHvacModeScope, OpeningManager, @@ -82,6 +85,8 @@ from . import DOMAIN, PLATFORMS from .const import ( ATTR_HVAC_ACTION_REASON, + ATTR_HVAC_POWER_LEVEL, + ATTR_HVAC_POWER_PERCENT, ATTR_PREV_HUMIDITY, ATTR_PREV_TARGET, ATTR_PREV_TARGET_HIGH, @@ -106,6 +111,10 @@ CONF_HEATER, CONF_HOT_TOLERANCE, CONF_HUMIDITY_SENSOR, + CONF_HVAC_POWER_LEVELS, + CONF_HVAC_POWER_MAX, + CONF_HVAC_POWER_MIN, + CONF_HVAC_POWER_TOLERANCE, CONF_INITIAL_HVAC_MODE, CONF_KEEP_ALIVE, CONF_MAX_FLOOR_TEMP, @@ -178,7 +187,7 @@ ), } -HYGROSTAT_SCHEMA = { +DEHUMIDIFYER_SCHEMA = { vol.Optional(CONF_DRYER): cv.entity_id, vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id, vol.Optional(CONF_MIN_HUMIDITY): vol.Coerce(float), @@ -192,6 +201,13 @@ vol.Optional(CONF_HEAT_PUMP_COOLING): cv.entity_id, } +HVAC_POWER_SCHEMA = { + vol.Optional(CONF_HVAC_POWER_LEVELS): vol.Coerce(int), + vol.Optional(CONF_HVAC_POWER_MIN): vol.Coerce(int), + vol.Optional(CONF_HVAC_POWER_MAX): vol.Coerce(int), + vol.Optional(CONF_HVAC_POWER_TOLERANCE): vol.Coerce(float), +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HEATER): cv.entity_id, @@ -241,10 +257,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(FAN_MODE_SCHEMA) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HYGROSTAT_SCHEMA) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(DEHUMIDIFYER_SCHEMA) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HEAT_PUMP_SCHEMA) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(HVAC_POWER_SCHEMA) + # Add the old presets schema to avoid breaking change PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(v): vol.Coerce(float) for (k, v) in CONF_PRESETS_OLD.items()} @@ -281,13 +299,17 @@ async def async_setup_platform( config, ) + hvac_power_manager = HvacPowerManager(hass, config, environment_manager) + feature_manager = FeatureManager(hass, config, environment_manager) preset_manager = PresetManager(hass, config, environment_manager, feature_manager) device_factory = HVACDeviceFactory(hass, config, feature_manager) - hvac_device = device_factory.create_device(environment_manager, opening_manager) + hvac_device = device_factory.create_device( + environment_manager, opening_manager, hvac_power_manager + ) async_add_entities( [ @@ -308,6 +330,7 @@ async def async_setup_platform( environment_manager, opening_manager, feature_manager, + hvac_power_manager, ) ] ) @@ -362,6 +385,7 @@ def __init__( environment_manager: EnvironmentManager, opening_manager: OpeningManager, feature_manager: FeatureManager, + power_manager: HvacPowerManager, ) -> None: """Initialize the thermostat.""" self._attr_name = name @@ -383,6 +407,9 @@ def __init__( # opening manager self.openings = opening_manager + # power manager + self.power_manager = power_manager + # sensors self.sensor_entity_id = sensor_entity_id self.sensor_floor_entity_id = sensor_floor_entity_id @@ -774,6 +801,14 @@ def extra_state_attributes(self) -> dict: self._hvac_action_reason or HVACActionReason.NONE ) + # TODO: set these only if configured to avoid unnecessary DB writes + if self.features.is_configured_for_hvac_power_levels: + _LOGGER.debug( + "Setting HVAC Power Level: %s", self.power_manager.hvac_power_level + ) + attributes[ATTR_HVAC_POWER_LEVEL] = self.power_manager.hvac_power_level + attributes[ATTR_HVAC_POWER_PERCENT] = self.power_manager.hvac_power_percent + _LOGGER.debug("Extra state attributes: %s", attributes) return attributes diff --git a/custom_components/dual_smart_thermostat/const.py b/custom_components/dual_smart_thermostat/const.py index dfde082..523bd4f 100644 --- a/custom_components/dual_smart_thermostat/const.py +++ b/custom_components/dual_smart_thermostat/const.py @@ -64,6 +64,14 @@ CONF_HEAT_COOL_MODE = "heat_cool_mode" CONF_HEAT_PUMP_COOLING = "heat_pump_cooling" +# HVAC power levels +CONF_HVAC_POWER_LEVELS = "hvac_power_levels" +CONF_HVAC_POWER_MIN = "hvac_power_min" +CONF_HVAC_POWER_MAX = "hvac_power_max" +CONF_HVAC_POWER_TOLERANCE = "hvac_power_tolerance" +ATTR_HVAC_POWER_LEVEL = "hvac_power_level" +ATTR_HVAC_POWER_PERCENT = "hvac_power_percent" + ATTR_PREV_TARGET = "prev_target_temp" ATTR_PREV_TARGET_LOW = "prev_target_temp_low" ATTR_PREV_TARGET_HIGH = "prev_target_temp_high" diff --git a/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py b/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py index 27fcab8..1258dbb 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/cooler_device.py @@ -19,6 +19,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -39,6 +42,7 @@ def __init__( environment: EnvironmentManager, openings: OpeningManager, features: FeatureManager, + hvac_power: HvacPowerManager, ) -> None: super().__init__( hass, @@ -48,6 +52,7 @@ def __init__( environment, openings, features, + hvac_power, hvac_goal=HvacGoal.LOWER, ) diff --git a/custom_components/dual_smart_thermostat/hvac_device/dryer_device.py b/custom_components/dual_smart_thermostat/hvac_device/dryer_device.py index 10954da..0856c9b 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/dryer_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/dryer_device.py @@ -19,6 +19,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -41,6 +44,7 @@ def __init__( environment: EnvironmentManager, openings: OpeningManager, features: FeatureManager, + hvac_power: HvacPowerManager, ) -> None: super().__init__( hass, @@ -50,6 +54,7 @@ def __init__( environment, openings, features, + hvac_power, hvac_goal=HvacGoal.LOWER, ) diff --git a/custom_components/dual_smart_thermostat/hvac_device/fan_device.py b/custom_components/dual_smart_thermostat/hvac_device/fan_device.py index 54f9089..ba07f83 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/fan_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/fan_device.py @@ -1,10 +1,24 @@ +from datetime import timedelta import logging from homeassistant.components.climate import HVACAction, HVACMode +from homeassistant.core import HomeAssistant from custom_components.dual_smart_thermostat.hvac_device.cooler_device import ( CoolerDevice, ) +from custom_components.dual_smart_thermostat.managers.environment_manager import ( + EnvironmentManager, +) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) +from custom_components.dual_smart_thermostat.managers.opening_manager import ( + OpeningManager, +) _LOGGER = logging.getLogger(__name__) @@ -16,13 +30,14 @@ class FanDevice(CoolerDevice): def __init__( self, - hass, - entity_id, - min_cycle_duration, - initial_hvac_mode, - environment, - openings, - features, + hass: HomeAssistant, + entity_id: str, + min_cycle_duration: timedelta, + initial_hvac_mode: HVACMode, + environment: EnvironmentManager, + openings: OpeningManager, + features: FeatureManager, + hvac_power: HvacPowerManager, ) -> None: super().__init__( hass, @@ -32,6 +47,7 @@ def __init__( environment, openings, features, + hvac_power, ) if self.features.is_fan_uses_outside_air: diff --git a/custom_components/dual_smart_thermostat/hvac_device/generic_hvac_device.py b/custom_components/dual_smart_thermostat/hvac_device/generic_hvac_device.py index f6cc846..a82c10d 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/generic_hvac_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/generic_hvac_device.py @@ -43,6 +43,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -67,6 +70,7 @@ def __init__( environment: EnvironmentManager, openings: OpeningManager, features: FeatureManager, + hvac_power: HvacPowerManager, hvac_goal: HvacGoal, ) -> None: super().__init__(hass, environment, openings) @@ -77,6 +81,7 @@ def __init__( self.hvac_goal = hvac_goal self.features = features + self.hvac_power = hvac_power self.entity_id = entity_id self.min_cycle_duration = min_cycle_duration @@ -222,16 +227,15 @@ async def async_control_hvac(self, time=None, force=False): any_opeing_open, time, ) - # await self._async_control_when_active(time) else: await self.hvac_controller.async_control_device_when_off( self.strategy, any_opeing_open, time, ) - # await self._async_control_when_inactive(time) self._hvac_action_reason = self.hvac_controller.hvac_action_reason + self.hvac_power.update_hvac_power(self.strategy, self.target_env_attr) async def async_on_startup(self): entity_state = self.hass.states.get(self.entity_id) diff --git a/custom_components/dual_smart_thermostat/hvac_device/heat_pump_device.py b/custom_components/dual_smart_thermostat/hvac_device/heat_pump_device.py index 4381079..83e7f14 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/heat_pump_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/heat_pump_device.py @@ -1,8 +1,9 @@ +from datetime import timedelta import logging from homeassistant.components.climate import HVACMode from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from custom_components.dual_smart_thermostat.hvac_controller.cooler_controller import ( CoolerHvacController, @@ -21,8 +22,18 @@ merge_hvac_modes, ) from custom_components.dual_smart_thermostat.managers.environment_manager import ( + EnvironmentManager, TargetTemperatures, ) +from custom_components.dual_smart_thermostat.managers.feature_manager import ( + FeatureManager, +) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) +from custom_components.dual_smart_thermostat.managers.opening_manager import ( + OpeningManager, +) _LOGGER = logging.getLogger(__name__) @@ -33,13 +44,14 @@ class HeatPumpDevice(GenericHVACDevice): def __init__( self, - hass, - entity_id, - min_cycle_duration, - initial_hvac_mode, - environment, - openings, - features, + hass: HomeAssistant, + entity_id: str, + min_cycle_duration: timedelta, + initial_hvac_mode: HVACMode, + environment: EnvironmentManager, + openings: OpeningManager, + features: FeatureManager, + hvac_power: HvacPowerManager, ) -> None: super().__init__( hass, @@ -49,6 +61,7 @@ def __init__( environment, openings, features, + hvac_power, hvac_goal=HvacGoal.RAISE, # will not take effect as we will define new controllers ) diff --git a/custom_components/dual_smart_thermostat/hvac_device/heater_device.py b/custom_components/dual_smart_thermostat/hvac_device/heater_device.py index 3aff426..8b298c3 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/heater_device.py +++ b/custom_components/dual_smart_thermostat/hvac_device/heater_device.py @@ -19,6 +19,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -39,6 +42,7 @@ def __init__( environment: EnvironmentManager, openings: OpeningManager, features: FeatureManager, + hvac_power: HvacPowerManager, ) -> None: super().__init__( hass, @@ -48,6 +52,7 @@ def __init__( environment, openings, features, + hvac_power, hvac_goal=HvacGoal.RAISE, ) diff --git a/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py b/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py index 67c5087..7f9f303 100644 --- a/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py +++ b/custom_components/dual_smart_thermostat/hvac_device/hvac_device_factory.py @@ -49,6 +49,9 @@ from custom_components.dual_smart_thermostat.managers.feature_manager import ( FeatureManager, ) +from custom_components.dual_smart_thermostat.managers.hvac_power_manager import ( + HvacPowerManager, +) from custom_components.dual_smart_thermostat.managers.opening_manager import ( OpeningManager, ) @@ -92,7 +95,10 @@ def __init__( self._initial_hvac_mode = config.get(CONF_INITIAL_HVAC_MODE) def create_device( - self, environment: EnvironmentManager, openings: OpeningManager + self, + environment: EnvironmentManager, + openings: OpeningManager, + hvac_power: HvacPowerManager, ) -> ControlableHVACDevice: dryer_device = None @@ -110,6 +116,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if self._features.is_configured_for_fan_only_mode: @@ -121,6 +128,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if self._features.is_configured_for_fan_mode: @@ -132,6 +140,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if self._features.is_configured_for_aux_heating_mode: @@ -143,6 +152,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if self._features.is_configured_for_dual_mode: @@ -155,7 +165,7 @@ def create_device( or self._cooler_entity_id is not None ): cooler_device = self._create_cooler_device( - environment, openings, cooler_entity_id, fan_device + environment, openings, hvac_power, cooler_entity_id, fan_device ) if self._features.is_configured_for_heat_pump_mode: @@ -167,6 +177,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if ( @@ -184,6 +195,7 @@ def create_device( environment, openings, self._features, + hvac_power, ) if aux_heater_device and heater_device: @@ -256,6 +268,7 @@ def _create_cooler_device( self, environment: EnvironmentManager, openings: OpeningManager, + hvac_power: HvacPowerManager, cooler_entitiy_id: str, fan_device: FanDevice | None, ) -> CoolerDevice: @@ -268,6 +281,7 @@ def _create_cooler_device( environment, openings, self._features, + hvac_power, ) if fan_device: diff --git a/custom_components/dual_smart_thermostat/managers/environment_manager.py b/custom_components/dual_smart_thermostat/managers/environment_manager.py index a937d92..710e962 100644 --- a/custom_components/dual_smart_thermostat/managers/environment_manager.py +++ b/custom_components/dual_smart_thermostat/managers/environment_manager.py @@ -1,4 +1,5 @@ from datetime import timedelta +import enum import logging import math from typing import Any @@ -58,6 +59,13 @@ def __init__(self, temperature: float, temp_high: float, temp_low: float) -> Non self.temp_low = temp_low +class EnvironmentAttributeType(enum.StrEnum): + """Enum for environment attributes.""" + + TEMPERATURE = "temperature" + HUMIDITY = "humidity" + + class EnvironmentManager(StateManager): """Class to manage the temperatures of the thermostat.""" @@ -239,6 +247,13 @@ def target_humidity(self, humidity: float) -> None: def cur_humidity(self) -> float: return self._cur_humidity + def get_env_attr_type(self, attr: str) -> EnvironmentAttributeType: + return ( + EnvironmentAttributeType.HUMIDITY + if attr == "_target_humidity" + else EnvironmentAttributeType.TEMPERATURE + ) + def set_temperature_range_from_saved(self) -> None: self.target_temp_low = self.saved_target_temp_low self.target_temp_high = self.saved_target_temp_high diff --git a/custom_components/dual_smart_thermostat/managers/feature_manager.py b/custom_components/dual_smart_thermostat/managers/feature_manager.py index 6952ddc..0b9a345 100644 --- a/custom_components/dual_smart_thermostat/managers/feature_manager.py +++ b/custom_components/dual_smart_thermostat/managers/feature_manager.py @@ -7,7 +7,7 @@ HVACMode, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import State +from homeassistant.core import HomeAssistant, State from homeassistant.helpers.typing import ConfigType from custom_components.dual_smart_thermostat.const import ( @@ -26,6 +26,8 @@ CONF_HEAT_PUMP_COOLING, CONF_HEATER, CONF_HUMIDITY_SENSOR, + CONF_HVAC_POWER_LEVELS, + CONF_HVAC_POWER_TOLERANCE, ) from custom_components.dual_smart_thermostat.managers.environment_manager import ( EnvironmentManager, @@ -38,7 +40,7 @@ class FeatureManager(StateManager): def __init__( - self, hass, config: ConfigType, environment: EnvironmentManager + self, hass: HomeAssistant, config: ConfigType, environment: EnvironmentManager ) -> None: self.hass = hass self.environment = environment @@ -68,6 +70,9 @@ def __init__( ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + self._hvac_power_levels = config.get(CONF_HVAC_POWER_LEVELS) + self._hvac_power_tolerance = config.get(CONF_HVAC_POWER_TOLERANCE) + @property def heat_pump_cooling_entity_id(self) -> str: return self._heat_pump_cooling_entity_id @@ -180,6 +185,14 @@ def is_configured_for_heat_pump_mode(self) -> bool: """Determines if the heat pump cooling is configured.""" return self._heat_pump_cooling_entity_id is not None + @property + def is_configured_for_hvac_power_levels(self) -> bool: + """Determines if the HVAC power levels are configured.""" + return ( + self._hvac_power_levels is not None + or self._hvac_power_tolerance is not None + ) + def set_support_flags( self, presets: dict[str, Any], diff --git a/custom_components/dual_smart_thermostat/managers/hvac_power_manager.py b/custom_components/dual_smart_thermostat/managers/hvac_power_manager.py new file mode 100644 index 0000000..2641ce7 --- /dev/null +++ b/custom_components/dual_smart_thermostat/managers/hvac_power_manager.py @@ -0,0 +1,184 @@ +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from custom_components.dual_smart_thermostat.const import ( + CONF_HVAC_POWER_LEVELS, + CONF_HVAC_POWER_MAX, + CONF_HVAC_POWER_MIN, + CONF_HVAC_POWER_TOLERANCE, +) +from custom_components.dual_smart_thermostat.hvac_controller.hvac_controller import ( + HvacEnvStrategy, +) +from custom_components.dual_smart_thermostat.managers.environment_manager import ( + EnvironmentAttributeType, + EnvironmentManager, +) + +_LOGGER = logging.getLogger(__name__) + + +class HvacPowerManager: + + _hvac_power_level = 0 + _hvac_power_percent = 0 + + def __init__( + self, hass: HomeAssistant, config: ConfigType, environment: EnvironmentManager + ) -> None: + self.hass = hass + self.config = config + self.environment = environment + + self._hvac_power_levels = config.get(CONF_HVAC_POWER_LEVELS) or 5 + + hvac_power_min = config.get(CONF_HVAC_POWER_MIN) + hvac_power_max = config.get(CONF_HVAC_POWER_MAX) + + self._hvac_power_tolerance = config.get(CONF_HVAC_POWER_TOLERANCE) + + # don't allow min to be greater than max + # TODO: cover these cases with tests + if ( + hvac_power_min is not None + and hvac_power_max is not None + and hvac_power_min > hvac_power_max + ): + raise ValueError( + f"{CONF_HVAC_POWER_MIN} must be less than {CONF_HVAC_POWER_MAX}" + ) + + # don't allow min to be greater than power levels + if hvac_power_min is not None and hvac_power_min > self._hvac_power_levels: + raise ValueError( + f"{CONF_HVAC_POWER_MIN} must be less than or equal to {CONF_HVAC_POWER_LEVELS}" + ) + + # don't allow max to be greater than power levels + if hvac_power_max is not None and hvac_power_max > self._hvac_power_levels: + raise ValueError( + f"{CONF_HVAC_POWER_MAX} must be less than or equal to {CONF_HVAC_POWER_LEVELS}" + ) + + self._hvac_power_min = hvac_power_min or 1 + self._hvac_power_max = hvac_power_max or self._hvac_power_levels + + self._hvac_power_min_percent = ( + self._hvac_power_min / self._hvac_power_levels * 100 + ) + self._hvac_power_max_percent = ( + self._hvac_power_max / self._hvac_power_levels * 100 + ) + + @property + def hvac_power_level(self) -> int: + return self._hvac_power_level + + @property + def hvac_power_percent(self) -> int: + return self._hvac_power_percent + + def _get_hvac_power_tolerance(self, is_temperature: bool) -> int: + """handles the default value for the hvac power tolerance + based on the unit system and the environment attribute""" + + is_imperial = self.hass.config.units is US_CUSTOMARY_SYSTEM + default_imperial_tolerance = 33 + default_metric_tolerance = 1 + + default_temperatue_tolerance = ( + default_imperial_tolerance if is_imperial else default_metric_tolerance + ) + + default_tolerance = ( + default_temperatue_tolerance if is_temperature else default_metric_tolerance + ) + + return ( + self._hvac_power_tolerance + if self._hvac_power_tolerance is not None + else default_tolerance + ) + + def update_hvac_power( + self, strategy: HvacEnvStrategy, target_env_attr: str + ) -> None: + """updates the hvac power level based on the strategy and the target environment attribute""" + + _LOGGER.debug("Updating hvac power") + + goal_reached = strategy.hvac_goal_reached + goal_not_reached = strategy.hvac_goal_not_reached + + if goal_reached: + _LOGGER.debug("Updating hvac power because goal reached") + self._hvac_power_level = 0 + self._hvac_power_percent = 0 + return + + if goal_not_reached: + _LOGGER.debug("Updating hvac power because goal not reached") + self._calculate_power(target_env_attr) + + def _calculate_power(self, target_env_attr: str): + env_attribute_type = self.environment.get_env_attr_type(target_env_attr) + is_temperature = env_attribute_type is EnvironmentAttributeType.TEMPERATURE + + match env_attribute_type: + case EnvironmentAttributeType.TEMPERATURE: + curr_env_value = self.environment.cur_temp + case EnvironmentAttributeType.HUMIDITY: + curr_env_value = self.environment.cur_humidity + case _: + raise ValueError( + f"Unsupported environment attribute type: {env_attribute_type}" + ) + + target_env_value = getattr(self.environment, target_env_attr) + + power_tolerance = self._get_hvac_power_tolerance(is_temperature) + + step_value = power_tolerance / self._hvac_power_levels + + env_difference = abs(curr_env_value - target_env_value) + + _LOGGER.debug("step value: %s", step_value) + _LOGGER.debug("env difference: %s", env_difference) + + self._hvac_power_level = self._calculate_power_level(step_value, env_difference) + self._hvac_power_percent = self._calculate_power_percent( + env_difference, power_tolerance + ) + + def _calculate_power_level(self, step_value: float, env_difference: float) -> int: + # calculate the power level + # should increase or decrease the power level based on the difference between the current and target temperature + _LOGGER.debug("Calculating hvac power level") + + calculated_power_level = round(env_difference / step_value, 0) + + return max( + self._hvac_power_min, min(calculated_power_level, self._hvac_power_max) + ) + + def _calculate_power_percent( + self, env_difference: float, power_tolerance: float + ) -> int: + # calculate the power percent + # should increase or decrease the power level based on the difference between the current and target temperature + _LOGGER.debug("Calculating hvac power percent") + + calculated_power_level = env_difference / power_tolerance + + return max( + self._hvac_power_min_percent, + min( + calculated_power_level * 100, + self._hvac_power_max_percent, + ), + ) + + # TODO: apply preset (verify min/max)