From 88d76ff0909c9dcbf1fa1c6bd727d330bc9a37b6 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Fri, 12 Apr 2024 01:51:37 +0200 Subject: [PATCH 1/8] Completely refactored and streamlined code. Fixed problem with using number as modes (i.e fan mode). Fixed issue with 'Already running' warning as script was executed twice, when using both template and script. Added input validation. Added debug messages for easier troubleshooting. Warning: Some config parameters and variables names were renamed to align with HA attributes name, please check Readme if you get error. --- README.md | 85 +- custom_components/climate_template/climate.py | 1218 +++++++++++------ 2 files changed, 844 insertions(+), 459 deletions(-) diff --git a/README.md b/README.md index b887573..5cb8bef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![HACS Badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) [![License](https://img.shields.io/github/license/litinoveweedle/hass-template-climate?style=for-the-badge)](https://github.com/litinoveweedle/hass-template-climate/blob/main/LICENSE) [![Latest Release](https://img.shields.io/github/v/release/litinoveweedle/hass-template-climate?style=for-the-badge)](https://github.com/litinoveweedle/hass-template-climate/releases) -[![Size](https://img.badgesize.io/https:/github.com/litinoveweedle/hass-template-climate/releases/latest/download/climate_template.zip?style=for-the-badge)](https://github.com/litinoveweedle/hass-template-climate/releases) [![Code style](https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge)](https://github.com/psf/black) The `climate_template` platform creates climate devices that combine integrations and provides the ability to run scripts or invoke services for each of the `set_*` commands of a climate entity. @@ -16,42 +15,48 @@ All configuration variables are optional. The climate device will work in optimi If you do not define a `template` or its corresponding `action` the climate device will not have that attribute, e.g. either `swing_mode_template` or `set_swing_mode` must be defined for the climate to have a swing mode. -| Name | Type | Description | Default Value | -|----------------------------------| ------------------------------------------------------------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------| -| name | `string` | The name of the climate device. | "Template Climate" | -| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | -| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | -| max_action | `positive_int` | positive number from 1 | 1 | -| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | -| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | -| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | -| | | | | -| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | -| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | -| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | -| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device. | | -| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device. | | -| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | -| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | -| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | -| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | -| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | -| | | | | -| set_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `temperature`, `target_temp_high`, `target_temp_low` and `hvac_mode` variables. | | -| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | -| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | -| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | -| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | -| | | | | -| modes | `list` | A list of supported hvac modes. Needs to be a subset of the default values. | ["auto", "off", "cool", "heat", "dry", "fan_only"] | -| fan_modes | `list` | A list of supported fan modes. | ["auto", "low", "medium", "high"] | -| preset_modes | `list` | A list of supported preset modes. | ["activity", "away", "boost", "comfort", "eco", "home", "sleep"] | -| swing_modes | `list` | A list of supported swing modes. | ["on", "off"] | -| | | | | -| min_temp | `float` | Minimum set point available. | 7 | -| max_temp | `float` | Maximum set point available. | 35 | -| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | -| temp_step | `float` | Step size for temperature set point. | 1 | +| Name | Type | Description | Default Value | +| -------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| name | `string` | The name of the climate device. | "Template Climate" | +| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | +| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | +| max_action | `positive_int` | positive number from 1 | 1 | +| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | +| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | +| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | +| | | | | +| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | +| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | +| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | +| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device with the HEAT_COOL hvac_mode. | | +| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device with the HEAT_COOL hvac_mode. | | +| target_humidity | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target humidity of the climate device. | | +| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | +| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | +| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | +| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | +| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | +| | | | | +| set_target_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temperature` variable. | | +| set_target_temperature_low | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_low` variable. | | +| set_target_temperature_high | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_high` variable. | | +| set_target_humidity | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set humidity command. Can use `target_humidity` variable. | | +| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | +| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | +| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | +| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | +| update_timeout | `positive_int` | If both attribute `_template` (to get current atribute state) and `set_` attribute action (to set the attribute state) are used, then this value is maximum time in seconds between the set action is trigerred and attribute update is received via template. If there is no update received before this timeout, climate_template entity attribute will not updated. If not set, this functionality is disabled by default and attribute will be always updated. In such case integration could execute `set_` action twice (first time on action trigger and second time on `_template` update), leading in the worst case to two paralel action exectution and ceoresponding HA warnings. | 0 | +| | | | | +| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_mode`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. | ["off", "auto", "cool", "heat", "dry", "fan_only"] | +| fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. | ["off", "auto", "low", "medium", "high"] | +| preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. | ["activity", "away", "boost", "comfort", "eco", "home", "sleep"] | +| swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. | ["off", "on"] | +| min_temperature | `float` | Minimum temperature set point available. | 7 | +| max_temperature | `float` | Maximum temperature set point available. | 35 | +| min_humidity | `float` | Minimum humidity set point available. | 30 | +| max_humidity | `float` | Maximum humidity set point available. | 99 | +| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | +| temp_step | `float` | Step size for temperature set point. | 1 | ## Example Configuration @@ -59,8 +64,8 @@ If you do not define a `template` or its corresponding `action` the climate devi climate: - platform: climate_template name: Bedroom Aircon - - modes: + + hvac_modes: - "auto" - "dry" - "off" @@ -91,7 +96,7 @@ climate: # send the climates current state to esphome. - service: esphome.bedroom_node_aircon_state data: - temperature: "{{ state_attr('climate.bedroom_aircon', 'temperature') | int }}" + temperature: "{{ state_attr('climate.bedroom_aircon', 'target_temperature') | int }}" operation_mode: "{{ states('climate.bedroom_aircon') }}" fan_mode: "{{ state_attr('climate.bedroom_aircon', 'fan_mode') }}" swing_mode: "{{ is_state_attr('climate.bedroom_aircon', 'swing_mode', 'on') }}" diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index da2cc89..81ee2a6 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -4,7 +4,7 @@ import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.core import Context +from homeassistant.core import Context, callback from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -13,18 +13,25 @@ from homeassistant.components.climate.const import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, ATTR_HVAC_MODE, - ATTR_FAN_MODE, ATTR_PRESET_MODE, + ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_CURRENT_TEMPERATURE, ATTR_CURRENT_HUMIDITY, + ATTR_HVAC_ACTION, ATTR_HUMIDITY, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH, SWING_OFF, + SWING_ON, PRESET_ACTIVITY, PRESET_AWAY, PRESET_BOOST, @@ -32,15 +39,13 @@ PRESET_ECO, PRESET_HOME, PRESET_SLEEP, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, HVACMode, HVACAction, ) from homeassistant.components.template.const import CONF_AVAILABILITY_TEMPLATE from homeassistant.components.template.template_entity import TemplateEntity +from homeassistant.exceptions import TemplateError from homeassistant.const import ( - STATE_ON, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -57,31 +62,37 @@ from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, HomeAssistantType + _LOGGER = logging.getLogger(__name__) -CONF_FAN_MODE_LIST = "fan_modes" +CONF_HVAC_MODE_LIST = "hvac_modes" CONF_PRESET_MODE_LIST = "preset_modes" -CONF_MODE_LIST = "modes" +CONF_FAN_MODE_LIST = "fan_modes" CONF_SWING_MODE_LIST = "swing_modes" -CONF_TEMP_MIN = "min_temp" -CONF_TEMP_MAX = "max_temp" +CONF_TEMPERATURE_MIN = "min_temp" +CONF_TEMPERATURE_MAX = "max_temp" +CONF_HUMIDITY_MIN = "min_humidity" +CONF_HUMIDITY_MAX = "max_humidity" CONF_PRECISION = "precision" -CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_TEMP_STEP = "temp_step" +CONF_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template" CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" -CONF_TARGET_HUMIDITY_TEMPLATE = "target_humidity_template" CONF_TARGET_TEMPERATURE_TEMPLATE = "target_temperature_template" CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE = "target_temperature_high_template" CONF_TARGET_TEMPERATURE_LOW_TEMPLATE = "target_temperature_low_template" +CONF_TARGET_HUMIDITY_TEMPLATE = "target_humidity_template" CONF_HVAC_MODE_TEMPLATE = "hvac_mode_template" CONF_FAN_MODE_TEMPLATE = "fan_mode_template" CONF_PRESET_MODE_TEMPLATE = "preset_mode_template" CONF_SWING_MODE_TEMPLATE = "swing_mode_template" CONF_HVAC_ACTION_TEMPLATE = "hvac_action_template" -CONF_SET_TEMPERATURE_ACTION = "set_temperature" -CONF_SET_HUMIDITY_ACTION = "set_humidity" +CONF_UPDATE_TIMEOUT = "update_timeout" +CONF_SET_TEMPERATURE_ACTION = "set_target_temperature" +CONF_SET_TEMPERATURE_LOW_ACTION = "set_target_temperature_low" +CONF_SET_TEMPERATURE_HIGH_ACTION = "set_target_temperature_high" +CONF_SET_HUMIDITY_ACTION = "set_target_humidity" CONF_SET_HVAC_MODE_ACTION = "set_hvac_mode" CONF_SET_FAN_MODE_ACTION = "set_fan_mode" CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" @@ -89,11 +100,14 @@ CONF_MODE_ACTION = "mode_action" CONF_MAX_ACTION = "max_action" -CONF_CLIMATES = "climates" - DEFAULT_NAME = "Template Climate" -DEFAULT_TEMP = 21 -DEFAULT_PRECISION = 1.0 +DEFAULT_TEMPERATURE = 21 +DEFAULT_HUMIDITY = 50 +DEFAULT_HVAC_MODE = HVACMode.OFF +DEFAULT_PRESET_MODE = PRESET_COMFORT +DEFAULT_FAN_MODE = FAN_LOW +DEFAULT_SWING_MODE = SWING_OFF +DEFAULT_TEMP_STEP = 1 DEFAULT_MODE_ACTION = "single" DEFAULT_MAX_ACTION = 1 DOMAIN = "climate_template" @@ -108,7 +122,7 @@ vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template, vol.Optional(CONF_TARGET_HUMIDITY_TEMPLATE): cv.template, vol.Optional(CONF_TARGET_TEMPERATURE_TEMPLATE): cv.template, @@ -120,13 +134,15 @@ vol.Optional(CONF_SWING_MODE_TEMPLATE): cv.template, vol.Optional(CONF_HVAC_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_SET_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_TEMPERATURE_LOW_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_TEMPERATURE_HIGH_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_HUMIDITY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_HVAC_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_FAN_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SWING_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( - CONF_MODE_LIST, + CONF_HVAC_MODE_LIST, default=[ HVACMode.AUTO, HVACMode.OFF, @@ -136,10 +152,6 @@ HVACMode.FAN_ONLY, ], ): cv.ensure_list, - vol.Optional( - CONF_FAN_MODE_LIST, - default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], - ): cv.ensure_list, vol.Optional( CONF_PRESET_MODE_LIST, default=[ @@ -153,14 +165,34 @@ ], ): cv.ensure_list, vol.Optional( - CONF_SWING_MODE_LIST, default=[STATE_ON, HVACMode.OFF] + CONF_FAN_MODE_LIST, + default=[ + FAN_OFF, + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + ], + ): cv.ensure_list, + vol.Optional( + CONF_SWING_MODE_LIST, + default=[ + SWING_OFF, + SWING_ON, + ], ): cv.ensure_list, - vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMPERATURE_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_TEMPERATURE_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce( + float + ), + vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce( + float + ), vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), - vol.Optional(CONF_TEMP_STEP, default=DEFAULT_PRECISION): vol.Coerce(float), + vol.Optional(CONF_TEMP_STEP, default=DEFAULT_TEMP_STEP): vol.Coerce(float), vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -176,6 +208,8 @@ async def async_setup_platform( class TemplateClimate(TemplateEntity, ClimateEntity, RestoreEntity): """A template climate component.""" + _attr_should_poll = False + def __init__(self, hass: HomeAssistantType, config: ConfigType): """Initialize the climate device.""" super().__init__( @@ -185,152 +219,206 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): entity_picture_template=config.get(CONF_ENTITY_PICTURE_TEMPLATE), ) self._config = config - self._attr_unique_id = config.get(CONF_UNIQUE_ID, None) + self._enable_turn_on_off_backwards_compatibility = False + self._attr_name = config[CONF_NAME] + self._attr_entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, config[CONF_NAME], hass=hass + ) + self._attr_unique_id = config.get(CONF_UNIQUE_ID, None) + self._attr_supported_features = 0 + self._attr_unit_of_measurement = hass.config.units.temperature_unit + self._attr_target_temperature_step = config[CONF_TEMP_STEP] self._attr_mode_action = config[CONF_MODE_ACTION] self._attr_max_action = config[CONF_MAX_ACTION] - self._attr_min_temp = config[CONF_TEMP_MIN] - self._attr_max_temp = config[CONF_TEMP_MAX] - self._attr_target_temperature_step = config[CONF_TEMP_STEP] - - self._current_temp = None - self._current_humidity = None - - self._current_fan_mode = FAN_LOW # default optimistic state - self._current_preset_mode = PRESET_COMFORT # default optimistic state - self._current_operation = HVACMode.OFF # default optimistic state - self._current_swing_mode = HVACMode.OFF # default optimistic state - self._target_temp = DEFAULT_TEMP # default optimistic state - self._target_humidity = None - self._attr_target_temperature_high = None - self._attr_target_temperature_low = None - - self._current_temp_template = config.get(CONF_CURRENT_TEMP_TEMPLATE) - self._current_humidity_template = config.get(CONF_CURRENT_HUMIDITY_TEMPLATE) - self._target_temperature_template = config.get(CONF_TARGET_TEMPERATURE_TEMPLATE) - self._target_humidity_template = config.get(CONF_TARGET_HUMIDITY_TEMPLATE) - self._target_temperature_high_template = config.get( - CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE + self._attr_min_temp = config[CONF_TEMPERATURE_MIN] + self._attr_max_temp = config[CONF_TEMPERATURE_MAX] + self._attr_min_humidity = config[CONF_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_HUMIDITY_MAX] + self._attr_hvac_modes = list( + map(lambda item: str(item), config[CONF_HVAC_MODE_LIST]), ) - self._target_temperature_low_template = config.get( - CONF_TARGET_TEMPERATURE_LOW_TEMPLATE + self._attr_fan_modes = list( + map(lambda item: str(item), config[CONF_FAN_MODE_LIST]), ) - self._hvac_mode_template = config.get(CONF_HVAC_MODE_TEMPLATE) - self._fan_mode_template = config.get(CONF_FAN_MODE_TEMPLATE) - self._preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE) - self._swing_mode_template = config.get(CONF_SWING_MODE_TEMPLATE) - self._hvac_action_template = config.get(CONF_HVAC_ACTION_TEMPLATE) - - self._available = True - self._unit_of_measurement = hass.config.units.temperature_unit - self._attr_supported_features = 0 - self._enable_turn_on_off_backwards_compatibility = False - self._last_on_operation = None - - self._attr_hvac_modes = config[CONF_MODE_LIST] - self._attr_fan_modes = config[CONF_FAN_MODE_LIST] - self._attr_preset_modes = config[CONF_PRESET_MODE_LIST] - self._swing_modes_list = config[CONF_SWING_MODE_LIST] - - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, config[CONF_NAME], hass=hass + self._attr_preset_modes = list( + map(lambda item: str(item), config[CONF_PRESET_MODE_LIST]), + ) + self._attr_swing_modes = list( + map(lambda item: str(item), config[CONF_SWING_MODE_LIST]), ) + self._attr_current_temperature = None + self._attr_current_humidity = None + self._attr_hvac_action = None + self._attr_hvac_mode = DEFAULT_HVAC_MODE + self._attr_preset_mode = DEFAULT_PRESET_MODE + self._attr_fan_mode = DEFAULT_FAN_MODE + self._attr_swing_mode = DEFAULT_SWING_MODE + self._attr_target_temperature = DEFAULT_TEMPERATURE + self._attr_target_temperature_low = DEFAULT_TEMPERATURE + self._attr_target_temperature_high = DEFAULT_TEMPERATURE + self._attr_target_humidity = DEFAULT_HUMIDITY + + self._off_mode = {} + self._last_on_mode = {} + + self._template_hvac_action = config.get(CONF_HVAC_ACTION_TEMPLATE) + self._template_hvac_mode = config.get(CONF_HVAC_MODE_TEMPLATE) + self._template_preset_mode = config.get(CONF_PRESET_MODE_TEMPLATE) + self._template_fan_mode = config.get(CONF_FAN_MODE_TEMPLATE) + self._template_swing_mode = config.get(CONF_SWING_MODE_TEMPLATE) + self._template_current_temperature = config.get( + CONF_CURRENT_TEMPERATURE_TEMPLATE + ) + self._template_current_humidity = config.get( + CONF_CURRENT_HUMIDITY_TEMPLATE, + ) + self._template_target_temperature = config.get( + CONF_TARGET_TEMPERATURE_TEMPLATE, + ) + self._template_target_temperature_low = config.get( + CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, + ) + self._template_target_temperature_high = config.get( + CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, + ) + self._template_target_humidity = config.get(CONF_TARGET_HUMIDITY_TEMPLATE) - # set supported features to enable turn_on and turn_off services - if HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) >= 2: - self._attr_supported_features |= ( - ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON - ) - if HVACMode.AUTO in self._attr_hvac_modes: - self._last_on_operation = HVACMode.AUTO - elif len(self._attr_hvac_modes) == 2: - self._last_on_operation = list( - filter(lambda item: item != HVACMode.OFF, self._attr_hvac_modes) - )[0] - else: - self._last_on_operation = HVACMode.OFF - - # set script variables - self._set_hvac_mode_script = None - set_hvac_mode_action = config.get(CONF_SET_HVAC_MODE_ACTION) - if set_hvac_mode_action: - self._set_hvac_mode_script = Script( + # Set script callbacks and supported features flags. + self._script_hvac_mode = None + if action_hvac_mode := config.get(CONF_SET_HVAC_MODE_ACTION): + self._script_hvac_mode = Script( hass, - set_hvac_mode_action, + action_hvac_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) + if ( + HVACMode.OFF in self._attr_hvac_modes + and len(self._attr_hvac_modes) >= 2 + ): + self._off_mode["hvac_mode"] = HVACMode.OFF + self._attr_supported_features |= ( + ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + ) + if HVACMode.AUTO in self._attr_hvac_modes: + self._last_on_mode["hvac_mode"] = HVACMode.AUTO + elif len(self._attr_hvac_modes) == 2: + self._last_on_mode["hvac_mode"] = list( + filter(lambda item: item != HVACMode.OFF, self._attr_hvac_modes) + )[0] + else: + self._last_on_mode["hvac_mode"] = HVACMode.OFF + + self._script_preset_mode = None + if action_preset_mode := config.get(CONF_SET_PRESET_MODE_ACTION): + self._script_preset_mode = Script( + hass, + action_preset_mode, + self._attr_name, + DOMAIN, + ) + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE - self._set_swing_mode_script = None - set_swing_mode_action = config.get(CONF_SET_SWING_MODE_ACTION) - if set_swing_mode_action: - self._set_swing_mode_script = Script( + self._script_fan_mode = None + if action_fan_mode := config.get(CONF_SET_FAN_MODE_ACTION): + self._script_fan_mode = Script( hass, - set_swing_mode_action, + action_fan_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - self._set_fan_mode_script = None - set_fan_mode_action = config.get(CONF_SET_FAN_MODE_ACTION) - if set_fan_mode_action: - self._set_fan_mode_script = Script( + self._script_swing_mode = None + if action_swing_mode := config.get(CONF_SET_SWING_MODE_ACTION): + self._script_swing_mode = Script( hass, - set_fan_mode_action, + action_swing_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE - self._set_preset_mode_script = None - set_preset_mode_action = config.get(CONF_SET_PRESET_MODE_ACTION) - if set_preset_mode_action: - self._set_preset_mode_script = Script( - hass, set_preset_mode_action, self._attr_name, DOMAIN - ) - self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + self._script_set_target_temperature = None + self._script_target_temperature_low = None + self._script_target_temperature_high = None + action_target_temperature = config.get(CONF_SET_TEMPERATURE_ACTION) + action_target_temperature_low = config.get(CONF_SET_TEMPERATURE_LOW_ACTION) + action_target_temperature_high = config.get(CONF_SET_TEMPERATURE_HIGH_ACTION) + if ( + HVACMode.HEAT_COOL in self._attr_hvac_modes + and action_target_temperature_low + and action_target_temperature_high + ): + if not ( + action_target_temperature + and ( + ( + HVACMode.OFF in self._attr_hvac_modes + and len(self._attr_hvac_modes) > 2 + ) + or ( + HVACMode.OFF not in self._attr_hvac_modes + and len(self._attr_hvac_modes) > 1 + ) + ) + ): + # Set action when heat_cool and off are not the only hvac_modes. + action_target_temperature = None + elif action_target_temperature: + action_target_temperature_low = None + action_target_temperature_high = None + else: + action_target_temperature = None + action_target_temperature_low = None + action_target_temperature_high = None - self._set_temperature_script = None - set_temperature_action = config.get(CONF_SET_TEMPERATURE_ACTION) - if set_temperature_action: - self._set_temperature_script = Script( + if action_target_temperature: + self._script_target_temperature = Script( hass, - set_temperature_action, + action_target_temperature, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - if HVACMode.HEAT_COOL in self._attr_hvac_modes: - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) - if HVACMode.OFF in self._attr_hvac_modes: - if len(self._attr_hvac_modes) > 2: - # when heat_cool and off are not the only modes - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE - ) - elif len(self._attr_hvac_modes) > 1: - # when heat_cool is not the only mode - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE - ) - else: - self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + if action_target_temperature_low and action_target_temperature_high: + self._script_target_temperature_low = Script( + hass, + action_target_temperature_low, + self._attr_name, + DOMAIN, + script_mode=self._attr_mode_action, + max_runs=self._attr_max_action, + ) + self._script_target_temperature_high = Script( + hass, + action_target_temperature_high, + self._attr_name, + DOMAIN, + script_mode=self._attr_mode_action, + max_runs=self._attr_max_action, + ) + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) - self._set_humidity_script = None - set_humidity_action = config.get(CONF_SET_HUMIDITY_ACTION) - if set_humidity_action: - self._set_humidity_script = Script( - hass, set_humidity_action, self._attr_name, DOMAIN + self._set_target_humidity_script = None + set_target_humidity_action = config.get(CONF_SET_HUMIDITY_ACTION) + if set_target_humidity_action: + self._set_target_humidity_script = Script( + hass, + set_target_humidity_action, + self._attr_name, + DOMAIN, ) self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY @@ -338,259 +426,540 @@ async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() - # Check If we have an old state + # Check if we have an previous stored state and use it as default state. previous_state = await self.async_get_last_state() if previous_state is not None: - if previous_state.state in self._attr_hvac_modes: - self._current_operation = previous_state.state + _LOGGER.debug( + "Entity %s restoring previously stored attributes.", + self._attr_name, + ) - if temperature := previous_state.attributes.get( - ATTR_TEMPERATURE, DEFAULT_TEMP - ): - self._target_temp = float(temperature) - if temperature_high := previous_state.attributes.get(ATTR_TARGET_TEMP_HIGH): - self._attr_target_temperature_high = float(temperature_high) - if temperature_low := previous_state.attributes.get(ATTR_TARGET_TEMP_LOW): - self._attr_target_temperature_low = float(temperature_low) + if ( + value := self._validate_value( + "hvac_mode", + previous_state.state, + self._attr_hvac_modes, + ) + ) is not None: + self._attr_hvac_mode = value - self._current_fan_mode = previous_state.attributes.get( - ATTR_FAN_MODE, FAN_LOW - ) - self._current_preset_mode = previous_state.attributes.get( - ATTR_PRESET_MODE, PRESET_COMFORT - ) - self._current_swing_mode = previous_state.attributes.get( - ATTR_SWING_MODE, SWING_OFF - ) + if ( + value := self._validate_value( + "preset_mode", + previous_state.attributes.get( + ATTR_PRESET_MODE, + ), + self._attr_preset_modes, + ) + ) is not None: + self._attr_preset_mode = value - if current_temperature := previous_state.attributes.get( - ATTR_CURRENT_TEMPERATURE - ): - self._current_temp = float(current_temperature) + if ( + value := self._validate_value( + "fan_mode", + previous_state.attributes.get( + ATTR_FAN_MODE, + ), + self._attr_fan_modes, + ) + ) is not None: + self._attr_fan_mode = value + + if ( + value := self._validate_value( + "swing_mode", + previous_state.attributes.get( + ATTR_SWING_MODE, + ), + self._attr_swing_modes, + ) + ) is not None: + self._attr_swing_mode = value + + if ( + value := self._validate_value( + "target_temperature", + previous_state.attributes.get( + ATTR_TEMPERATURE, + ), + "temperature_setpoint", + ) + ) is not None: + self._attr_target_temperature = value + + if ( + value := self._validate_value( + "target_temperature", + previous_state.attributes.get( + ATTR_TARGET_TEMP_LOW, + ), + "temperature_setpoint", + ) + ) is not None: + self._attr_target_temperature_low = value + + if ( + value := self._validate_value( + "target_temperature", + previous_state.attributes.get( + ATTR_TARGET_TEMP_HIGH, + ), + "temperature_setpoint", + ) + ) is not None: + self._attr_target_temperature_high = value + + if ( + value := self._validate_value( + "target_humidity", + previous_state.attributes.get( + ATTR_HUMIDITY, + ), + "humidity_setpoint", + ) + ) is not None: + self._target_humidity = value + + if ( + value := self._validate_value( + "current_temperature", + previous_state.attributes.get( + ATTR_CURRENT_TEMPERATURE, + ), + "current_temperature", + ) + ) is not None: + self._attr_current_temperature = value + + if ( + value := self._validate_value( + "current_humidity", + previous_state.attributes.get( + ATTR_CURRENT_HUMIDITY, + ), + "current_humidity", + ) + ) is not None: + self._attr_current_humidity = value + + if ( + value := self._validate_value( + "hvac_action", + previous_state.attributes.get( + ATTR_HVAC_ACTION, + ), + [member.value for member in HVACAction], + ) + ) is not None: + self._attr_hvac_action = value - if humidity := previous_state.attributes.get(ATTR_CURRENT_HUMIDITY): - self._current_humidity = humidity + if (value := previous_state.attributes.get("last_on_mode")) is not None: + for mode in value.keys(): + self._last_on_mode[mode] = value[mode] - if "last_on_operation" in previous_state.attributes: - self._last_on_operation = previous_state.attributes["last_on_operation"] + _LOGGER.debug( + "Entity %s registering templates callbacks.", + self._attr_name, + ) - # register templates - if self._current_temp_template: + # Register templates callback. + if self._template_current_temperature: self.add_template_attribute( - "_current_temp", - self._current_temp_template, + "_current_temperature", + self._template_current_temperature, None, - self._update_current_temp, + self._update_current_temperature, none_on_template_error=True, ) - if self._current_humidity_template: + if self._template_current_humidity: self.add_template_attribute( "_current_humidity", - self._current_humidity_template, + self._template_current_humidity, None, self._update_current_humidity, none_on_template_error=True, ) - if self._target_temperature_template: + if self._template_hvac_action: + self.add_template_attribute( + "_hvac_action", + self._template_hvac_action, + None, + self._update_hvac_action, + none_on_template_error=True, + ) + + if self._template_target_temperature: self.add_template_attribute( - "_target_temp", - self._target_temperature_template, + "_target_temperature", + self._template_target_temperature, None, - self._update_target_temp, + self._update_target_temperature, none_on_template_error=True, ) - if self._target_humidity_template: + if self._template_target_temperature_high: self.add_template_attribute( - "_target_humidity", - self._target_humidity_template, + "_target_temperature_high", + self._template_target_temperature_high, None, - self._update_target_humidity, + self._update_target_temperature_high, none_on_template_error=True, ) - if self._target_temperature_high_template: + if self._template_target_temperature_low: self.add_template_attribute( - "_attr_target_temperature_high", - self._target_temperature_high_template, + "_target_temperature_low", + self._template_target_temperature_low, None, - self._update_target_temp_high, + self._update_target_temperature_low, none_on_template_error=True, ) - if self._target_temperature_low_template: + if self._template_target_humidity: self.add_template_attribute( - "_attr_target_temperature_low", - self._target_temperature_low_template, + "_target_humidity", + self._template_target_humidity, None, - self._update_target_temp_low, + self._update_target_humidity, none_on_template_error=True, ) - if self._hvac_mode_template: + if self._template_hvac_mode: self.add_template_attribute( - "_current_operation", - self._hvac_mode_template, + "_current_hvac_mode", + self._template_hvac_mode, None, self._update_hvac_mode, none_on_template_error=True, ) - if self._preset_mode_template: + + if self._template_preset_mode: self.add_template_attribute( "_current_preset_mode", - self._preset_mode_template, + self._template_preset_mode, None, self._update_preset_mode, none_on_template_error=True, ) - if self._fan_mode_template: + if self._template_fan_mode: self.add_template_attribute( "_current_fan_mode", - self._fan_mode_template, + self._template_fan_mode, None, self._update_fan_mode, none_on_template_error=True, ) - if self._swing_mode_template: + if self._template_swing_mode: self.add_template_attribute( "_current_swing_mode", - self._swing_mode_template, + self._template_swing_mode, None, self._update_swing_mode, none_on_template_error=True, ) - if self._hvac_action_template: - self.add_template_attribute( - "_hvac_action", - self._hvac_action_template, - None, - self._update_hvac_action, - none_on_template_error=True, - ) + _LOGGER.debug( + "Entity %s succesfully registered to homeassistant.", + self._attr_name, + ) - def _update_current_temp(self, temp): - if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._current_temp = float(temp) - except ValueError: - _LOGGER.error("Could not parse temperature from %s", temp) - - def _update_current_humidity(self, humidity): - if humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._current_humidity = float(humidity) - except ValueError: - _LOGGER.error("Could not parse humidity from %s", humidity) - - def _update_target_temp(self, temp): - if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._target_temp = float(temp) - self.hass.async_create_task( - self.async_set_temperature(**{ATTR_TEMPERATURE: self._target_temp}) - ) - except ValueError: - _LOGGER.error("Could not parse temperature from %s", temp) - - def _update_target_humidity(self, humidity): - if humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._target_humidity = float(humidity) - self.hass.async_create_task( - self.async_set_humidity(**{ATTR_HUMIDITY: self._target_humidity}) - ) - except ValueError: - _LOGGER.error("Could not parse humidity from %s", humidity) - - def _update_target_temp_high(self, temp): - if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._attr_target_temperature_high = float(temp) - self.hass.async_create_task( - self.async_set_temperature( - **{ATTR_TARGET_TEMP_HIGH: self._attr_target_temperature_high} + def _validate_value(self, attribute, value, format): + if value is None: + # Shoudl never happen, but who knows. + _LOGGER.warning( + "Entity %s attribute %s returned value: None.", + self._attr_name, + attribute, + ) + return None + elif isinstance(value, TemplateError): + _LOGGER.error( + "Entity %s attribute %s returned exception: %s.", + self._attr_name, + attribute, + value, + ) + return None + elif value in (STATE_UNKNOWN, STATE_UNAVAILABLE): + _LOGGER.info( + "Entity %s attribute %s returned Uknown or Unavailable: %s.", + self._attr_name, + attribute, + value, + ) + return None + elif format is not None: + if type(format) is list or type(format) is dict: + value = str(value) + if str(value) not in format: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s. Expected one of: %s.", + self._attr_name, + attribute, + value, + format, ) - ) - except ValueError: - _LOGGER.error("Could not parse temperature high from %s", temp) - - def _update_target_temp_low(self, temp): - if temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - try: - self._attr_target_temperature_low = float(temp) - self.hass.async_create_task( - self.async_set_temperature( - **{ATTR_TARGET_TEMP_LOW: self._attr_target_temperature_low} + return None + elif format == "current_temperature": + try: + if self.precision == PRECISION_HALVES: + value = round(float(value) / 0.5) * 0.5 + elif self.precision == PRECISION_TENTHS: + value = round(float(value), 1) + else: + value = round(float(value)) + except ValueError: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + self._attr_name, + attribute, + value, ) - ) - except ValueError: - _LOGGER.error("Could not parse temperature low from %s", temp) - - def _update_hvac_mode(self, hvac_mode): - if hvac_mode in self._attr_hvac_modes: - self._current_operation = hvac_mode - self.hass.async_create_task(self.async_set_hvac_mode(hvac_mode)) - elif hvac_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.error( - "Received invalid hvac mode: %s. Expected: %s.", - hvac_mode, - self._attr_hvac_modes, + return None + elif format == "target_temperature": + try: + value = ( + round(float(value) / self._attr_target_temperature_step) + * self._attr_target_temperature_step + ) + except ValueError: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s. Expected integer or float.", + self._attr_name, + attribute, + value, + ) + return None + if value > self._attr_max_temp: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", + self._attr_name, + attribute, + value, + self._attr_max_temp, + ) + return None + if value < self._attr_min_temp: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", + self._attr_name, + attribute, + value, + self._attr_min_temp, + ) + return None + elif format == "curent_humidity": + try: + value = round(value) + except ValueError: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + self._attr_name, + attribute, + value, + ) + return None + elif format == "target_humidity": + try: + value = round(value) + except ValueError: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + self._attr_name, + attribute, + value, + ) + return None + if value > self._attr_max_humidity: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", + self._attr_name, + attribute, + value, + self._attr_max_humidity, + ) + return None + if value < self._attr_min_humidity: + _LOGGER.error( + "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", + self._attr_name, + attribute, + value, + self._attr_min_humidity, + ) + return None + + _LOGGER.debug( + "Entity %s attribute %s triggered update with value: %s.", + self._attr_name, + attribute, + value, + ) + return value + + @callback + def _update_current_temperature(self, result: float): + if current_temperature := self._validate_value( + "current_temperature", result, "temperature" + ): + self._attr_current_temperature = current_temperature + + @callback + def _update_current_humidity(self, result: float): + if current_humidity := self._validate_value( + "current_humidity", result, "humidity" + ): + self._attr_current_humidity = current_humidity + + @callback + def _update_hvac_action(self, result: str): + if hvac_action := self._validate_value( + "hvac_action", result, [member.value for member in HVACAction] + ): + self._attr_hvac_action = hvac_action + + @callback + def _update_target_temperature(self, result: float): + if target_temperature := self._validate_value( + "target_temperature", result, "setpoint" + ): + self.hass.async_create_task( + self._async_set_attribute("target_temperature", target_temperature), ) - def _update_preset_mode(self, preset_mode): - if preset_mode in self._attr_preset_modes: - self._current_preset_mode = preset_mode - self.hass.async_create_task(self.async_set_preset_mode(preset_mode)) - elif preset_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.error( - "Received invalid preset mode %s. Expected %s.", - preset_mode, - self._attr_preset_modes, + @callback + def _update_target_temperature_high(self, result: float): + if target_temperature_high := self._validate_value( + "target_temperature_high", result, "setpoint" + ): + self.hass.async_create_task( + self._async_set_attribute( + "target_temperature_high", target_temperature_high + ), ) - def _update_fan_mode(self, fan_mode): - if fan_mode in self._attr_fan_modes: - self._current_fan_mode = fan_mode - self.hass.async_create_task(self.async_set_fan_mode(fan_mode)) - elif fan_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.error( - "Received invalid fan mode: %s. Expected: %s.", - fan_mode, - self._attr_fan_modes, - ) - - def _update_swing_mode(self, swing_mode): - if swing_mode in self._swing_modes_list: - # check swing mode actually changed - if self._current_swing_mode != swing_mode: - self._current_swing_mode = swing_mode - self.hass.async_create_task(self.async_set_swing_mode(swing_mode)) - elif swing_mode not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.error( - "Received invalid swing mode: %s. Expected: %s.", - swing_mode, - self._swing_modes_list, + @callback + def _update_target_temperature_low(self, result: float): + if target_temperature_low := self._validate_value( + "target_temperature_low", result, "setpoint" + ): + self.hass.async_create_task( + self._async_set_attribute( + "target_temperature_low", target_temperature_low + ), ) - def _update_hvac_action(self, hvac_action): - if ( - hvac_action in [member.value for member in HVACAction] - or hvac_action is None + @callback + def _update_target_humidity(self, result: int): + if target_humidity := self._validate_value( + "target_humidity", result, "humidity" ): - if self._attr_hvac_action != hvac_action: - self._attr_hvac_action = hvac_action - elif hvac_action not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.error( - "Received invalid hvac action: %s. Expected: %s.", - hvac_action, - [member.value for member in HVACAction], + self.hass.async_create_task( + self._async_set_attribute("target_humidity", target_humidity), + ) + + @callback + def _update_hvac_mode(self, result: str): + if hvac_mode := self._validate_value( + "hvac_mode", result, self._attr_hvac_modes + ): + self.hass.async_create_task( + self._async_set_attribute("hvac_mode", hvac_mode), ) + @callback + def _update_preset_mode(self, result: str): + if preset_mode := self._validate_value( + "preset_mode", result, self._attr_preset_modes + ): + self.hass.async_create_task( + self._async_set_attribute("preset_mode", preset_mode), + ) + + @callback + def _update_fan_mode(self, result: str): + if fan_mode := self._validate_value("fan_mode", result, self.fan_modes): + self.hass.async_create_task( + self._async_set_attribute("fan_mode", fan_mode), + ) + + @callback + def _update_swing_mode(self, result: str): + if swing_mode := self._validate_value("swing_mode", result, self.swing_modes): + self.hass.async_create_task( + self._async_set_attribute("swing_mode", swing_mode) + ) + + async def _async_set_attribute(self, attr, value) -> None: + if getattr(self, "_attr_" + attr) == value: + # Nothing to do. + _LOGGER.debug( + "Entity %s attribute %s is already set to value: %s.", + self._attr_name, + attr, + value, + ) + return + + # Update entity attribute. + _LOGGER.debug( + "Entity %s updating attribute %s to value: %s.", + self._attr_name, + attr, + value, + ) + setattr(self, "_attr_" + attr, value) + # Update last_on modes if not off mode. + if attr in self._off_mode.keys() and value != self._off_mode[attr]: + self._last_on_mode[attr] = value + self.async_write_ha_state() + + if script := getattr(self, "_script_" + attr): + # Create a context referring to the trigger context. + trigger_context_id = None if self._context is None else self._context.id + script_context = Context(parent_id=trigger_context_id) + # Execute set action script. + _LOGGER.debug( + "Entity %s executing script set_action_%s with attribute %s set to value: %s.", + self._attr_name, + attr, + attr, + value, + ) + await script.async_run( + run_variables={attr: value}, + context=script_context, + ) + _LOGGER.debug( + "Entity %s execution of script set_action_%s finished.", + self._attr_name, + attr, + ) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._attr_unique_id + + @property + def name(self): + """Return the name of the climate device.""" + return self._attr_name + + @property + def state(self): + """Return the current state.""" + return self._attr_hvac_mode + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._attr_supported_features + @property def precision(self): """Return the precision of the system.""" @@ -598,25 +967,75 @@ def precision(self): return self._config.get(CONF_PRECISION) return super().precision + @property + def target_temperature_step(self) -> float | None: + """Return the supported step of target temperature.""" + return self._attr_target_temperature_step + + @property + def mode_action(self): + """Return the type of action mode.""" + return self._attr_mode_action + + @property + def max_action(self): + """Return the type of action mode.""" + return self._attr_max_action + @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit_of_measurement + return self._attr_unit_of_measurement + + @property + def min_temp(self): + """Return the polling state.""" + return self._attr_min_temp + + @property + def max_temp(self): + """Return the polling state.""" + return self._attr_max_temp + + @property + def min_humidity(self): + """Return the polling state.""" + return self._attr_min_humidity + + @property + def max_humidity(self): + """Return the polling state.""" + return self._attr_max_humidity @property def current_temperature(self): """Return the current temperature.""" - return self._current_temp + return self._attr_current_temperature @property def current_humidity(self): """Return the current humidity.""" - return self._current_humidity + return self._attr_current_humidity + + @property + def hvac_action(self): + """Return the current hvac_action.""" + return self._attr_hvac_action @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temp + return self._attr_target_temperature + + @property + def target_temperature_low(self): + """Return the temperature we try to reach.""" + return self._attr_target_temperature_low + + @property + def target_temperature_high(self): + """Return the temperature we try to reach.""" + return self._attr_target_temperature_high @property def target_humidity(self): @@ -625,144 +1044,105 @@ def target_humidity(self): @property def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - return self._current_operation + """Return current hvac_mode ie. heat, cool, idle.""" + return self._attr_hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return self._attr_hvac_modes @property def preset_mode(self): + """Return the list of available preset modes.""" + return self._attr_preset_mode + + @property + def preset_modes(self): """Return preset setting""" - return self._current_preset_mode + return self._attr_preset_modes @property def fan_mode(self): """Return the fan setting.""" - return self._current_fan_mode + return self._attr_fan_mode + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return self._attr_fan_modes @property def swing_mode(self): """Return the swing setting.""" - return self._current_swing_mode + return self._attr_swing_mode @property def swing_modes(self): - """List of available swing modes.""" - return self._swing_modes_list + """Return the list of available fan swing modes.""" + return self._attr_swing_modes @property - def last_on_operation(self): - """Return the last non-idle operation ie. heat, cool.""" - return self._last_on_operation - - async def _run_script(self, script, variables, context): - # Create a context referring to the trigger context. - trigger_context_id = None if context is None else context.id - script_context = Context(parent_id=trigger_context_id) - await script.async_run(run_variables=variables, context=script_context) + def extra_state_attributes(self): + """Platform specific attributes.""" + return { + "last_on_mode": self._last_on_mode, + "off_mode": self._off_mode, + } async def async_turn_off(self) -> None: """Turn climate off.""" - if HVACMode.OFF in self._attr_hvac_modes: - await self.async_set_hvac_mode(HVACMode.OFF) + if self._off_mode["hvac_mode"] in self._attr_hvac_modes: + await self.async_set_hvac_mode(self._off_mode["hvac_mode"]) async def async_turn_on(self) -> None: """Turn climate on.""" - if self._last_on_operation in self._attr_hvac_modes: - await self.async_set_hvac_mode(self._last_on_operation) + if self._last_on_mode["hvac_mode"] in self._attr_hvac_modes: + await self.async_set_hvac_mode(self._last_on_mode["hvac_mode"]) async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set new operation mode.""" - if self._hvac_mode_template is None: - self._current_operation = hvac_mode # always optimistic - self.async_write_ha_state() - - if self._set_hvac_mode_script is not None: - await self._run_script( - self._set_hvac_mode_script, {ATTR_HVAC_MODE: hvac_mode}, self._context - ) - - if not hvac_mode == HVACMode.OFF: - self._last_on_operation = hvac_mode + """Set new hvac mode.""" + await self._async_set_attribute("hvac_mode", hvac_mode) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self._preset_mode_template is None: - self._current_preset_mode = preset_mode - self.async_write_ha_state() - - if self._set_preset_mode_script is not None: - await self._run_script( - self._set_preset_mode_script, - {ATTR_PRESET_MODE: preset_mode}, - self._context, - ) + await self._async_set_attribute("preset_mode", preset_mode) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - if self._fan_mode_template is None: - self._current_fan_mode = fan_mode # always optimistic - self.async_write_ha_state() - - if self._set_fan_mode_script is not None: - await self._run_script( - self._set_fan_mode_script, {ATTR_FAN_MODE: fan_mode}, self._context - ) + await self._async_set_attribute("fan_mode", fan_mode) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" - if self._swing_mode_template is None: # use optimistic mode - self._current_swing_mode = swing_mode - self.async_write_ha_state() + await self._async_set_attribute("swing_mode", swing_mode) - if self._set_swing_mode_script is not None: - await self._run_script( - self._set_swing_mode_script, - {ATTR_SWING_MODE: swing_mode}, - self._context, - ) + async def async_set_humidity(self, target_humidity: int) -> None: + """Set new humidity target.""" + await self._async_set_attribute("target_humidity", round(target_humidity)) async def async_set_temperature(self, **kwargs) -> None: - """Set new target temperature.""" - # handle optimistic mode - if kwargs.get(ATTR_HVAC_MODE, self._current_operation) == HVACMode.HEAT_COOL: - if self._target_temperature_high_template is None: - self._attr_target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - - if self._target_temperature_low_template is None: - self._attr_target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - - if ( - self._target_temperature_high_template - or self._target_temperature_low_template - ): - self.async_write_ha_state() + """Set new target temperatures.""" + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + if hvac_mode is not None: + await self._async_set_attribute("hvac_mode", hvac_mode) + + if hvac_mode == HVACMode.HEAT_COOL: + target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + if target_temperature_high is not None: + await self._async_set_attribute( + "target_temperature_high", + self._round_temp_step(target_temperature_high), + ) + if target_temperature_low is not None: + await self._async_set_attribute( + "target_temperature_low", + self._round__temp_step(target_temperature_low), + ) else: - if self._target_temperature_template is None: - self._target_temp = kwargs.get(ATTR_TEMPERATURE) - self.async_write_ha_state() - - # set temperature calls can contain a new hvac mode. - if operation_mode := kwargs.get(ATTR_HVAC_MODE): - await self.async_set_hvac_mode(operation_mode) - - if self._set_temperature_script is not None: - await self._run_script( - self._set_temperature_script, - { - ATTR_TEMPERATURE: kwargs.get(ATTR_TEMPERATURE), - ATTR_TARGET_TEMP_HIGH: kwargs.get(ATTR_TARGET_TEMP_HIGH), - ATTR_TARGET_TEMP_LOW: kwargs.get(ATTR_TARGET_TEMP_LOW), - ATTR_HVAC_MODE: kwargs.get(ATTR_HVAC_MODE), - }, - self._context, - ) - - async def async_set_humidity(self, humidity) -> None: - """Set new humidity target.""" - if self._target_humidity_template is None: # use optimistic mode - self._current_humidity = humidity - self.async_write_ha_state() - - if self._set_humidity_script is not None: - await self._run_script( - self._set_humidity_script, {ATTR_HUMIDITY: humidity}, self._context - ) + target_temperature = kwargs.get(ATTR_TEMPERATURE, None) + if target_temperature is not None: + await self._async_set_attribute( + "target_temperature", + self._round_temp_step(target_temperature), + ) From 303b077473fad1efd1c20b858616684fe0b97b6b Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:34:40 +0200 Subject: [PATCH 2/8] fixed invalid temperature setpoint rounding fixed 0 being treated as invalid input added input validation for HA climate service calls and UI actions --- custom_components/climate_template/climate.py | 187 ++++++++++++------ 1 file changed, 130 insertions(+), 57 deletions(-) diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 81ee2a6..4fc0a76 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -669,8 +669,7 @@ async def async_added_to_hass(self): def _validate_value(self, attribute, value, format): if value is None: - # Shoudl never happen, but who knows. - _LOGGER.warning( + _LOGGER.debug( "Entity %s attribute %s returned value: None.", self._attr_name, attribute, @@ -803,39 +802,49 @@ def _validate_value(self, attribute, value, format): @callback def _update_current_temperature(self, result: float): - if current_temperature := self._validate_value( - "current_temperature", result, "temperature" - ): + if ( + current_temperature := self._validate_value( + "current_temperature", result, "temperature" + ) + ) is not None: self._attr_current_temperature = current_temperature @callback def _update_current_humidity(self, result: float): - if current_humidity := self._validate_value( - "current_humidity", result, "humidity" - ): + if ( + current_humidity := self._validate_value( + "current_humidity", result, "humidity" + ) + ) is not None: self._attr_current_humidity = current_humidity @callback def _update_hvac_action(self, result: str): - if hvac_action := self._validate_value( - "hvac_action", result, [member.value for member in HVACAction] - ): + if ( + hvac_action := self._validate_value( + "hvac_action", result, [member.value for member in HVACAction] + ) + ) is not None: self._attr_hvac_action = hvac_action @callback def _update_target_temperature(self, result: float): - if target_temperature := self._validate_value( - "target_temperature", result, "setpoint" - ): + if ( + target_temperature := self._validate_value( + "target_temperature", result, "setpoint" + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute("target_temperature", target_temperature), ) @callback def _update_target_temperature_high(self, result: float): - if target_temperature_high := self._validate_value( - "target_temperature_high", result, "setpoint" - ): + if ( + target_temperature_high := self._validate_value( + "target_temperature_high", result, "setpoint" + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute( "target_temperature_high", target_temperature_high @@ -844,9 +853,11 @@ def _update_target_temperature_high(self, result: float): @callback def _update_target_temperature_low(self, result: float): - if target_temperature_low := self._validate_value( - "target_temperature_low", result, "setpoint" - ): + if ( + target_temperature_low := self._validate_value( + "target_temperature_low", result, "setpoint" + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute( "target_temperature_low", target_temperature_low @@ -855,41 +866,51 @@ def _update_target_temperature_low(self, result: float): @callback def _update_target_humidity(self, result: int): - if target_humidity := self._validate_value( - "target_humidity", result, "humidity" - ): + if ( + target_humidity := self._validate_value( + "target_humidity", result, "humidity" + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute("target_humidity", target_humidity), ) @callback def _update_hvac_mode(self, result: str): - if hvac_mode := self._validate_value( - "hvac_mode", result, self._attr_hvac_modes - ): + if ( + hvac_mode := self._validate_value( + "hvac_mode", result, self._attr_hvac_modes + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute("hvac_mode", hvac_mode), ) @callback def _update_preset_mode(self, result: str): - if preset_mode := self._validate_value( - "preset_mode", result, self._attr_preset_modes - ): + if ( + preset_mode := self._validate_value( + "preset_mode", result, self._attr_preset_modes + ) + ) is not None: self.hass.async_create_task( self._async_set_attribute("preset_mode", preset_mode), ) @callback def _update_fan_mode(self, result: str): - if fan_mode := self._validate_value("fan_mode", result, self.fan_modes): + if ( + fan_mode := self._validate_value("fan_mode", result, self.fan_modes) + ) is not None: self.hass.async_create_task( self._async_set_attribute("fan_mode", fan_mode), ) @callback def _update_swing_mode(self, result: str): - if swing_mode := self._validate_value("swing_mode", result, self.swing_modes): + if ( + swing_mode := self._validate_value("swing_mode", result, self.swing_modes) + ) is not None: self.hass.async_create_task( self._async_set_attribute("swing_mode", swing_mode) ) @@ -1092,57 +1113,109 @@ def extra_state_attributes(self): async def async_turn_off(self) -> None: """Turn climate off.""" - if self._off_mode["hvac_mode"] in self._attr_hvac_modes: + if "hvac_mode" in self._off_mode.keys(): await self.async_set_hvac_mode(self._off_mode["hvac_mode"]) async def async_turn_on(self) -> None: """Turn climate on.""" - if self._last_on_mode["hvac_mode"] in self._attr_hvac_modes: + if "hvac_mode" in self._last_on_mode.keys(): await self.async_set_hvac_mode(self._last_on_mode["hvac_mode"]) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, input: str) -> None: """Set new hvac mode.""" - await self._async_set_attribute("hvac_mode", hvac_mode) + if ( + hvac_mode := self._validate_value( + "hvac_mode", + input, + self._attr_hvac_modes, + ) + ) is not None: + await self._async_set_attribute("hvac_mode", hvac_mode) - async def async_set_preset_mode(self, preset_mode: str) -> None: + async def async_set_preset_mode(self, input: str) -> None: """Set new preset mode.""" - await self._async_set_attribute("preset_mode", preset_mode) + if ( + preset_mode := self._validate_value( + "preset_mode", + input, + self._attr_preset_modes, + ) + ) is not None: + await self._async_set_attribute("preset_mode", preset_mode) - async def async_set_fan_mode(self, fan_mode: str) -> None: + async def async_set_fan_mode(self, input: str) -> None: """Set new fan mode.""" - await self._async_set_attribute("fan_mode", fan_mode) + if ( + fan_mode := self._validate_value( + "fan_mode", + input, + self._attr_fan_modes, + ) + ) is not None: + await self._async_set_attribute("fan_mode", fan_mode) - async def async_set_swing_mode(self, swing_mode: str) -> None: + async def async_set_swing_mode(self, input: str) -> None: """Set new swing mode.""" - await self._async_set_attribute("swing_mode", swing_mode) + if ( + swing_mode := self._validate_value( + "swing_mode", + input, + self._attr_swing_modes, + ) + ) is not None: + await self._async_set_attribute("swing_mode", swing_mode) - async def async_set_humidity(self, target_humidity: int) -> None: + async def async_set_humidity(self, input: int) -> None: """Set new humidity target.""" - await self._async_set_attribute("target_humidity", round(target_humidity)) + if ( + target_humidity := self._validate_value( + "target_humidity", + input, + "target_humidity", + ) + ) is not None: + await self._async_set_attribute("target_humidity", target_humidity) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" - hvac_mode = kwargs.get(ATTR_HVAC_MODE) - if hvac_mode is not None: + if ( + hvac_mode := self._validate_value( + "hvac_mode", + kwargs.get(ATTR_HVAC_MODE), + self._attr_hvac_modes, + ) + ) is not None: await self._async_set_attribute("hvac_mode", hvac_mode) - if hvac_mode == HVACMode.HEAT_COOL: - target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - if target_temperature_high is not None: + if self._attr_hvac_mode == HVACMode.HEAT_COOL: + if ( + target_temperature_low := self._validate_value( + "target_temperature_low", + kwargs.get(ATTR_TARGET_TEMP_LOW), + "target_temperature", + ) + ) is not None: await self._async_set_attribute( + "target_temperature_low", target_temperature_low + ) + if ( + target_temperature_high := self._validate_value( "target_temperature_high", - self._round_temp_step(target_temperature_high), + kwargs.get(ATTR_TARGET_TEMP_HIGH), + "target_temperature", ) - if target_temperature_low is not None: + ) is not None: await self._async_set_attribute( - "target_temperature_low", - self._round__temp_step(target_temperature_low), + "target_temperature_high", target_temperature_high ) else: - target_temperature = kwargs.get(ATTR_TEMPERATURE, None) - if target_temperature is not None: - await self._async_set_attribute( + if ( + target_temperature := self._validate_value( + "target_temperature", + kwargs.get(ATTR_TEMPERATURE), "target_temperature", - self._round_temp_step(target_temperature), + ) + ) is not None: + await self._async_set_attribute( + "target_temperature", target_temperature ) From 9810efb47e89539f9096805f95d997ccc61fa987 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:38:29 +0200 Subject: [PATCH 3/8] fixed non functional translation fixed kwargs - HA func input parameters expected naming removed unnecessary properties (use defaults from climate class) reformatted Readme --- README.md | 83 +++--- custom_components/climate_template/climate.py | 277 +++++------------- 2 files changed, 107 insertions(+), 253 deletions(-) diff --git a/README.md b/README.md index 5cb8bef..411c696 100644 --- a/README.md +++ b/README.md @@ -15,48 +15,47 @@ All configuration variables are optional. The climate device will work in optimi If you do not define a `template` or its corresponding `action` the climate device will not have that attribute, e.g. either `swing_mode_template` or `set_swing_mode` must be defined for the climate to have a swing mode. -| Name | Type | Description | Default Value | -| -------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| name | `string` | The name of the climate device. | "Template Climate" | -| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | -| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | -| max_action | `positive_int` | positive number from 1 | 1 | -| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | -| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | -| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | -| | | | | -| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | -| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | -| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | -| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device with the HEAT_COOL hvac_mode. | | -| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device with the HEAT_COOL hvac_mode. | | -| target_humidity | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target humidity of the climate device. | | -| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | -| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | -| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | -| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | -| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | -| | | | | -| set_target_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temperature` variable. | | -| set_target_temperature_low | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_low` variable. | | -| set_target_temperature_high | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_high` variable. | | -| set_target_humidity | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set humidity command. Can use `target_humidity` variable. | | -| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | -| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | -| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | -| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | -| update_timeout | `positive_int` | If both attribute `_template` (to get current atribute state) and `set_` attribute action (to set the attribute state) are used, then this value is maximum time in seconds between the set action is trigerred and attribute update is received via template. If there is no update received before this timeout, climate_template entity attribute will not updated. If not set, this functionality is disabled by default and attribute will be always updated. In such case integration could execute `set_` action twice (first time on action trigger and second time on `_template` update), leading in the worst case to two paralel action exectution and ceoresponding HA warnings. | 0 | -| | | | | -| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_mode`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. | ["off", "auto", "cool", "heat", "dry", "fan_only"] | -| fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. | ["off", "auto", "low", "medium", "high"] | -| preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. | ["activity", "away", "boost", "comfort", "eco", "home", "sleep"] | -| swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. | ["off", "on"] | -| min_temperature | `float` | Minimum temperature set point available. | 7 | -| max_temperature | `float` | Maximum temperature set point available. | 35 | -| min_humidity | `float` | Minimum humidity set point available. | 30 | -| max_humidity | `float` | Maximum humidity set point available. | 99 | -| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | -| temp_step | `float` | Step size for temperature set point. | 1 | +| Name | Type | Description | Default Value | +| -------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| name | `string` | The name of the climate device. | "Template Climate" | +| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | +| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | +| max_action | `positive_int` | positive number from 1 | 1 | +| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | +| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | +| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | +| | | | | +| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | +| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | +| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | +| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device with the HEAT_COOL hvac_mode. | | +| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device with the HEAT_COOL hvac_mode. | | +| target_humidity | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target humidity of the climate device. | | +| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | +| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | +| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | +| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | +| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | +| | | | | +| set_target_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temperature` variable. | | +| set_target_temperature_low | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_low` variable. | | +| set_target_temperature_high | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_high` variable. | | +| set_target_humidity | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set humidity command. Can use `target_humidity` variable. | | +| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | +| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | +| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | +| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | +| | | | | +| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_mode`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. | ["off", "auto", "cool", "heat", "dry", "fan_only"] | +| fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. | ["off", "auto", "low", "medium", "high"] | +| preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. | ["activity", "away", "boost", "comfort", "eco", "home", "sleep"] | +| swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. | ["off", "on"] | +| min_temperature | `float` | Minimum temperature set point available. | 7 | +| max_temperature | `float` | Maximum temperature set point available. | 35 | +| min_humidity | `float` | Minimum humidity set point available. | 30 | +| max_humidity | `float` | Maximum humidity set point available. | 99 | +| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | +| temp_step | `float` | Step size for temperature set point. | 1 | ## Example Configuration diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 4fc0a76..567e554 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -227,7 +227,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): ) self._attr_unique_id = config.get(CONF_UNIQUE_ID, None) self._attr_supported_features = 0 - self._attr_unit_of_measurement = hass.config.units.temperature_unit + self._attr_temperature_unit = hass.config.units.temperature_unit self._attr_target_temperature_step = config[CONF_TEMP_STEP] self._attr_mode_action = config[CONF_MODE_ACTION] self._attr_max_action = config[CONF_MAX_ACTION] @@ -628,7 +628,7 @@ async def async_added_to_hass(self): if self._template_hvac_mode: self.add_template_attribute( - "_current_hvac_mode", + "_hvac_mode", self._template_hvac_mode, None, self._update_hvac_mode, @@ -637,7 +637,7 @@ async def async_added_to_hass(self): if self._template_preset_mode: self.add_template_attribute( - "_current_preset_mode", + "_preset_mode", self._template_preset_mode, None, self._update_preset_mode, @@ -646,7 +646,7 @@ async def async_added_to_hass(self): if self._template_fan_mode: self.add_template_attribute( - "_current_fan_mode", + "_fan_mode", self._template_fan_mode, None, self._update_fan_mode, @@ -655,7 +655,7 @@ async def async_added_to_hass(self): if self._template_swing_mode: self.add_template_attribute( - "_current_swing_mode", + "_swing_mode", self._template_swing_mode, None, self._update_swing_mode, @@ -801,119 +801,111 @@ def _validate_value(self, attribute, value, format): return value @callback - def _update_current_temperature(self, result: float): + def _update_current_temperature(self, current_temperature: float): if ( - current_temperature := self._validate_value( - "current_temperature", result, "temperature" + value := self._validate_value( + "current_temperature", current_temperature, "temperature" ) ) is not None: - self._attr_current_temperature = current_temperature + self._attr_current_temperature = value @callback - def _update_current_humidity(self, result: float): + def _update_current_humidity(self, current_humidity: float): if ( - current_humidity := self._validate_value( - "current_humidity", result, "humidity" + value := self._validate_value( + "current_humidity", current_humidity, "humidity" ) ) is not None: - self._attr_current_humidity = current_humidity + self._attr_current_humidity = value @callback - def _update_hvac_action(self, result: str): + def _update_hvac_action(self, hvac_action: str): if ( - hvac_action := self._validate_value( - "hvac_action", result, [member.value for member in HVACAction] + value := self._validate_value( + "hvac_action", hvac_action, [member.value for member in HVACAction] ) ) is not None: - self._attr_hvac_action = hvac_action + self._attr_hvac_action = value @callback - def _update_target_temperature(self, result: float): + def _update_target_temperature(self, target_temperature: float): if ( - target_temperature := self._validate_value( - "target_temperature", result, "setpoint" + value := self._validate_value( + "target_temperature", target_temperature, "setpoint" ) ) is not None: self.hass.async_create_task( - self._async_set_attribute("target_temperature", target_temperature), + self._async_set_attribute("target_temperature", value), ) @callback - def _update_target_temperature_high(self, result: float): + def _update_target_temperature_low(self, target_temperature_low: float): if ( - target_temperature_high := self._validate_value( - "target_temperature_high", result, "setpoint" + value := self._validate_value( + "target_temperature_low", target_temperature_low, "setpoint" ) ) is not None: self.hass.async_create_task( - self._async_set_attribute( - "target_temperature_high", target_temperature_high - ), + self._async_set_attribute("target_temperature_low", value), ) @callback - def _update_target_temperature_low(self, result: float): + def _update_target_temperature_high(self, target_temperature_high: float): if ( - target_temperature_low := self._validate_value( - "target_temperature_low", result, "setpoint" + value := self._validate_value( + "target_temperature_high", target_temperature_high, "setpoint" ) ) is not None: self.hass.async_create_task( - self._async_set_attribute( - "target_temperature_low", target_temperature_low - ), + self._async_set_attribute("target_temperature_high", value), ) @callback - def _update_target_humidity(self, result: int): + def _update_target_humidity(self, target_humidity: int): if ( - target_humidity := self._validate_value( - "target_humidity", result, "humidity" + value := self._validate_value( + "target_humidity", target_humidity, "humidity" ) ) is not None: self.hass.async_create_task( - self._async_set_attribute("target_humidity", target_humidity), + self._async_set_attribute("target_humidity", value), ) @callback - def _update_hvac_mode(self, result: str): + def _update_hvac_mode(self, hvac_mode: str): if ( - hvac_mode := self._validate_value( - "hvac_mode", result, self._attr_hvac_modes - ) + value := self._validate_value("hvac_mode", hvac_mode, self._attr_hvac_modes) ) is not None: self.hass.async_create_task( - self._async_set_attribute("hvac_mode", hvac_mode), + self._async_set_attribute("hvac_mode", value), ) @callback - def _update_preset_mode(self, result: str): + def _update_preset_mode(self, preset_mode: str): if ( - preset_mode := self._validate_value( - "preset_mode", result, self._attr_preset_modes + value := self._validate_value( + "preset_mode", preset_mode, self._attr_preset_modes ) ) is not None: self.hass.async_create_task( - self._async_set_attribute("preset_mode", preset_mode), + self._async_set_attribute("preset_mode", value), ) @callback - def _update_fan_mode(self, result: str): + def _update_fan_mode(self, fan_mode: str): if ( - fan_mode := self._validate_value("fan_mode", result, self.fan_modes) + value := self._validate_value("fan_mode", fan_mode, self.fan_modes) ) is not None: self.hass.async_create_task( - self._async_set_attribute("fan_mode", fan_mode), + self._async_set_attribute("fan_mode", value), ) @callback - def _update_swing_mode(self, result: str): + def _update_swing_mode(self, swing_mode: str): if ( - swing_mode := self._validate_value("swing_mode", result, self.swing_modes) + value := self._validate_value("swing_mode", swing_mode, self.swing_modes) ) is not None: - self.hass.async_create_task( - self._async_set_attribute("swing_mode", swing_mode) - ) + self.hass.async_create_task(self._async_set_attribute("swing_mode", value)) async def _async_set_attribute(self, attr, value) -> None: if getattr(self, "_attr_" + attr) == value: @@ -961,148 +953,11 @@ async def _async_set_attribute(self, attr, value) -> None: attr, ) - @property - def unique_id(self): - """Return a unique ID.""" - return self._attr_unique_id - - @property - def name(self): - """Return the name of the climate device.""" - return self._attr_name - @property def state(self): """Return the current state.""" return self._attr_hvac_mode - @property - def supported_features(self): - """Return the list of supported features.""" - return self._attr_supported_features - - @property - def precision(self): - """Return the precision of the system.""" - if self._config.get(CONF_PRECISION) is not None: - return self._config.get(CONF_PRECISION) - return super().precision - - @property - def target_temperature_step(self) -> float | None: - """Return the supported step of target temperature.""" - return self._attr_target_temperature_step - - @property - def mode_action(self): - """Return the type of action mode.""" - return self._attr_mode_action - - @property - def max_action(self): - """Return the type of action mode.""" - return self._attr_max_action - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._attr_unit_of_measurement - - @property - def min_temp(self): - """Return the polling state.""" - return self._attr_min_temp - - @property - def max_temp(self): - """Return the polling state.""" - return self._attr_max_temp - - @property - def min_humidity(self): - """Return the polling state.""" - return self._attr_min_humidity - - @property - def max_humidity(self): - """Return the polling state.""" - return self._attr_max_humidity - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._attr_current_temperature - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._attr_current_humidity - - @property - def hvac_action(self): - """Return the current hvac_action.""" - return self._attr_hvac_action - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._attr_target_temperature - - @property - def target_temperature_low(self): - """Return the temperature we try to reach.""" - return self._attr_target_temperature_low - - @property - def target_temperature_high(self): - """Return the temperature we try to reach.""" - return self._attr_target_temperature_high - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - return self._target_humidity - - @property - def hvac_mode(self): - """Return current hvac_mode ie. heat, cool, idle.""" - return self._attr_hvac_mode - - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return self._attr_hvac_modes - - @property - def preset_mode(self): - """Return the list of available preset modes.""" - return self._attr_preset_mode - - @property - def preset_modes(self): - """Return preset setting""" - return self._attr_preset_modes - - @property - def fan_mode(self): - """Return the fan setting.""" - return self._attr_fan_mode - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return self._attr_fan_modes - - @property - def swing_mode(self): - """Return the swing setting.""" - return self._attr_swing_mode - - @property - def swing_modes(self): - """Return the list of available fan swing modes.""" - return self._attr_swing_modes - @property def extra_state_attributes(self): """Platform specific attributes.""" @@ -1121,60 +976,60 @@ async def async_turn_on(self) -> None: if "hvac_mode" in self._last_on_mode.keys(): await self.async_set_hvac_mode(self._last_on_mode["hvac_mode"]) - async def async_set_hvac_mode(self, input: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new hvac mode.""" if ( - hvac_mode := self._validate_value( + value := self._validate_value( "hvac_mode", - input, + hvac_mode, self._attr_hvac_modes, ) ) is not None: - await self._async_set_attribute("hvac_mode", hvac_mode) + await self._async_set_attribute("hvac_mode", value) - async def async_set_preset_mode(self, input: str) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if ( - preset_mode := self._validate_value( + value := self._validate_value( "preset_mode", - input, + preset_mode, self._attr_preset_modes, ) ) is not None: - await self._async_set_attribute("preset_mode", preset_mode) + await self._async_set_attribute("preset_mode", value) - async def async_set_fan_mode(self, input: str) -> None: + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if ( - fan_mode := self._validate_value( + value := self._validate_value( "fan_mode", - input, + fan_mode, self._attr_fan_modes, ) ) is not None: - await self._async_set_attribute("fan_mode", fan_mode) + await self._async_set_attribute("fan_mode", value) - async def async_set_swing_mode(self, input: str) -> None: + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" if ( - swing_mode := self._validate_value( + value := self._validate_value( "swing_mode", - input, + swing_mode, self._attr_swing_modes, ) ) is not None: - await self._async_set_attribute("swing_mode", swing_mode) + await self._async_set_attribute("swing_mode", value) - async def async_set_humidity(self, input: int) -> None: + async def async_set_humidity(self, target_humidity: int) -> None: """Set new humidity target.""" if ( - target_humidity := self._validate_value( + value := self._validate_value( "target_humidity", - input, + target_humidity, "target_humidity", ) ) is not None: - await self._async_set_attribute("target_humidity", target_humidity) + await self._async_set_attribute("target_humidity", value) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" From abff72d2cf87941dcb9af01f1d0725ad4dd18de7 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Wed, 17 Apr 2024 03:19:20 +0200 Subject: [PATCH 4/8] reverted temperature action changes added configuration logical checks added stored attributes checks added input attributes checks removed default modes - now all used modes shall be configured explicitly --- README.md | 82 +- custom_components/climate_template/climate.py | 1193 ++++++++++------- 2 files changed, 743 insertions(+), 532 deletions(-) diff --git a/README.md b/README.md index 411c696..c995af5 100644 --- a/README.md +++ b/README.md @@ -15,47 +15,45 @@ All configuration variables are optional. The climate device will work in optimi If you do not define a `template` or its corresponding `action` the climate device will not have that attribute, e.g. either `swing_mode_template` or `set_swing_mode` must be defined for the climate to have a swing mode. -| Name | Type | Description | Default Value | -| -------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -| name | `string` | The name of the climate device. | "Template Climate" | -| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | -| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | -| max_action | `positive_int` | positive number from 1 | 1 | -| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | -| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | -| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | -| | | | | -| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | -| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | -| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | -| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device with the HEAT_COOL hvac_mode. | | -| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device with the HEAT_COOL hvac_mode. | | -| target_humidity | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target humidity of the climate device. | | -| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | -| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | -| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | -| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | -| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | -| | | | | -| set_target_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temperature` variable. | | -| set_target_temperature_low | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_low` variable. | | -| set_target_temperature_high | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `target_temp_high` variable. | | -| set_target_humidity | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set humidity command. Can use `target_humidity` variable. | | -| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | -| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | -| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | -| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | -| | | | | -| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_mode`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. | ["off", "auto", "cool", "heat", "dry", "fan_only"] | -| fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. | ["off", "auto", "low", "medium", "high"] | -| preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. | ["activity", "away", "boost", "comfort", "eco", "home", "sleep"] | -| swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. | ["off", "on"] | -| min_temperature | `float` | Minimum temperature set point available. | 7 | -| max_temperature | `float` | Maximum temperature set point available. | 35 | -| min_humidity | `float` | Minimum humidity set point available. | 30 | -| max_humidity | `float` | Maximum humidity set point available. | 99 | -| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | -| temp_step | `float` | Step size for temperature set point. | 1 | +| Name | Type | Description | Default Value | +| -------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | +| name | `string` | The name of the climate device. | "Template Climate" | +| unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | +| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | +| max_action | `positive_int` | positive number from 1 | 1 | +| icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | +| entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | +| availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | +| | | | | +| current_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current temperature. | | +| current_humidity_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the current humidity. | | +| target_temperature_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature of the climate device. | | +| target_temperature_low_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature low of the climate device with the HEAT_COOL hvac_mode. | | +| target_temperature_high_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target temperature high of the climate device with the HEAT_COOL hvac_mode. | | +| target_humidity | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the target humidity of the climate device. | | +| hvac_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the hvac mode of the climate device. | | +| fan_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the fan mode of the climate device. | | +| preset_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the preset mode of the climate device. | | +| swing_mode_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the swing mode of the climate device. | | +| hvac_action_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the [`hvac action`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-action) of the climate device. | | +| | | | | +| set_temperature | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set temperature command. Can use `temperature`, `target_temp_high`, `target_temp_low` and `hvac_mode` variable. | +| set_humidity | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set humidity command. Can use `humidity` variable. | | +| set_hvac_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set hvac mode command. Can use `hvac_mode` variable. | | +| set_fan_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set fan mode command. Can use `fan_mode` variable. | | +| set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | +| set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | +| | | | | +| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. **At least One MUST be set.** | | +| preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. Default list of HA [`preset_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#presets). | | +| fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. Default list of HA [`fan_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#fan-modes). | | +| swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. Default list of HA [`swing_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#fan-modes). | | +| min_temperature | `float` | Minimum temperature set point available. | 7 | +| max_temperature | `float` | Maximum temperature set point available. | 35 | +| min_humidity | `float` | Minimum humidity set point available. | 30 | +| max_humidity | `float` | Maximum humidity set point available. | 99 | +| precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | +| temp_step | `float` | Step size for temperature set point. | 1 | ## Example Configuration @@ -95,7 +93,7 @@ climate: # send the climates current state to esphome. - service: esphome.bedroom_node_aircon_state data: - temperature: "{{ state_attr('climate.bedroom_aircon', 'target_temperature') | int }}" + temperature: "{{ state_attr('climate.bedroom_aircon', 'temperature') | int }}" operation_mode: "{{ states('climate.bedroom_aircon') }}" fan_mode: "{{ state_attr('climate.bedroom_aircon', 'fan_mode') }}" swing_mode: "{{ is_state_attr('climate.bedroom_aircon', 'swing_mode', 'on') }}" diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 567e554..fa7bf43 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -88,11 +88,8 @@ CONF_SWING_MODE_TEMPLATE = "swing_mode_template" CONF_HVAC_ACTION_TEMPLATE = "hvac_action_template" -CONF_UPDATE_TIMEOUT = "update_timeout" -CONF_SET_TEMPERATURE_ACTION = "set_target_temperature" -CONF_SET_TEMPERATURE_LOW_ACTION = "set_target_temperature_low" -CONF_SET_TEMPERATURE_HIGH_ACTION = "set_target_temperature_high" -CONF_SET_HUMIDITY_ACTION = "set_target_humidity" +CONF_SET_TEMPERATURE_ACTION = "set_temperature" +CONF_SET_HUMIDITY_ACTION = "set_humidity" CONF_SET_HVAC_MODE_ACTION = "set_hvac_mode" CONF_SET_FAN_MODE_ACTION = "set_fan_mode" CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" @@ -134,53 +131,15 @@ vol.Optional(CONF_SWING_MODE_TEMPLATE): cv.template, vol.Optional(CONF_HVAC_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_SET_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_TEMPERATURE_LOW_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_TEMPERATURE_HIGH_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_HUMIDITY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_HVAC_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_FAN_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SWING_MODE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional( - CONF_HVAC_MODE_LIST, - default=[ - HVACMode.AUTO, - HVACMode.OFF, - HVACMode.COOL, - HVACMode.HEAT, - HVACMode.DRY, - HVACMode.FAN_ONLY, - ], - ): cv.ensure_list, - vol.Optional( - CONF_PRESET_MODE_LIST, - default=[ - PRESET_ECO, - PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_HOME, - PRESET_SLEEP, - PRESET_ACTIVITY, - ], - ): cv.ensure_list, - vol.Optional( - CONF_FAN_MODE_LIST, - default=[ - FAN_OFF, - FAN_AUTO, - FAN_LOW, - FAN_MEDIUM, - FAN_HIGH, - ], - ): cv.ensure_list, - vol.Optional( - CONF_SWING_MODE_LIST, - default=[ - SWING_OFF, - SWING_ON, - ], - ): cv.ensure_list, + vol.Optional(CONF_HVAC_MODE_LIST, default=[]): cv.ensure_list, + vol.Optional(CONF_PRESET_MODE_LIST, default=[]): cv.ensure_list, + vol.Optional(CONF_FAN_MODE_LIST, default=[]): cv.ensure_list, + vol.Optional(CONF_SWING_MODE_LIST, default=[]): cv.ensure_list, vol.Optional(CONF_TEMPERATURE_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMPERATURE_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce( @@ -284,17 +243,23 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): ) self._template_target_humidity = config.get(CONF_TARGET_HUMIDITY_TEMPLATE) - # Set script callbacks and supported features flags. + self._action_hvac_mode = config.get(CONF_SET_HVAC_MODE_ACTION) + self._action_preset_mode = config.get(CONF_SET_PRESET_MODE_ACTION) + self._action_fan_mode = config.get(CONF_SET_FAN_MODE_ACTION) + self._action_swing_mode = config.get(CONF_SET_SWING_MODE_ACTION) + self._action_temperature = config.get(CONF_SET_TEMPERATURE_ACTION) + self._action_humidity = config.get(CONF_SET_HUMIDITY_ACTION) + + # Set scripts callbacks. self._script_hvac_mode = None - if action_hvac_mode := config.get(CONF_SET_HVAC_MODE_ACTION): - self._script_hvac_mode = Script( - hass, - action_hvac_mode, - self._attr_name, - DOMAIN, - script_mode=self._attr_mode_action, - max_runs=self._attr_max_action, - ) + self._script_preset_mode = None + self._script_fan_mode = None + self._script_swing_mode = None + self._script_temperature = None + self._script_humidity = None + + # Check config entries and set entity supported features flags. + if self._attr_hvac_modes and len(self._attr_hvac_modes) > 0: if ( HVACMode.OFF in self._attr_hvac_modes and len(self._attr_hvac_modes) >= 2 @@ -311,116 +276,216 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): )[0] else: self._last_on_mode["hvac_mode"] = HVACMode.OFF + else: + _LOGGER.error("At least one hvac mode shall be configured!") + return False + + if self._attr_preset_modes and len(self._attr_preset_modes) >= 2: + if self._action_preset_mode or self._template_preset_mode: + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + else: + _LOGGER.warning( + "Preset mode are configured, but there is neither action %s nor template %s configured.", + CONF_SET_PRESET_MODE_ACTION, + CONF_PRESET_MODE_TEMPLATE, + ) + self._attr_preset_modes = [] + elif self._action_preset_mode or self._template_preset_mode: + _LOGGER.warning( + "Preset mode are not configured, but there is action %s and/or template %s configured.", + CONF_SET_PRESET_MODE_ACTION, + CONF_PRESET_MODE_TEMPLATE, + ) + self._action_preset_mode = None + self._template_preset_mode = None + + if self._attr_fan_modes and len(self._attr_fan_modes) >= 2: + if self._action_fan_mode or self._template_fan_mode: + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + else: + _LOGGER.warning( + "Fan mode are configured, but there is neither action %s nor template %s configured.", + CONF_SET_FAN_MODE_ACTION, + CONF_FAN_MODE_TEMPLATE, + ) + self._attr_fan_modes = [] + elif self._action_fan_mode or self._template_fan_mode: + _LOGGER.warning( + "Fan mode are not configured, but there is action %s and/or template %s configured.", + CONF_SET_FAN_MODE_ACTION, + CONF_FAN_MODE_TEMPLATE, + ) + self._action_fan_mode = None + self._template_fan_mode = None + + if self._attr_swing_modes and len(self._attr_swing_modes) >= 2: + if self._action_swing_mode or self._template_swing_mode: + self._attr_supported_features |= ClimateEntityFeature.SWING_MODE + else: + _LOGGER.warning( + "Swing mode are configured, but there is neither action %s nor template %s configured.", + CONF_SET_SWING_MODE_ACTION, + CONF_SWING_MODE_TEMPLATE, + ) + self._attr_swing_modes = [] + elif self._action_swing_mode or self._template_swing_mode: + _LOGGER.warning( + "Swing mode are not configured, but there is action %s and/or template %s configured.", + CONF_SET_SWING_MODE_ACTION, + CONF_SWING_MODE_TEMPLATE, + ) + self._action_swing_mode = None + self._template_swing_mode = None - self._script_preset_mode = None - if action_preset_mode := config.get(CONF_SET_PRESET_MODE_ACTION): - self._script_preset_mode = Script( - hass, - action_preset_mode, - self._attr_name, - DOMAIN, + if HVACMode.HEAT_COOL in self._attr_hvac_modes: + if ( + not self._template_target_temperature_high + and self._template_target_temperature_low + ) or ( + not self._template_target_temperature_low + and self._template_target_temperature_high + ): + _LOGGER.warning( + "Either both templates %s and %s shall be configured or none of them.", + CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, + CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, + ) + self._template_target_temperature_low = None + self._template_target_temperature_high = None + if self._action_temperature or ( + self._template_target_temperature_low + and self._template_target_temperature_high + ): + self._attr_supported_features |= ( + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + else: + _LOGGER.warning( + "Hvac mode heat_cool is configured, but there is no action neither template for high or low temperature configued." + ) + else: + if ( + self._template_target_temperature_low + or self._template_target_temperature_high + ): + _LOGGER.warning( + "Hvac mode heat_cool is not configured, but there is %s and/or %s template configured.", + CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, + CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, + ) + self._template_target_temperature_low = None + self._template_target_temperature_high = None + + if any( + list( + map( + lambda var: var in self._attr_hvac_modes, + [HVACMode.AUTO, HVACMode.HEAT, HVACMode.COOL], + ) ) - self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + ): + if self._action_temperature or self._template_target_temperature: + self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE + else: + _LOGGER.warning( + "Hvac mode auto, heat or cool is configured, but there is no action neither template for temperature configued." + ) + else: + if self._action_temperature: + _LOGGER.warning( + "Hvac mode auto, heat or cool is not configured, but there is action %s configured.", + CONF_SET_TEMPERATURE_ACTION, + ) + self._action_temperature = None + if self._template_target_temperature: + _LOGGER.warning( + "Hvac mode auto, heat or cool is not configured, but there is template %s configured.", + CONF_TARGET_TEMPERATURE_TEMPLATE, + ) + self._template_target_temperature = None + + if HVACMode.DRY in self._attr_hvac_modes: + if self._action_humidity or self._template_target_humidity: + self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY + else: + _LOGGER.warning( + "Hvac mode dry is configured, but there is no %s action neither %s template configured.", + CONF_SET_HUMIDITY_ACTION, + CONF_TARGET_HUMIDITY_TEMPLATE, + ) + else: + if self._action_humidity: + _LOGGER.warning( + "Hvac mode dry is not configured, but there is action %s configured.", + CONF_SET_HUMIDITY_ACTION, + ) + self._action_humidity = None + if self._template_target_humidity: + _LOGGER.warning( + "Hvac mode dry is not configured, but there is template %s configured.", + CONF_TARGET_HUMIDITY_TEMPLATE, + ) + self._action_humidity = None - self._script_fan_mode = None - if action_fan_mode := config.get(CONF_SET_FAN_MODE_ACTION): - self._script_fan_mode = Script( + if self._action_hvac_mode: + self._script_hvac_mode = Script( hass, - action_fan_mode, + self._action_hvac_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - self._script_swing_mode = None - if action_swing_mode := config.get(CONF_SET_SWING_MODE_ACTION): - self._script_swing_mode = Script( + if self._action_preset_mode: + self._script_preset_mode = Script( hass, - action_swing_mode, + self._action_preset_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.SWING_MODE - - self._script_set_target_temperature = None - self._script_target_temperature_low = None - self._script_target_temperature_high = None - action_target_temperature = config.get(CONF_SET_TEMPERATURE_ACTION) - action_target_temperature_low = config.get(CONF_SET_TEMPERATURE_LOW_ACTION) - action_target_temperature_high = config.get(CONF_SET_TEMPERATURE_HIGH_ACTION) - if ( - HVACMode.HEAT_COOL in self._attr_hvac_modes - and action_target_temperature_low - and action_target_temperature_high - ): - if not ( - action_target_temperature - and ( - ( - HVACMode.OFF in self._attr_hvac_modes - and len(self._attr_hvac_modes) > 2 - ) - or ( - HVACMode.OFF not in self._attr_hvac_modes - and len(self._attr_hvac_modes) > 1 - ) - ) - ): - # Set action when heat_cool and off are not the only hvac_modes. - action_target_temperature = None - elif action_target_temperature: - action_target_temperature_low = None - action_target_temperature_high = None - else: - action_target_temperature = None - action_target_temperature_low = None - action_target_temperature_high = None - if action_target_temperature: - self._script_target_temperature = Script( + if self._action_fan_mode: + self._script_fan_mode = Script( hass, - action_target_temperature, + self._action_fan_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE - if action_target_temperature_low and action_target_temperature_high: - self._script_target_temperature_low = Script( + + if self._action_swing_mode: + self._script_swing_mode = Script( hass, - action_target_temperature_low, + self._action_swing_mode, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._script_target_temperature_high = Script( + + if self._action_temperature: + self._script_temperature = Script( hass, - action_target_temperature_high, + self._action_temperature, self._attr_name, DOMAIN, script_mode=self._attr_mode_action, max_runs=self._attr_max_action, ) - self._attr_supported_features |= ( - ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - ) - self._set_target_humidity_script = None - set_target_humidity_action = config.get(CONF_SET_HUMIDITY_ACTION) - if set_target_humidity_action: - self._set_target_humidity_script = Script( + if self._action_humidity: + self._script_humidity = Script( hass, - set_target_humidity_action, + self._action_humidity, self._attr_name, DOMAIN, + script_mode=self._attr_mode_action, + max_runs=self._attr_max_action, ) - self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -435,123 +500,143 @@ async def async_added_to_hass(self): ) if ( - value := self._validate_value( - "hvac_mode", - previous_state.state, - self._attr_hvac_modes, + hvac_mode := self._validate_value( + { + "name": "hvac_mode", + "value": previous_state.state, + "format": self._attr_hvac_modes, + } ) ) is not None: - self._attr_hvac_mode = value - - if ( - value := self._validate_value( - "preset_mode", - previous_state.attributes.get( - ATTR_PRESET_MODE, - ), - self._attr_preset_modes, - ) - ) is not None: - self._attr_preset_mode = value - - if ( - value := self._validate_value( - "fan_mode", - previous_state.attributes.get( - ATTR_FAN_MODE, - ), - self._attr_fan_modes, - ) - ) is not None: - self._attr_fan_mode = value - - if ( - value := self._validate_value( - "swing_mode", - previous_state.attributes.get( - ATTR_SWING_MODE, - ), - self._attr_swing_modes, - ) - ) is not None: - self._attr_swing_mode = value - - if ( - value := self._validate_value( - "target_temperature", - previous_state.attributes.get( - ATTR_TEMPERATURE, - ), - "temperature_setpoint", - ) - ) is not None: - self._attr_target_temperature = value - - if ( - value := self._validate_value( - "target_temperature", - previous_state.attributes.get( - ATTR_TARGET_TEMP_LOW, - ), - "temperature_setpoint", - ) - ) is not None: - self._attr_target_temperature_low = value - - if ( - value := self._validate_value( - "target_temperature", - previous_state.attributes.get( - ATTR_TARGET_TEMP_HIGH, - ), - "temperature_setpoint", - ) - ) is not None: - self._attr_target_temperature_high = value + self._attr_hvac_mode = hvac_mode + + if (value := previous_state.attributes.get(ATTR_PRESET_MODE)) is not None: + if ( + preset_mode := self._validate_value( + { + "name": "preset_mode", + "value": value, + "format": self._attr_preset_modes, + } + ) + ) is not None: + self._attr_preset_mode = preset_mode + + if (value := previous_state.attributes.get(ATTR_FAN_MODE)) is not None: + if ( + fan_mode := self._validate_value( + { + "name": "fan_mode", + "value": value, + "format": self._attr_fan_modes, + } + ) + ) is not None: + self._attr_fan_mode = fan_mode + + if (value := previous_state.attributes.get(ATTR_SWING_MODE)) is not None: + if ( + swing_mode := self._validate_value( + { + "name": "swing_mode", + "value": value, + "format": self._attr_swing_modes, + } + ) + ) is not None: + self._attr_swing_mode = swing_mode + + if (value := previous_state.attributes.get(ATTR_TEMPERATURE)) is not None: + if ( + target_temperature := self._validate_value( + { + "name": "target_temperature", + "value": value, + "format": "target_temperature", + } + ) + ) is not None: + self._attr_target_temperature = target_temperature if ( - value := self._validate_value( - "target_humidity", - previous_state.attributes.get( - ATTR_HUMIDITY, - ), - "humidity_setpoint", - ) + value := previous_state.attributes.get(ATTR_TARGET_TEMP_LOW) ) is not None: - self._target_humidity = value + if ( + target_temperature_low := self._validate_value( + { + "name": "target_temperature_low", + "value": value, + "format": "target_temperature", + } + ) + ) is not None: + self._attr_target_temperature_low = target_temperature_low if ( - value := self._validate_value( - "current_temperature", - previous_state.attributes.get( - ATTR_CURRENT_TEMPERATURE, - ), - "current_temperature", - ) + value := previous_state.attributes.get(ATTR_TARGET_TEMP_HIGH) ) is not None: - self._attr_current_temperature = value + if ( + target_temperature_high := self._validate_value( + { + "name": "target_temperature_high", + "value": value, + "format": "target_temperature", + } + ) + ) is not None: + self._attr_target_temperature_high = target_temperature_high + + if (value := previous_state.attributes.get(ATTR_HUMIDITY)) is not None: + if ( + target_humidity := self._validate_value( + { + "name": "target_humidity", + "value": value, + "format": "target_humidity", + } + ) + ) is not None: + self._target_humidity = target_humidity if ( - value := self._validate_value( - "current_humidity", - previous_state.attributes.get( - ATTR_CURRENT_HUMIDITY, - ), - "current_humidity", - ) + value := previous_state.attributes.get(ATTR_CURRENT_TEMPERATURE) ) is not None: - self._attr_current_humidity = value + if ( + current_temperature := self._validate_value( + { + "name": "current_temperature", + "value": value, + "format": "current_temperature", + } + ) + ) is not None: + self._attr_current_temperature = current_temperature if ( - value := self._validate_value( - "hvac_action", - previous_state.attributes.get( - ATTR_HVAC_ACTION, - ), - [member.value for member in HVACAction], - ) + value := previous_state.attributes.get(ATTR_CURRENT_HUMIDITY) ) is not None: - self._attr_hvac_action = value + if ( + current_humidity := self._validate_value( + { + "name": "current_humidity", + "value": value, + "format": "current_humidity", + } + ) + ) is not None: + self._attr_current_humidity = current_humidity + + if (value := previous_state.attributes.get(ATTR_HVAC_ACTION)) is not None: + if ( + hvac_action := self._validate_value( + { + "name": "hvac_action", + "value": value, + "format": [member.value for member in HVACAction], + } + ) + ) is not None: + self._attr_hvac_action = hvac_action if (value := previous_state.attributes.get("last_on_mode")) is not None: for mode in value.keys(): @@ -563,30 +648,39 @@ async def async_added_to_hass(self): ) # Register templates callback. - if self._template_current_temperature: + if self._template_hvac_mode: self.add_template_attribute( - "_current_temperature", - self._template_current_temperature, + "_hvac_mode", + self._template_hvac_mode, None, - self._update_current_temperature, + self._update_hvac_mode, none_on_template_error=True, ) - if self._template_current_humidity: + if self._template_preset_mode: self.add_template_attribute( - "_current_humidity", - self._template_current_humidity, + "_preset_mode", + self._template_preset_mode, None, - self._update_current_humidity, + self._update_preset_mode, none_on_template_error=True, ) - if self._template_hvac_action: + if self._template_fan_mode: self.add_template_attribute( - "_hvac_action", - self._template_hvac_action, + "_fan_mode", + self._template_fan_mode, None, - self._update_hvac_action, + self._update_fan_mode, + none_on_template_error=True, + ) + + if self._template_swing_mode: + self.add_template_attribute( + "_swing_mode", + self._template_swing_mode, + None, + self._update_swing_mode, none_on_template_error=True, ) @@ -626,39 +720,30 @@ async def async_added_to_hass(self): none_on_template_error=True, ) - if self._template_hvac_mode: - self.add_template_attribute( - "_hvac_mode", - self._template_hvac_mode, - None, - self._update_hvac_mode, - none_on_template_error=True, - ) - - if self._template_preset_mode: + if self._template_current_temperature: self.add_template_attribute( - "_preset_mode", - self._template_preset_mode, + "_current_temperature", + self._template_current_temperature, None, - self._update_preset_mode, + self._update_current_temperature, none_on_template_error=True, ) - if self._template_fan_mode: + if self._template_current_humidity: self.add_template_attribute( - "_fan_mode", - self._template_fan_mode, + "_current_humidity", + self._template_current_humidity, None, - self._update_fan_mode, + self._update_current_humidity, none_on_template_error=True, ) - if self._template_swing_mode: + if self._template_hvac_action: self.add_template_attribute( - "_swing_mode", - self._template_swing_mode, + "_hvac_action", + self._template_hvac_action, None, - self._update_swing_mode, + self._update_hvac_action, none_on_template_error=True, ) @@ -667,127 +752,127 @@ async def async_added_to_hass(self): self._attr_name, ) - def _validate_value(self, attribute, value, format): - if value is None: - _LOGGER.debug( + def _validate_value(self, attr): + if attr["value"] is None: + _LOGGER.error( "Entity %s attribute %s returned value: None.", self._attr_name, - attribute, + attr["name"], ) return None - elif isinstance(value, TemplateError): + elif isinstance(attr["value"], TemplateError): _LOGGER.error( "Entity %s attribute %s returned exception: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - elif value in (STATE_UNKNOWN, STATE_UNAVAILABLE): + elif attr["value"] in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.info( "Entity %s attribute %s returned Uknown or Unavailable: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - elif format is not None: - if type(format) is list or type(format) is dict: - value = str(value) - if str(value) not in format: + elif attr["format"] is not None: + if type(attr["format"]) is list or type(attr["format"]) is dict: + attr["value"] = str(attr["value"]) + if str(attr["value"]) not in attr["format"]: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s. Expected one of: %s.", self._attr_name, - attribute, - value, - format, + attr["name"], + attr["value"], + attr["format"], ) return None - elif format == "current_temperature": + elif attr["format"] == "current_temperature": try: if self.precision == PRECISION_HALVES: - value = round(float(value) / 0.5) * 0.5 + attr["value"] = round(float(attr["value"]) / 0.5) * 0.5 elif self.precision == PRECISION_TENTHS: - value = round(float(value), 1) + attr["value"] = round(float(attr["value"]), 1) else: - value = round(float(value)) + attr["value"] = round(float(attr["value"])) except ValueError: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - elif format == "target_temperature": + elif attr["format"] == "target_temperature": try: - value = ( - round(float(value) / self._attr_target_temperature_step) + attr["value"] = ( + round(float(attr["value"]) / self._attr_target_temperature_step) * self._attr_target_temperature_step ) except ValueError: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s. Expected integer or float.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - if value > self._attr_max_temp: + if attr["value"] > self._attr_max_temp: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], self._attr_max_temp, ) return None - if value < self._attr_min_temp: + if attr["value"] < self._attr_min_temp: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], self._attr_min_temp, ) return None - elif format == "curent_humidity": + elif attr["format"] == "curent_humidity": try: - value = round(value) + attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - elif format == "target_humidity": + elif attr["format"] == "target_humidity": try: - value = round(value) + attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) return None - if value > self._attr_max_humidity: + if attr["value"] > self._attr_max_humidity: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], self._attr_max_humidity, ) return None - if value < self._attr_min_humidity: + if attr["value"] < self._attr_min_humidity: _LOGGER.error( "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], self._attr_min_humidity, ) return None @@ -795,163 +880,253 @@ def _validate_value(self, attribute, value, format): _LOGGER.debug( "Entity %s attribute %s triggered update with value: %s.", self._attr_name, - attribute, - value, + attr["name"], + attr["value"], ) - return value + return attr["value"] @callback - def _update_current_temperature(self, current_temperature: float): - if ( - value := self._validate_value( - "current_temperature", current_temperature, "temperature" - ) - ) is not None: - self._attr_current_temperature = value + def _update_hvac_mode(self, hvac_mode: str): + self.hass.async_create_task( + self._async_set_attribute( + "hvac_mode", + [ + { + "name": "hvac_mode", + "attr": ATTR_HVAC_MODE, + "value": hvac_mode, + "format": self._attr_hvac_modes, + } + ], + ), + ) @callback - def _update_current_humidity(self, current_humidity: float): - if ( - value := self._validate_value( - "current_humidity", current_humidity, "humidity" - ) - ) is not None: - self._attr_current_humidity = value + def _update_preset_mode(self, preset_mode: str): + self.hass.async_create_task( + self._async_set_attribute( + "preset_mode", + [ + { + "name": "preset_mode", + "attr": ATTR_PRESET_MODE, + "value": preset_mode, + "format": self._attr_preset_modes, + } + ], + ), + ) @callback - def _update_hvac_action(self, hvac_action: str): - if ( - value := self._validate_value( - "hvac_action", hvac_action, [member.value for member in HVACAction] - ) - ) is not None: - self._attr_hvac_action = value + def _update_fan_mode(self, fan_mode: str): + self.hass.async_create_task( + self._async_set_attribute( + "fan_mode", + [ + { + "name": "fan_mode", + "attr": ATTR_FAN_MODE, + "value": fan_mode, + "format": self._attr_fan_modes, + } + ], + ), + ) + + @callback + def _update_swing_mode(self, swing_mode: str): + self.hass.async_create_task( + self._async_set_attribute( + "swing_mode", + [ + { + "name": "swing_mode", + "attr": ATTR_SWING_MODE, + "value": swing_mode, + "format": self._attr_swing_modes, + } + ], + ), + ) @callback def _update_target_temperature(self, target_temperature: float): - if ( - value := self._validate_value( - "target_temperature", target_temperature, "setpoint" - ) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("target_temperature", value), - ) + self.hass.async_create_task( + self._async_set_attribute( + "temperature", + [ + { + "name": "target_temperature", + "attr": ATTR_TEMPERATURE, + "value": target_temperature, + "format": "target_temperature", + } + ], + ), + ) @callback def _update_target_temperature_low(self, target_temperature_low: float): - if ( - value := self._validate_value( - "target_temperature_low", target_temperature_low, "setpoint" - ) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("target_temperature_low", value), - ) + self.hass.async_create_task( + self._async_set_attribute( + "temperature", + [ + { + "name": "target_temperature_low", + "attr": ATTR_TARGET_TEMP_LOW, + "value": target_temperature_low, + "format": "target_temperature", + }, + { + "name": "target_temperature_high", + "attr": ATTR_TARGET_TEMP_HIGH, + "value": self._attr_target_temperature_high, + "format": "target_temperature", + }, + ], + ), + ) @callback def _update_target_temperature_high(self, target_temperature_high: float): - if ( - value := self._validate_value( - "target_temperature_high", target_temperature_high, "setpoint" - ) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("target_temperature_high", value), - ) + self.hass.async_create_task( + self._async_set_attribute( + "temperature", + [ + { + "name": "target_temperature_high", + "attr": ATTR_TARGET_TEMP_HIGH, + "value": target_temperature_high, + "format": "target_temperature", + }, + { + "name": "target_temperature_low", + "attr": ATTR_TARGET_TEMP_LOW, + "value": self._attr_target_temperature_low, + "format": "target_temperature", + }, + ], + ), + ) @callback def _update_target_humidity(self, target_humidity: int): - if ( - value := self._validate_value( - "target_humidity", target_humidity, "humidity" - ) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("target_humidity", value), - ) + self.hass.async_create_task( + self._async_set_attribute( + "humidity", + [ + { + "name": "target_humidity", + "attr": ATTR_HUMIDITY, + "value": target_humidity, + "format": "target_humidity", + } + ], + ), + ) @callback - def _update_hvac_mode(self, hvac_mode: str): + def _update_current_temperature(self, current_temperature: float): if ( - value := self._validate_value("hvac_mode", hvac_mode, self._attr_hvac_modes) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("hvac_mode", value), + value := self._validate_value( + { + "name": "current_temperature", + "value": current_temperature, + "format": "current_temperature", + } ) + ) is not None: + self._attr_current_temperature = value @callback - def _update_preset_mode(self, preset_mode: str): + def _update_current_humidity(self, current_humidity: float): if ( value := self._validate_value( - "preset_mode", preset_mode, self._attr_preset_modes + { + "name": "current_humidity", + "value": current_humidity, + "format": "current_humidity", + } ) ) is not None: - self.hass.async_create_task( - self._async_set_attribute("preset_mode", value), - ) + self._attr_current_humidity = value @callback - def _update_fan_mode(self, fan_mode: str): + def _update_hvac_action(self, hvac_action: str): if ( - value := self._validate_value("fan_mode", fan_mode, self.fan_modes) - ) is not None: - self.hass.async_create_task( - self._async_set_attribute("fan_mode", value), + value := self._validate_value( + { + "name": "hvac_action", + "value": hvac_action, + "format": [member.value for member in HVACAction], + } ) - - @callback - def _update_swing_mode(self, swing_mode: str): - if ( - value := self._validate_value("swing_mode", swing_mode, self.swing_modes) ) is not None: - self.hass.async_create_task(self._async_set_attribute("swing_mode", value)) + self._attr_hvac_action = value - async def _async_set_attribute(self, attr, value) -> None: - if getattr(self, "_attr_" + attr) == value: - # Nothing to do. - _LOGGER.debug( - "Entity %s attribute %s is already set to value: %s.", - self._attr_name, - attr, - value, - ) - return + async def _async_set_attribute(self, action, attributes) -> None: + variables = {} + for attr in attributes: + # Validate input values + if (value := self._validate_value(attr)) is not None: + attr["value"] = value + variables[attr["attr"]] = value + else: + _LOGGER.error( + "Entity %s attribute %s value %s is invalid.", + self._attr_name, + attr["name"], + attr["value"], + ) + return False + + if getattr(self, "_attr_" + attr["name"]) == attr["value"]: + # Nothing to do. + _LOGGER.debug( + "Entity %s attribute %s is already set to value: %s.", + self._attr_name, + attr["name"], + attr["value"], + ) + else: + # Update entity attribute. + _LOGGER.debug( + "Entity %s updating attribute %s to value: %s.", + self._attr_name, + attr["name"], + attr["value"], + ) + setattr(self, "_attr_" + attr["name"], attr["value"]) + # Update last_on modes if not off mode. + if ( + attr["name"] in self._off_mode.keys() + and attr["value"] != self._off_mode[attr["name"]] + ): + self._last_on_mode[attr["name"]] = attr["value"] - # Update entity attribute. - _LOGGER.debug( - "Entity %s updating attribute %s to value: %s.", - self._attr_name, - attr, - value, - ) - setattr(self, "_attr_" + attr, value) - # Update last_on modes if not off mode. - if attr in self._off_mode.keys() and value != self._off_mode[attr]: - self._last_on_mode[attr] = value self.async_write_ha_state() - if script := getattr(self, "_script_" + attr): + if script := getattr(self, "_script_" + action): # Create a context referring to the trigger context. trigger_context_id = None if self._context is None else self._context.id script_context = Context(parent_id=trigger_context_id) # Execute set action script. _LOGGER.debug( - "Entity %s executing script set_action_%s with attribute %s set to value: %s.", + "Entity %s executing script set_action_%s variables: %s.", self._attr_name, - attr, - attr, - value, + action, + variables, ) await script.async_run( - run_variables={attr: value}, + run_variables=variables, context=script_context, ) _LOGGER.debug( "Entity %s execution of script set_action_%s finished.", self._attr_name, - attr, + action, ) + return True @property def state(self): @@ -976,101 +1151,139 @@ async def async_turn_on(self) -> None: if "hvac_mode" in self._last_on_mode.keys(): await self.async_set_hvac_mode(self._last_on_mode["hvac_mode"]) + async def async_toggle(self) -> None: + """Toggle climate.""" + if ( + "hvac_mode" in self._off_mode.keys() + and "hvac_mode" in self._last_on_mode.keys() + ): + if self._attr_hvac_mode == self._off_mode["hvac_mode"]: + await self.async_set_hvac_mode(self._last_on_mode["hvac_mode"]) + elif self._attr_hvac_mode == self._last_on_mode["hvac_mode"]: + await self.async_set_hvac_mode(self._off_mode["hvac_mode"]) + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new hvac mode.""" - if ( - value := self._validate_value( - "hvac_mode", - hvac_mode, - self._attr_hvac_modes, - ) - ) is not None: - await self._async_set_attribute("hvac_mode", value) + await self._async_set_attribute( + "hvac_mode", + [ + { + "name": "hvac_mode", + "attr": ATTR_HVAC_MODE, + "value": hvac_mode, + "format": self._attr_hvac_modes, + } + ], + ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if ( - value := self._validate_value( - "preset_mode", - preset_mode, - self._attr_preset_modes, - ) - ) is not None: - await self._async_set_attribute("preset_mode", value) + await self._async_set_attribute( + "preset_mode", + [ + { + "name": "preset_mode", + "attr": ATTR_PRESET_MODE, + "value": preset_mode, + "format": self._attr_preset_modes, + } + ], + ) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" - if ( - value := self._validate_value( - "fan_mode", - fan_mode, - self._attr_fan_modes, - ) - ) is not None: - await self._async_set_attribute("fan_mode", value) + await self._async_set_attribute( + "fan_mode", + [ + { + "name": "fan_mode", + "attr": ATTR_FAN_MODE, + "value": fan_mode, + "format": self._attr_fan_modes, + } + ], + ) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" - if ( - value := self._validate_value( - "swing_mode", - swing_mode, - self._attr_swing_modes, - ) - ) is not None: - await self._async_set_attribute("swing_mode", value) + await self._async_set_attribute( + "swing_mode", + [ + { + "name": "swing_mode", + "attr": ATTR_SWING_MODE, + "value": swing_mode, + "format": self._attr_swing_modes, + } + ], + ) async def async_set_humidity(self, target_humidity: int) -> None: """Set new humidity target.""" - if ( - value := self._validate_value( - "target_humidity", - target_humidity, - "target_humidity", - ) - ) is not None: - await self._async_set_attribute("target_humidity", value) + await self._async_set_attribute( + "humidity", + [ + { + "name": "target_humidity", + "attr": ATTR_HUMIDITY, + "value": target_humidity, + "format": "target_humidity", + } + ], + ) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" - if ( - hvac_mode := self._validate_value( - "hvac_mode", - kwargs.get(ATTR_HVAC_MODE), - self._attr_hvac_modes, + attributes = [] + hvac_mode = self._attr_hvac_mode + if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: + attributes.append( + { + "name": "hvac_mode", + "attr": ATTR_HVAC_MODE, + "value": hvac_mode, + "format": self._attr_hvac_modes, + } ) - ) is not None: - await self._async_set_attribute("hvac_mode", hvac_mode) - - if self._attr_hvac_mode == HVACMode.HEAT_COOL: - if ( - target_temperature_low := self._validate_value( - "target_temperature_low", - kwargs.get(ATTR_TARGET_TEMP_LOW), - "target_temperature", - ) - ) is not None: - await self._async_set_attribute( - "target_temperature_low", target_temperature_low + else: + hvac_mode = self._attr_hvac_mode + + if hvac_mode == HVACMode.HEAT_COOL: + if (target_temperature_low := kwargs.get(ATTR_TARGET_TEMP_LOW)) is not None: + attributes.append( + { + "name": "target_temperature_low", + "attr": ATTR_TARGET_TEMP_LOW, + "value": target_temperature_low, + "format": "target_temperature", + } ) + else: + return False if ( - target_temperature_high := self._validate_value( - "target_temperature_high", - kwargs.get(ATTR_TARGET_TEMP_HIGH), - "target_temperature", - ) + target_temperature_high := kwargs.get(ATTR_TARGET_TEMP_HIGH) ) is not None: - await self._async_set_attribute( - "target_temperature_high", target_temperature_high + attributes.append( + { + "name": "target_temperature_high", + "attr": ATTR_TARGET_TEMP_HIGH, + "value": target_temperature_high, + "format": "target_temperature", + } ) + else: + return False else: - if ( - target_temperature := self._validate_value( - "target_temperature", - kwargs.get(ATTR_TEMPERATURE), - "target_temperature", - ) - ) is not None: - await self._async_set_attribute( - "target_temperature", target_temperature + if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: + attributes.append( + { + "name": "target_temperature", + "attr": ATTR_TEMPERATURE, + "value": target_temperature, + "format": "target_temperature", + } ) + else: + return False + + await self._async_set_attribute("temperature", attributes) From 2a8c48662d0c1e77ee510fc5dfb678163ee46d4b Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:49:58 +0200 Subject: [PATCH 5/8] tweaked some logging levels added entity name into all log messages --- custom_components/climate_template/climate.py | 95 ++++++++++++------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index fa7bf43..d6e60b4 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -277,7 +277,10 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): else: self._last_on_mode["hvac_mode"] = HVACMode.OFF else: - _LOGGER.error("At least one hvac mode shall be configured!") + _LOGGER.error( + "Entity %s has no hvac mode, at least one hvac mode shall be configured!", + self._attr_name, + ) return False if self._attr_preset_modes and len(self._attr_preset_modes) >= 2: @@ -285,14 +288,16 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE else: _LOGGER.warning( - "Preset mode are configured, but there is neither action %s nor template %s configured.", + "Entity %s has preset mode configured, but there is neither action %s nor template %s configured.", + self._attr_name, CONF_SET_PRESET_MODE_ACTION, CONF_PRESET_MODE_TEMPLATE, ) self._attr_preset_modes = [] elif self._action_preset_mode or self._template_preset_mode: _LOGGER.warning( - "Preset mode are not configured, but there is action %s and/or template %s configured.", + "Entity %s has no preset mode configured, but there is action %s and/or template %s configured.", + self._attr_name, CONF_SET_PRESET_MODE_ACTION, CONF_PRESET_MODE_TEMPLATE, ) @@ -304,14 +309,16 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.FAN_MODE else: _LOGGER.warning( - "Fan mode are configured, but there is neither action %s nor template %s configured.", + "Entity %s has fan mode configured, but there is neither action %s nor template %s configured.", + self._attr_name, CONF_SET_FAN_MODE_ACTION, CONF_FAN_MODE_TEMPLATE, ) self._attr_fan_modes = [] elif self._action_fan_mode or self._template_fan_mode: _LOGGER.warning( - "Fan mode are not configured, but there is action %s and/or template %s configured.", + "Entity %s has not fan mode configured, but there is action %s and/or template %s configured.", + self._attr_name, CONF_SET_FAN_MODE_ACTION, CONF_FAN_MODE_TEMPLATE, ) @@ -323,14 +330,16 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.SWING_MODE else: _LOGGER.warning( - "Swing mode are configured, but there is neither action %s nor template %s configured.", + "Entity %s has swing mode configured, but there is neither action %s nor template %s configured.", + self._attr_name, CONF_SET_SWING_MODE_ACTION, CONF_SWING_MODE_TEMPLATE, ) self._attr_swing_modes = [] elif self._action_swing_mode or self._template_swing_mode: _LOGGER.warning( - "Swing mode are not configured, but there is action %s and/or template %s configured.", + "Entity %s has no swing mode configured, but there is action %s and/or template %s configured.", + self._attr_name, CONF_SET_SWING_MODE_ACTION, CONF_SWING_MODE_TEMPLATE, ) @@ -346,7 +355,8 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): and self._template_target_temperature_high ): _LOGGER.warning( - "Either both templates %s and %s shall be configured or none of them.", + "Entity %s shall have either both templates %s and %s configured or none of them.", + self._attr_name, CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, ) @@ -361,7 +371,8 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): ) else: _LOGGER.warning( - "Hvac mode heat_cool is configured, but there is no action neither template for high or low temperature configued." + "Entity %s has hvac mode heat_cool configured, but there is no action neither template for high or low temperature configued.", + self._attr_name, ) else: if ( @@ -369,7 +380,8 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): or self._template_target_temperature_high ): _LOGGER.warning( - "Hvac mode heat_cool is not configured, but there is %s and/or %s template configured.", + "Entity %s has no Hvac mode heat_cool configured, but there is %s and/or %s template configured.", + self._attr_name, CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, ) @@ -388,18 +400,21 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE else: _LOGGER.warning( - "Hvac mode auto, heat or cool is configured, but there is no action neither template for temperature configued." + "Entity %s has hvac mode auto, heat or cool configured, but there is no action neither template for temperature configued.", + self._attr_name, ) else: if self._action_temperature: _LOGGER.warning( - "Hvac mode auto, heat or cool is not configured, but there is action %s configured.", + "Entity %s has no hvac mode auto, heat or cool configured, but there is action %s configured.", + self._attr_name, CONF_SET_TEMPERATURE_ACTION, ) self._action_temperature = None if self._template_target_temperature: _LOGGER.warning( - "Hvac mode auto, heat or cool is not configured, but there is template %s configured.", + "Entity %s has no hvac mode auto, heat or cool configured, but there is template %s configured.", + self._attr_name, CONF_TARGET_TEMPERATURE_TEMPLATE, ) self._template_target_temperature = None @@ -409,20 +424,23 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY else: _LOGGER.warning( - "Hvac mode dry is configured, but there is no %s action neither %s template configured.", + "Entity %s has hvac mode dry configured, but there is no %s action neither %s template configured.", + self._attr_name, CONF_SET_HUMIDITY_ACTION, CONF_TARGET_HUMIDITY_TEMPLATE, ) else: if self._action_humidity: _LOGGER.warning( - "Hvac mode dry is not configured, but there is action %s configured.", + "Entity %s has no hvac mode dry configured, but there is action %s configured.", + self._attr_name, CONF_SET_HUMIDITY_ACTION, ) self._action_humidity = None if self._template_target_humidity: _LOGGER.warning( - "Hvac mode dry is not configured, but there is template %s configured.", + "Entity %s has no hvac mode dry configured, but there is template %s configured.", + self._attr_name, CONF_TARGET_HUMIDITY_TEMPLATE, ) self._action_humidity = None @@ -755,22 +773,22 @@ async def async_added_to_hass(self): def _validate_value(self, attr): if attr["value"] is None: _LOGGER.error( - "Entity %s attribute %s returned value: None.", + "Entity %s attribute %s returned value: 'None'.", self._attr_name, attr["name"], ) return None elif isinstance(attr["value"], TemplateError): _LOGGER.error( - "Entity %s attribute %s returned exception: %s.", + "Entity %s attribute %s returned exception: '%s'.", self._attr_name, attr["name"], attr["value"], ) return None elif attr["value"] in (STATE_UNKNOWN, STATE_UNAVAILABLE): - _LOGGER.info( - "Entity %s attribute %s returned Uknown or Unavailable: %s.", + _LOGGER.debug( + "Entity %s attribute %s returned Uknown or Unavailable: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -779,9 +797,9 @@ def _validate_value(self, attr): elif attr["format"] is not None: if type(attr["format"]) is list or type(attr["format"]) is dict: attr["value"] = str(attr["value"]) - if str(attr["value"]) not in attr["format"]: + if attr["value"] not in attr["format"]: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s. Expected one of: %s.", + "Entity %s attribute %s returned invalid value: '%s'. Expected one of: %s.", self._attr_name, attr["name"], attr["value"], @@ -798,7 +816,7 @@ def _validate_value(self, attr): attr["value"] = round(float(attr["value"])) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + "Entity %s attribute %s returned invalid value: '%s'. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -812,7 +830,7 @@ def _validate_value(self, attr): ) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s. Expected integer or float.", + "Entity %s attribute %s returned invalid value: '%s'. Expected integer or float.", self._attr_name, attr["name"], attr["value"], @@ -820,7 +838,7 @@ def _validate_value(self, attr): return None if attr["value"] > self._attr_max_temp: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", + "Entity %s attribute %s returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -829,7 +847,7 @@ def _validate_value(self, attr): return None if attr["value"] < self._attr_min_temp: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", + "Entity %s attribute %s returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -841,7 +859,7 @@ def _validate_value(self, attr): attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + "Entity %s attribute %s returned invalid value: '%s'. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -852,7 +870,7 @@ def _validate_value(self, attr): attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s. Expected integer of float.", + "Entity %s attribute %s returned invalid value: '%'s. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -860,7 +878,7 @@ def _validate_value(self, attr): return None if attr["value"] > self._attr_max_humidity: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s, which is bigger than max setpoint: %s.", + "Entity %s attribute %s returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -869,16 +887,23 @@ def _validate_value(self, attr): return None if attr["value"] < self._attr_min_humidity: _LOGGER.error( - "Entity %s attribute %s returned invalid value: %s, which is smaller than min setpoint: %s.", + "Entity %s attribute %s returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], self._attr_min_humidity, ) return None + else: + _LOGGER.debug( + "Entity %s attribute %s test called with invalid format: '%s'.", + self._attr_name, + attr["name"], + attr["format"], + ) _LOGGER.debug( - "Entity %s attribute %s triggered update with value: %s.", + "Entity %s attribute %s triggered update with value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1072,8 +1097,8 @@ async def _async_set_attribute(self, action, attributes) -> None: attr["value"] = value variables[attr["attr"]] = value else: - _LOGGER.error( - "Entity %s attribute %s value %s is invalid.", + _LOGGER.debug( + "Entity %s attribute %s update called with invalid value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1083,7 +1108,7 @@ async def _async_set_attribute(self, action, attributes) -> None: if getattr(self, "_attr_" + attr["name"]) == attr["value"]: # Nothing to do. _LOGGER.debug( - "Entity %s attribute %s is already set to value: %s.", + "Entity %s attribute %s is already set to value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1091,7 +1116,7 @@ async def _async_set_attribute(self, action, attributes) -> None: else: # Update entity attribute. _LOGGER.debug( - "Entity %s updating attribute %s to value: %s.", + "Entity %s updating attribute %s to value: '%s'.", self._attr_name, attr["name"], attr["value"], From 260e603b5e8b5128a17f3cdc994557b5ddbd8853 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:51:20 +0200 Subject: [PATCH 6/8] improved logging messages fixed 'Already running' for some cases - do not execute script if attribute value is not changed --- custom_components/climate_template/climate.py | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index d6e60b4..36b7c74 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -278,7 +278,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._last_on_mode["hvac_mode"] = HVACMode.OFF else: _LOGGER.error( - "Entity %s has no hvac mode, at least one hvac mode shall be configured!", + "Entity '%s' has no hvac mode, at least one hvac mode shall be configured!", self._attr_name, ) return False @@ -288,7 +288,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE else: _LOGGER.warning( - "Entity %s has preset mode configured, but there is neither action %s nor template %s configured.", + "Entity '%s' has preset mode configured, but there is neither action '%s' nor template '%s' configured.", self._attr_name, CONF_SET_PRESET_MODE_ACTION, CONF_PRESET_MODE_TEMPLATE, @@ -296,7 +296,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_preset_modes = [] elif self._action_preset_mode or self._template_preset_mode: _LOGGER.warning( - "Entity %s has no preset mode configured, but there is action %s and/or template %s configured.", + "Entity '%s' has no preset mode configured, but there is action '%s' and/or template '%s' configured.", self._attr_name, CONF_SET_PRESET_MODE_ACTION, CONF_PRESET_MODE_TEMPLATE, @@ -309,7 +309,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.FAN_MODE else: _LOGGER.warning( - "Entity %s has fan mode configured, but there is neither action %s nor template %s configured.", + "Entity '%s' has fan mode configured, but there is neither action '%s' nor template '%s' configured.", self._attr_name, CONF_SET_FAN_MODE_ACTION, CONF_FAN_MODE_TEMPLATE, @@ -317,7 +317,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_fan_modes = [] elif self._action_fan_mode or self._template_fan_mode: _LOGGER.warning( - "Entity %s has not fan mode configured, but there is action %s and/or template %s configured.", + "Entity '%s' has not fan mode configured, but there is action '%s' and/or template '%s' configured.", self._attr_name, CONF_SET_FAN_MODE_ACTION, CONF_FAN_MODE_TEMPLATE, @@ -330,7 +330,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.SWING_MODE else: _LOGGER.warning( - "Entity %s has swing mode configured, but there is neither action %s nor template %s configured.", + "Entity '%s' has swing mode configured, but there is neither action '%s' nor template '%s' configured.", self._attr_name, CONF_SET_SWING_MODE_ACTION, CONF_SWING_MODE_TEMPLATE, @@ -338,7 +338,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_swing_modes = [] elif self._action_swing_mode or self._template_swing_mode: _LOGGER.warning( - "Entity %s has no swing mode configured, but there is action %s and/or template %s configured.", + "Entity '%s' has no swing mode configured, but there is action '%s' and/or template '%s' configured.", self._attr_name, CONF_SET_SWING_MODE_ACTION, CONF_SWING_MODE_TEMPLATE, @@ -355,7 +355,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): and self._template_target_temperature_high ): _LOGGER.warning( - "Entity %s shall have either both templates %s and %s configured or none of them.", + "Entity '%s' shall have either both templates '%s' and '%s' configured or none of them.", self._attr_name, CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, @@ -371,7 +371,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): ) else: _LOGGER.warning( - "Entity %s has hvac mode heat_cool configured, but there is no action neither template for high or low temperature configued.", + "Entity '%s' has hvac mode heat_cool configured, but there is no action neither template for high or low temperature configued.", self._attr_name, ) else: @@ -380,7 +380,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): or self._template_target_temperature_high ): _LOGGER.warning( - "Entity %s has no Hvac mode heat_cool configured, but there is %s and/or %s template configured.", + "Entity '%s' has no Hvac mode heat_cool configured, but there is '%s' and/or '%s' template configured.", self._attr_name, CONF_TARGET_TEMPERATURE_LOW_TEMPLATE, CONF_TARGET_TEMPERATURE_HIGH_TEMPLATE, @@ -400,20 +400,20 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE else: _LOGGER.warning( - "Entity %s has hvac mode auto, heat or cool configured, but there is no action neither template for temperature configued.", + "Entity '%s' has hvac mode auto, heat or cool configured, but there is no action neither template for temperature configued.", self._attr_name, ) else: if self._action_temperature: _LOGGER.warning( - "Entity %s has no hvac mode auto, heat or cool configured, but there is action %s configured.", + "Entity '%s' has no hvac mode auto, heat or cool configured, but there is action '%s' configured.", self._attr_name, CONF_SET_TEMPERATURE_ACTION, ) self._action_temperature = None if self._template_target_temperature: _LOGGER.warning( - "Entity %s has no hvac mode auto, heat or cool configured, but there is template %s configured.", + "Entity '%s' has no hvac mode auto, heat or cool configured, but there is template '%s' configured.", self._attr_name, CONF_TARGET_TEMPERATURE_TEMPLATE, ) @@ -424,7 +424,7 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY else: _LOGGER.warning( - "Entity %s has hvac mode dry configured, but there is no %s action neither %s template configured.", + "Entity '%s' has hvac mode dry configured, but there is no '%s' action neither '%s' template configured.", self._attr_name, CONF_SET_HUMIDITY_ACTION, CONF_TARGET_HUMIDITY_TEMPLATE, @@ -432,14 +432,14 @@ def __init__(self, hass: HomeAssistantType, config: ConfigType): else: if self._action_humidity: _LOGGER.warning( - "Entity %s has no hvac mode dry configured, but there is action %s configured.", + "Entity '%s' has no hvac mode dry configured, but there is action '%s' configured.", self._attr_name, CONF_SET_HUMIDITY_ACTION, ) self._action_humidity = None if self._template_target_humidity: _LOGGER.warning( - "Entity %s has no hvac mode dry configured, but there is template %s configured.", + "Entity '%s' has no hvac mode dry configured, but there is template '%s' configured.", self._attr_name, CONF_TARGET_HUMIDITY_TEMPLATE, ) @@ -513,7 +513,7 @@ async def async_added_to_hass(self): previous_state = await self.async_get_last_state() if previous_state is not None: _LOGGER.debug( - "Entity %s restoring previously stored attributes.", + "Entity '%s' restoring previously stored attributes.", self._attr_name, ) @@ -661,7 +661,7 @@ async def async_added_to_hass(self): self._last_on_mode[mode] = value[mode] _LOGGER.debug( - "Entity %s registering templates callbacks.", + "Entity '%s' registering templates callbacks.", self._attr_name, ) @@ -766,21 +766,21 @@ async def async_added_to_hass(self): ) _LOGGER.debug( - "Entity %s succesfully registered to homeassistant.", + "Entity '%s' succesfully registered to homeassistant.", self._attr_name, ) def _validate_value(self, attr): if attr["value"] is None: _LOGGER.error( - "Entity %s attribute %s returned value: 'None'.", + "Entity '%s' attribute '%s' returned value: 'None'.", self._attr_name, attr["name"], ) return None elif isinstance(attr["value"], TemplateError): _LOGGER.error( - "Entity %s attribute %s returned exception: '%s'.", + "Entity '%s' attribute '%s' returned exception: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -788,7 +788,7 @@ def _validate_value(self, attr): return None elif attr["value"] in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.debug( - "Entity %s attribute %s returned Uknown or Unavailable: '%s'.", + "Entity '%s' attribute '%s' returned Uknown or Unavailable: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -799,7 +799,7 @@ def _validate_value(self, attr): attr["value"] = str(attr["value"]) if attr["value"] not in attr["format"]: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s'. Expected one of: %s.", + "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected one of: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -816,7 +816,7 @@ def _validate_value(self, attr): attr["value"] = round(float(attr["value"])) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s'. Expected integer of float.", + "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -830,7 +830,7 @@ def _validate_value(self, attr): ) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s'. Expected integer or float.", + "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer or float.", self._attr_name, attr["name"], attr["value"], @@ -838,7 +838,7 @@ def _validate_value(self, attr): return None if attr["value"] > self._attr_max_temp: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s', which is bigger than max setpoint: '%s'.", + "Entity '%s' attribute '%s' returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -847,7 +847,7 @@ def _validate_value(self, attr): return None if attr["value"] < self._attr_min_temp: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s', which is smaller than min setpoint: '%s'.", + "Entity '%s' attribute '%s' returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -859,7 +859,7 @@ def _validate_value(self, attr): attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s'. Expected integer of float.", + "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -870,7 +870,7 @@ def _validate_value(self, attr): attr["value"] = round(attr["value"]) except ValueError: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%'s. Expected integer of float.", + "Entity '%s' attribute '%s' returned invalid value: '%'s. Expected integer of float.", self._attr_name, attr["name"], attr["value"], @@ -878,7 +878,7 @@ def _validate_value(self, attr): return None if attr["value"] > self._attr_max_humidity: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s', which is bigger than max setpoint: '%s'.", + "Entity '%s' attribute '%s' returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -887,7 +887,7 @@ def _validate_value(self, attr): return None if attr["value"] < self._attr_min_humidity: _LOGGER.error( - "Entity %s attribute %s returned invalid value: '%s', which is smaller than min setpoint: '%s'.", + "Entity '%s' attribute '%s' returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -896,14 +896,14 @@ def _validate_value(self, attr): return None else: _LOGGER.debug( - "Entity %s attribute %s test called with invalid format: '%s'.", + "Entity '%s' attribute '%s' test called with invalid format: '%s'.", self._attr_name, attr["name"], attr["format"], ) _LOGGER.debug( - "Entity %s attribute %s triggered update with value: '%s'.", + "Entity '%s' attribute '%s' triggered update with value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1090,6 +1090,7 @@ def _update_hvac_action(self, hvac_action: str): self._attr_hvac_action = value async def _async_set_attribute(self, action, attributes) -> None: + update = False variables = {} for attr in attributes: # Validate input values @@ -1098,7 +1099,7 @@ async def _async_set_attribute(self, action, attributes) -> None: variables[attr["attr"]] = value else: _LOGGER.debug( - "Entity %s attribute %s update called with invalid value: '%s'.", + "Entity '%s' attribute '%s' update called with invalid value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1108,15 +1109,16 @@ async def _async_set_attribute(self, action, attributes) -> None: if getattr(self, "_attr_" + attr["name"]) == attr["value"]: # Nothing to do. _LOGGER.debug( - "Entity %s attribute %s is already set to value: '%s'.", + "Entity '%s' attribute '%s' is already set to value: '%s'.", self._attr_name, attr["name"], attr["value"], ) else: + update = True # Update entity attribute. _LOGGER.debug( - "Entity %s updating attribute %s to value: '%s'.", + "Entity '%s' updating attribute '%s' to value: '%s'.", self._attr_name, attr["name"], attr["value"], @@ -1131,13 +1133,13 @@ async def _async_set_attribute(self, action, attributes) -> None: self.async_write_ha_state() - if script := getattr(self, "_script_" + action): + if update and (script := getattr(self, "_script_" + action)): # Create a context referring to the trigger context. trigger_context_id = None if self._context is None else self._context.id script_context = Context(parent_id=trigger_context_id) # Execute set action script. _LOGGER.debug( - "Entity %s executing script set_action_%s variables: %s.", + "Entity '%s' executing script 'set_action_%s' variables: '%s'.", self._attr_name, action, variables, @@ -1147,7 +1149,7 @@ async def _async_set_attribute(self, action, attributes) -> None: context=script_context, ) _LOGGER.debug( - "Entity %s execution of script set_action_%s finished.", + "Entity '%s' execution of script 'set_action_%s' finished.", self._attr_name, action, ) From 00fbfa4ee6bfa15514245fbd25b6602cb1333c74 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:32:25 +0200 Subject: [PATCH 7/8] code refactoring --- custom_components/climate_template/climate.py | 398 ++++++++---------- 1 file changed, 169 insertions(+), 229 deletions(-) diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 36b7c74..0d3d5f4 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -519,11 +519,7 @@ async def async_added_to_hass(self): if ( hvac_mode := self._validate_value( - { - "name": "hvac_mode", - "value": previous_state.state, - "format": self._attr_hvac_modes, - } + "hvac_mode", previous_state.state, self._attr_hvac_modes ) ) is not None: self._attr_hvac_mode = hvac_mode @@ -531,11 +527,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_PRESET_MODE)) is not None: if ( preset_mode := self._validate_value( - { - "name": "preset_mode", - "value": value, - "format": self._attr_preset_modes, - } + "preset_mode", + value, + self._attr_preset_modes, ) ) is not None: self._attr_preset_mode = preset_mode @@ -543,11 +537,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_FAN_MODE)) is not None: if ( fan_mode := self._validate_value( - { - "name": "fan_mode", - "value": value, - "format": self._attr_fan_modes, - } + "fan_mode", + value, + self._attr_fan_modes, ) ) is not None: self._attr_fan_mode = fan_mode @@ -555,11 +547,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_SWING_MODE)) is not None: if ( swing_mode := self._validate_value( - { - "name": "swing_mode", - "value": value, - "format": self._attr_swing_modes, - } + "swing_mode", + value, + self._attr_swing_modes, ) ) is not None: self._attr_swing_mode = swing_mode @@ -567,11 +557,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_TEMPERATURE)) is not None: if ( target_temperature := self._validate_value( - { - "name": "target_temperature", - "value": value, - "format": "target_temperature", - } + "target_temperature", + value, + "target_temperature", ) ) is not None: self._attr_target_temperature = target_temperature @@ -581,11 +569,9 @@ async def async_added_to_hass(self): ) is not None: if ( target_temperature_low := self._validate_value( - { - "name": "target_temperature_low", - "value": value, - "format": "target_temperature", - } + "target_temperature_low", + value, + "target_temperature", ) ) is not None: self._attr_target_temperature_low = target_temperature_low @@ -595,11 +581,9 @@ async def async_added_to_hass(self): ) is not None: if ( target_temperature_high := self._validate_value( - { - "name": "target_temperature_high", - "value": value, - "format": "target_temperature", - } + "target_temperature_high", + value, + "target_temperature", ) ) is not None: self._attr_target_temperature_high = target_temperature_high @@ -607,11 +591,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_HUMIDITY)) is not None: if ( target_humidity := self._validate_value( - { - "name": "target_humidity", - "value": value, - "format": "target_humidity", - } + "target_humidity", + value, + "target_humidity", ) ) is not None: self._target_humidity = target_humidity @@ -621,11 +603,9 @@ async def async_added_to_hass(self): ) is not None: if ( current_temperature := self._validate_value( - { - "name": "current_temperature", - "value": value, - "format": "current_temperature", - } + "current_temperature", + value, + "current_temperature", ) ) is not None: self._attr_current_temperature = current_temperature @@ -635,11 +615,9 @@ async def async_added_to_hass(self): ) is not None: if ( current_humidity := self._validate_value( - { - "name": "current_humidity", - "value": value, - "format": "current_humidity", - } + "current_humidity", + value, + "current_humidity", ) ) is not None: self._attr_current_humidity = current_humidity @@ -647,11 +625,9 @@ async def async_added_to_hass(self): if (value := previous_state.attributes.get(ATTR_HVAC_ACTION)) is not None: if ( hvac_action := self._validate_value( - { - "name": "hvac_action", - "value": value, - "format": [member.value for member in HVACAction], - } + "hvac_action", + value, + [member.value for member in HVACAction], ) ) is not None: self._attr_hvac_action = hvac_action @@ -770,127 +746,127 @@ async def async_added_to_hass(self): self._attr_name, ) - def _validate_value(self, attr): - if attr["value"] is None: + def _validate_value(self, attr, value, format): + if value is None: _LOGGER.error( "Entity '%s' attribute '%s' returned value: 'None'.", self._attr_name, - attr["name"], + attr, ) return None - elif isinstance(attr["value"], TemplateError): + elif isinstance(value, TemplateError): _LOGGER.error( "Entity '%s' attribute '%s' returned exception: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - elif attr["value"] in (STATE_UNKNOWN, STATE_UNAVAILABLE): + elif value in (STATE_UNKNOWN, STATE_UNAVAILABLE): _LOGGER.debug( "Entity '%s' attribute '%s' returned Uknown or Unavailable: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - elif attr["format"] is not None: - if type(attr["format"]) is list or type(attr["format"]) is dict: - attr["value"] = str(attr["value"]) - if attr["value"] not in attr["format"]: + elif format is not None: + if type(format) is list or type(format) is dict: + value = str(value) + if value not in format: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected one of: '%s'.", self._attr_name, - attr["name"], - attr["value"], - attr["format"], + attr, + value, + format, ) return None - elif attr["format"] == "current_temperature": + elif format == "current_temperature": try: if self.precision == PRECISION_HALVES: - attr["value"] = round(float(attr["value"]) / 0.5) * 0.5 + value = round(float(value) / 0.5) * 0.5 elif self.precision == PRECISION_TENTHS: - attr["value"] = round(float(attr["value"]), 1) + value = round(float(value), 1) else: - attr["value"] = round(float(attr["value"])) + value = round(float(value)) except ValueError: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer of float.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - elif attr["format"] == "target_temperature": + elif format == "target_temperature": try: - attr["value"] = ( - round(float(attr["value"]) / self._attr_target_temperature_step) + value = ( + round(float(value) / self._attr_target_temperature_step) * self._attr_target_temperature_step ) except ValueError: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer or float.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - if attr["value"] > self._attr_max_temp: + if value > self._attr_max_temp: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, self._attr_max_temp, ) return None - if attr["value"] < self._attr_min_temp: + if value < self._attr_min_temp: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, self._attr_min_temp, ) return None - elif attr["format"] == "curent_humidity": + elif format == "curent_humidity": try: - attr["value"] = round(attr["value"]) + value = round(value) except ValueError: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s'. Expected integer of float.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - elif attr["format"] == "target_humidity": + elif format == "target_humidity": try: - attr["value"] = round(attr["value"]) + value = round(value) except ValueError: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%'s. Expected integer of float.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) return None - if attr["value"] > self._attr_max_humidity: + if value > self._attr_max_humidity: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s', which is bigger than max setpoint: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, self._attr_max_humidity, ) return None - if attr["value"] < self._attr_min_humidity: + if value < self._attr_min_humidity: _LOGGER.error( "Entity '%s' attribute '%s' returned invalid value: '%s', which is smaller than min setpoint: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, self._attr_min_humidity, ) return None @@ -898,31 +874,30 @@ def _validate_value(self, attr): _LOGGER.debug( "Entity '%s' attribute '%s' test called with invalid format: '%s'.", self._attr_name, - attr["name"], - attr["format"], + attr, + format, ) _LOGGER.debug( "Entity '%s' attribute '%s' triggered update with value: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + value, ) - return attr["value"] + return value @callback def _update_hvac_mode(self, hvac_mode: str): self.hass.async_create_task( self._async_set_attribute( "hvac_mode", - [ - { - "name": "hvac_mode", + { + "hvac_mode": { "attr": ATTR_HVAC_MODE, "value": hvac_mode, "format": self._attr_hvac_modes, } - ], + }, ), ) @@ -931,14 +906,13 @@ def _update_preset_mode(self, preset_mode: str): self.hass.async_create_task( self._async_set_attribute( "preset_mode", - [ - { - "name": "preset_mode", + { + "preset_mode": { "attr": ATTR_PRESET_MODE, "value": preset_mode, "format": self._attr_preset_modes, } - ], + }, ), ) @@ -947,14 +921,13 @@ def _update_fan_mode(self, fan_mode: str): self.hass.async_create_task( self._async_set_attribute( "fan_mode", - [ - { - "name": "fan_mode", + { + "fan_mode": { "attr": ATTR_FAN_MODE, "value": fan_mode, "format": self._attr_fan_modes, } - ], + }, ), ) @@ -963,14 +936,13 @@ def _update_swing_mode(self, swing_mode: str): self.hass.async_create_task( self._async_set_attribute( "swing_mode", - [ - { - "name": "swing_mode", + { + "swing_mode": { "attr": ATTR_SWING_MODE, "value": swing_mode, "format": self._attr_swing_modes, } - ], + }, ), ) @@ -979,14 +951,13 @@ def _update_target_temperature(self, target_temperature: float): self.hass.async_create_task( self._async_set_attribute( "temperature", - [ - { - "name": "target_temperature", + { + "target_temperature": { "attr": ATTR_TEMPERATURE, "value": target_temperature, "format": "target_temperature", } - ], + }, ), ) @@ -995,20 +966,18 @@ def _update_target_temperature_low(self, target_temperature_low: float): self.hass.async_create_task( self._async_set_attribute( "temperature", - [ - { - "name": "target_temperature_low", + { + "target_temperature_low": { "attr": ATTR_TARGET_TEMP_LOW, "value": target_temperature_low, "format": "target_temperature", }, - { - "name": "target_temperature_high", + "target_temperature_high": { "attr": ATTR_TARGET_TEMP_HIGH, "value": self._attr_target_temperature_high, "format": "target_temperature", }, - ], + }, ), ) @@ -1017,20 +986,18 @@ def _update_target_temperature_high(self, target_temperature_high: float): self.hass.async_create_task( self._async_set_attribute( "temperature", - [ - { - "name": "target_temperature_high", + { + "target_temperature_high": { "attr": ATTR_TARGET_TEMP_HIGH, "value": target_temperature_high, "format": "target_temperature", }, - { - "name": "target_temperature_low", + "target_temperature_low": { "attr": ATTR_TARGET_TEMP_LOW, "value": self._attr_target_temperature_low, "format": "target_temperature", }, - ], + }, ), ) @@ -1039,14 +1006,13 @@ def _update_target_humidity(self, target_humidity: int): self.hass.async_create_task( self._async_set_attribute( "humidity", - [ - { - "name": "target_humidity", + { + "target_humidity": { "attr": ATTR_HUMIDITY, "value": target_humidity, "format": "target_humidity", } - ], + }, ), ) @@ -1054,11 +1020,7 @@ def _update_target_humidity(self, target_humidity: int): def _update_current_temperature(self, current_temperature: float): if ( value := self._validate_value( - { - "name": "current_temperature", - "value": current_temperature, - "format": "current_temperature", - } + "current_temperature", current_temperature, "current_temperature" ) ) is not None: self._attr_current_temperature = value @@ -1067,11 +1029,7 @@ def _update_current_temperature(self, current_temperature: float): def _update_current_humidity(self, current_humidity: float): if ( value := self._validate_value( - { - "name": "current_humidity", - "value": current_humidity, - "format": "current_humidity", - } + "current_humidity", current_humidity, "current_humidity" ) ) is not None: self._attr_current_humidity = value @@ -1080,60 +1038,59 @@ def _update_current_humidity(self, current_humidity: float): def _update_hvac_action(self, hvac_action: str): if ( value := self._validate_value( - { - "name": "hvac_action", - "value": hvac_action, - "format": [member.value for member in HVACAction], - } + "hvac_action", hvac_action, [member.value for member in HVACAction] ) ) is not None: self._attr_hvac_action = value async def _async_set_attribute(self, action, attributes) -> None: - update = False variables = {} - for attr in attributes: + for attr in attributes.keys(): # Validate input values - if (value := self._validate_value(attr)) is not None: - attr["value"] = value - variables[attr["attr"]] = value + if ( + value := self._validate_value( + attr, attributes[attr]["value"], attributes[attr]["format"] + ) + ) is not None: + attributes[attr]["value"] = value else: _LOGGER.debug( "Entity '%s' attribute '%s' update called with invalid value: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + attributes[attr]["value"], ) return False - if getattr(self, "_attr_" + attr["name"]) == attr["value"]: + if getattr(self, "_attr_" + attr) == attributes[attr]["value"]: # Nothing to do. _LOGGER.debug( "Entity '%s' attribute '%s' is already set to value: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + attributes[attr]["value"], ) else: - update = True # Update entity attribute. _LOGGER.debug( "Entity '%s' updating attribute '%s' to value: '%s'.", self._attr_name, - attr["name"], - attr["value"], + attr, + attributes[attr]["value"], ) - setattr(self, "_attr_" + attr["name"], attr["value"]) + setattr(self, "_attr_" + attr, attributes[attr]["value"]) # Update last_on modes if not off mode. if ( - attr["name"] in self._off_mode.keys() - and attr["value"] != self._off_mode[attr["name"]] + attr in self._off_mode.keys() + and attributes[attr]["value"] != self._off_mode[attr] ): - self._last_on_mode[attr["name"]] = attr["value"] + self._last_on_mode[attr] = attributes[attr]["value"] + # Set script varaible + variables[attributes[attr]["attr"]] = attributes[attr]["value"] self.async_write_ha_state() - if update and (script := getattr(self, "_script_" + action)): + if len(variables) and (script := getattr(self, "_script_" + action)): # Create a context referring to the trigger context. trigger_context_id = None if self._context is None else self._context.id script_context = Context(parent_id=trigger_context_id) @@ -1193,123 +1150,106 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new hvac mode.""" await self._async_set_attribute( "hvac_mode", - [ - { - "name": "hvac_mode", + { + "hvac_mode": { "attr": ATTR_HVAC_MODE, "value": hvac_mode, "format": self._attr_hvac_modes, } - ], + }, ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" await self._async_set_attribute( "preset_mode", - [ - { - "name": "preset_mode", + { + "preset_mode": { "attr": ATTR_PRESET_MODE, "value": preset_mode, "format": self._attr_preset_modes, } - ], + }, ) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" await self._async_set_attribute( "fan_mode", - [ - { - "name": "fan_mode", + { + "fan_mode": { "attr": ATTR_FAN_MODE, "value": fan_mode, "format": self._attr_fan_modes, } - ], + }, ) async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" await self._async_set_attribute( "swing_mode", - [ - { - "name": "swing_mode", + { + "swing_mode": { "attr": ATTR_SWING_MODE, "value": swing_mode, "format": self._attr_swing_modes, } - ], + }, ) async def async_set_humidity(self, target_humidity: int) -> None: """Set new humidity target.""" await self._async_set_attribute( "humidity", - [ - { - "name": "target_humidity", + { + "target_humidity": { "attr": ATTR_HUMIDITY, "value": target_humidity, "format": "target_humidity", } - ], + }, ) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" - attributes = [] + attributes = {} hvac_mode = self._attr_hvac_mode if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: - attributes.append( - { - "name": "hvac_mode", - "attr": ATTR_HVAC_MODE, - "value": hvac_mode, - "format": self._attr_hvac_modes, - } - ) + attributes["hvac_mode"] = { + "attr": ATTR_HVAC_MODE, + "value": hvac_mode, + "format": self._attr_hvac_modes, + } else: hvac_mode = self._attr_hvac_mode if hvac_mode == HVACMode.HEAT_COOL: if (target_temperature_low := kwargs.get(ATTR_TARGET_TEMP_LOW)) is not None: - attributes.append( - { - "name": "target_temperature_low", - "attr": ATTR_TARGET_TEMP_LOW, - "value": target_temperature_low, - "format": "target_temperature", - } - ) + attributes["target_temperature_low"] = { + "attr": ATTR_TARGET_TEMP_LOW, + "value": target_temperature_low, + "format": "target_temperature", + } else: return False if ( target_temperature_high := kwargs.get(ATTR_TARGET_TEMP_HIGH) ) is not None: - attributes.append( - { - "name": "target_temperature_high", - "attr": ATTR_TARGET_TEMP_HIGH, - "value": target_temperature_high, - "format": "target_temperature", - } - ) + attributes["target_temperature_high"] = { + "attr": ATTR_TARGET_TEMP_HIGH, + "value": target_temperature_high, + "format": "target_temperature", + } else: return False else: if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: - attributes.append( - { - "name": "target_temperature", - "attr": ATTR_TEMPERATURE, - "value": target_temperature, - "format": "target_temperature", - } - ) + attributes["target_temperature"] = { + "attr": ATTR_TEMPERATURE, + "value": target_temperature, + "format": "target_temperature", + } else: return False From d2195a90dfd12690fe8ee099492e84cb4660bca7 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sat, 20 Apr 2024 16:20:45 +0200 Subject: [PATCH 8/8] updated documentation set minimal default hvac_modes for better backward compatibility --- README.md | 25 ++++++++++++------- custom_components/climate_template/climate.py | 4 ++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c995af5..6a99de4 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,26 @@ The `climate_template` platform creates climate devices that combine integrations and provides the ability to run scripts or invoke services for each of the `set_*` commands of a climate entity. -This is a fork of the original repository jcwillox/hass-template-climate which seems to be unmaintained at the time with several pull requests pending. As those were very useful to my usage I decided to fork and merge the work of the corresponding authors to allow at least for me simple usage of the integration through HACS. Therefore all the corresponding rights belong to the original authors. +## Disclaimer ## -## Configuration +This is a fork of the original repository jcwillox/hass-template-climate which seems to be unmaintained at the time with several pull requests pending. As those were very useful to my usage I decided to fork and merge the work of the corresponding authors to allow for simple usage of the integration through HACS. Therefore all the corresponding rights belong to the original authors. I also lately started to fix some additional users issues and adding some functionality, trying to keep compatibility but please note, that there are some **breaking changes** from the original version. + +## Breaking changes from jcwillox versions -All configuration variables are optional. The climate device will work in optimistic mode (assumed state) if a template isn't defined. +- Config parameter `modes` renamed to `hvac_modes` +- `hvac_modes` list is set only to `["off", "heat"]` by default. +- `preset_modes`, `fan_modes` and `swing_modes` are now not set by default and shall be configured **only** if being used and set to the used miminum list of modes. -If you do not define a `template` or its corresponding `action` the climate device will not have that attribute, e.g. either `swing_mode_template` or `set_swing_mode` must be defined for the climate to have a swing mode. +## Configuration + +All configuration variables are optional. If you do not define a `template` or its corresponding `action` the climate device will not register to HA given attribute/function, e.g. either `swing_mode_template` or `set_swing_mode` shall be defined (together with allowed `swing_modes`) for the climate entity to have a working swing mode functionality. | Name | Type | Description | Default Value | | -------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | name | `string` | The name of the climate device. | "Template Climate" | | unique_id | `string` | The [unique id](https://developers.home-assistant.io/docs/entity_registry_index/#unique-id) of the climate entity. | None | -| mode_action | `string` | possible value: "parallel", "queued", "restart", "single" | single | -| max_action | `positive_int` | positive number from 1 | 1 | +| mode_action | `string` | possible value: `parallel`, `queued`, `restart`, `single`. For explanation see [`script`](https://www.home-assistant.io/integrations/script/#script-modes) documentation. | single | +| max_action | `positive_int` | Limits number of concurent runs of actions. Used together with `parallel` and `queued` mode_action, set to positive number greater than 1. For explanation see [`script`](https://www.home-assistant.io/integrations/script/#max) documentation. | 1 | | icon_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the icon of the sensor. | | | entity_picture_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template for the entity picture of the sensor. | | | availability_template | [`template`](https://www.home-assistant.io/docs/configuration/templating) | Defines a template to get the `available` state of the component. If the template returns `true`, the device is `available`. If the template returns any other value, the device will be `unavailable`. If `availability_template` is not configured, the component will always be `available`. | true | @@ -44,12 +50,13 @@ If you do not define a `template` or its corresponding `action` the climate devi | set_preset_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set preset mode command. Can use `preset_mode` variable. | | | set_swing_mode | [`action`](https://www.home-assistant.io/docs/scripts) | Defines an action to run when the climate device is given the set swing mode command. Can use `swing_mode` variable. | | | | | | | -| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. **At least One MUST be set.** | | +| hvac_modes | `list` | A list of supported hvac modes. Needs to be a subset of the default climate device [`hvac_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#hvac-modes) values. | ["off", "heat"] | | preset_modes | `list` | A list of supported preset modes. Custom presets modes are allowed. Default list of HA [`preset_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#presets). | | | fan_modes | `list` | A list of supported fan modes. Custom fan modes are allowed. Default list of HA [`fan_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#fan-modes). | | | swing_modes | `list` | A list of supported swing modes. Custom swing modes are allowed. Default list of HA [`swing_modes`](https://developers.home-assistant.io/docs/core/entity/climate/#fan-modes). | | -| min_temperature | `float` | Minimum temperature set point available. | 7 | -| max_temperature | `float` | Maximum temperature set point available. | 35 | +| | | | | +| min_temp | `float` | Minimum temperature set point available. | 7 | +| max_temp | `float` | Maximum temperature set point available. | 35 | | min_humidity | `float` | Minimum humidity set point available. | 30 | | max_humidity | `float` | Maximum humidity set point available. | 99 | | precision | `float` | The desired precision for this device. | 0.1 for Celsius and 1.0 for Fahrenheit. | diff --git a/custom_components/climate_template/climate.py b/custom_components/climate_template/climate.py index 0d3d5f4..2546926 100644 --- a/custom_components/climate_template/climate.py +++ b/custom_components/climate_template/climate.py @@ -136,7 +136,9 @@ vol.Optional(CONF_SET_FAN_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SWING_MODE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_HVAC_MODE_LIST, default=[]): cv.ensure_list, + vol.Optional( + CONF_HVAC_MODE_LIST, default=[HVACMode.OFF, HVACMode.HEAT] + ): cv.ensure_list, vol.Optional(CONF_PRESET_MODE_LIST, default=[]): cv.ensure_list, vol.Optional(CONF_FAN_MODE_LIST, default=[]): cv.ensure_list, vol.Optional(CONF_SWING_MODE_LIST, default=[]): cv.ensure_list,