diff --git a/custom_components/versatile_thermostat/climate.py b/custom_components/versatile_thermostat/climate.py index 3e206a2..3a3744e 100644 --- a/custom_components/versatile_thermostat/climate.py +++ b/custom_components/versatile_thermostat/climate.py @@ -94,6 +94,8 @@ from .const import ( PRESET_POWER, PROPORTIONAL_FUNCTION_TPI, SERVICE_SET_PRESENCE, + SERVICE_SET_PRESET_TEMPERATURE, + PRESET_AWAY_SUFFIX, ) from .prop_algorithm import PropAlgorithm @@ -191,6 +193,16 @@ async def async_setup_entry( "service_set_presence", ) + platform.async_register_entity_service( + SERVICE_SET_PRESET_TEMPERATURE, + { + vol.Required("preset"): vol.In(CONF_PRESETS), + vol.Optional("temperature"): vol.Coerce(float), + vol.Optional("temperature_away"): vol.Coerce(float), + }, + "service_set_preset_temperature", + ) + class VersatileThermostat(ClimateEntity, RestoreEntity): """Representation of a Versatile Thermostat device.""" @@ -199,6 +211,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): _heater_entity_id: str _prop_algorithm: PropAlgorithm _async_cancel_cycle: CALLBACK_TYPE + _attr_preset_modes: list[str] | None def __init__( self, @@ -249,6 +262,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self._motion_delay_sec = motion_delay_sec self._motion_preset = motion_preset self._no_motion_preset = no_motion_preset + self._motion_on = ( + self._motion_sensor_entity_id is not None + and self._motion_preset is not None + and self._no_motion_preset is not None + ) + self._tpi_coef_int = tpi_coef_int self._tpi_coef_ext = tpi_coef_ext self._presence_sensor_entity_id = presence_sensor_entity_id @@ -266,15 +285,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self._saved_hvac_mode = self._hvac_mode self._support_flags = SUPPORT_FLAGS - if len(presets): - self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE - self._attr_preset_modes = ( - [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY] - ) - _LOGGER.debug("Set preset_modes to %s", self._attr_preset_modes) - else: - _LOGGER.debug("No preset_modes") - self._attr_preset_modes = [PRESET_NONE] + self._presets = presets self._presets_away = presets_away @@ -344,6 +355,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self._overpowering_state = None self._presence_state = None + # Calculate all possible presets + self._attr_preset_modes = [PRESET_NONE] + if len(presets): + self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE + + for k, v in presets.items(): + if v != 0.0: + self._attr_preset_modes.append(k) + + # self._attr_preset_modes = ( + # [PRESET_NONE] + list(presets.keys()) + [PRESET_ACTIVITY] + # ) + _LOGGER.debug( + "After adding presets, preset_modes to %s", self._attr_preset_modes + ) + else: + _LOGGER.debug("No preset_modes") + + if self._motion_on: + self._attr_preset_modes.append(PRESET_ACTIVITY) + _LOGGER.debug( "%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s", self, @@ -442,15 +474,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): await self._async_set_preset_mode_internal(preset_mode) await self._async_control_heating() - async def _async_set_preset_mode_internal(self, preset_mode): + async def _async_set_preset_mode_internal(self, preset_mode, force=False): """Set new preset mode.""" - _LOGGER.info("%s - Set preset_mode: %s", self, preset_mode) - if preset_mode not in (self._attr_preset_modes or []): + _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force) + if ( + preset_mode not in (self._attr_preset_modes or []) + and preset_mode != PRESET_POWER + ): raise ValueError( f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long ) - if preset_mode == self._attr_preset_mode: + if preset_mode == self._attr_preset_mode and not force: # I don't think we need to call async_write_ha_state if we didn't change the state return if preset_mode == PRESET_NONE: @@ -464,7 +499,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): if self._attr_preset_mode == PRESET_NONE: self._saved_target_temp = self._target_temp self._attr_preset_mode = preset_mode - self._target_temp = self._presets[preset_mode] + preset_temp = self.find_preset_temp(preset_mode) + self._target_temp = ( + preset_temp if preset_mode != PRESET_POWER else self._power_temp + ) # Don't saved preset_mode if we are in POWER mode or in Away mode and presence detection is on if preset_mode != PRESET_POWER: @@ -472,6 +510,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): self.recalculate() + def find_preset_temp(self, preset_mode): + """Find the right temperature of a preset considering the presence if configured""" + if self._presence_on is False or self._presence_state in [STATE_ON, STATE_HOME]: + return self._presets[preset_mode] + else: + return self._presets_away[self.get_preset_away_name(preset_mode)] + + def get_preset_away_name(self, preset_mode): + """Get the preset name in away mode (when presence is off)""" + return preset_mode + PRESET_AWAY_SUFFIX + async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" _LOGGER.info("%s - Set fan mode: %s", self, fan_mode) @@ -1061,6 +1110,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state) self._presence_state = new_state if self._attr_preset_mode == PRESET_POWER or self._presence_on is False: + _LOGGER.info( + "%s - Ignoring presence change cause in Power preset or presence not configured", + self, + ) return if new_state is None or new_state not in ( STATE_OFF, @@ -1082,7 +1135,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): new_temp, ) else: - new_temp = self._presets_away[self._attr_preset_mode + "_away"] + new_temp = self._presets_away[ + self.get_preset_away_name(self._attr_preset_mode) + ] _LOGGER.info( "%s - No one is at home. Apply temperature %.2f", self, @@ -1277,9 +1332,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): "eco_temp": self._presets[PRESET_ECO], "boost_temp": self._presets[PRESET_BOOST], "comfort_temp": self._presets[PRESET_COMFORT], - "eco_away_temp": self._presets_away[PRESET_ECO + "_away"], - "boost_away_temp": self._presets_away[PRESET_BOOST + "_away"], - "comfort_away_temp": self._presets_away[PRESET_COMFORT + "_away"], + "eco_away_temp": self._presets_away[self.get_preset_away_name(PRESET_ECO)], + "boost_away_temp": self._presets_away[ + self.get_preset_away_name(PRESET_BOOST) + ], + "comfort_away_temp": self._presets_away[ + self.get_preset_away_name(PRESET_COMFORT) + ], "power_temp": self._power_temp, "on_percent": self._prop_algorithm.on_percent, "on_time_sec": self._prop_algorithm.on_time_sec, @@ -1316,11 +1375,45 @@ class VersatileThermostat(ClimateEntity, RestoreEntity): async def service_set_presence(self, presence): """Called by a service call: service: versatile_thermostat.set_presence - entity_id: climate.xxxxxx - data: { - presence: 'on' - } + data: + presence: "off" + target: + entity_id: climate.thermostat_1 """ _LOGGER.info("%s - Calling service_set_presence, presence: %s", self, presence) - # Do something self._update_presence(presence) + + async def service_set_preset_temperature( + self, preset, temperature=None, temperature_away=None + ): + """Called by a service call: + service: versatile_thermostat.set_preset_temperature + data: + temperature: 17.8 + preset: boost + temperature_away: 15 + target: + entity_id: climate.thermostat_2 + """ + _LOGGER.info( + "%s - Calling service_set_preset_temperature, preset: %s, temperature: %s, temperature_away: %s", + self, + preset, + temperature, + temperature_away, + ) + if preset in self._presets: + if temperature is not None: + self._presets[preset] = temperature + if self._presence_on and temperature_away is not None: + self._presets_away[self.get_preset_away_name(preset)] = temperature_away + else: + _LOGGER.warning( + "%s - No preset %s configured for this thermostat. Ignoring set_preset_temperature call", + self, + preset, + ) + + # If the changed preset is active, change the current temperature + if self._attr_preset_mode == preset: + await self._async_set_preset_mode_internal(preset, force=True) diff --git a/custom_components/versatile_thermostat/config_flow.py b/custom_components/versatile_thermostat/config_flow.py index 16d9497..f51713f 100644 --- a/custom_components/versatile_thermostat/config_flow.py +++ b/custom_components/versatile_thermostat/config_flow.py @@ -2,9 +2,12 @@ from __future__ import annotations import logging - +import copy import voluptuous as vol +from collections.abc import Mapping +from typing import Any + from homeassistant.core import callback from homeassistant.config_entries import ( ConfigEntry, @@ -48,91 +51,46 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_HEATER): cv.string, - vol.Required(CONF_TEMP_SENSOR): cv.string, - vol.Required(CONF_EXTERNAL_TEMP_SENSOR): cv.string, - vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int, - vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI): vol.In( - [ - PROPORTIONAL_FUNCTION_TPI, - ] - ), - } -) -STEP_TPI_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float), - vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float), - } -) - -STEP_PRESETS_DATA_SCHEMA = vol.Schema( - {vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()} -) - -STEP_WINDOW_DATA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_WINDOW_SENSOR): cv.string, - vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, - } -) - -STEP_MOTION_DATA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MOTION_SENSOR): cv.string, - vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int, - vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In( - CONF_PRESETS_SELECTIONABLE - ), - vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In( - CONF_PRESETS_SELECTIONABLE - ), - } -) - -STEP_POWER_DATA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_POWER_SENSOR): cv.string, - vol.Optional(CONF_MAX_POWER_SENSOR): cv.string, - vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float), - vol.Optional(CONF_PRESET_POWER): vol.Coerce(float), - } -) - -STEP_PRESENCE_DATA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_PRESENCE_SENSOR): cv.string, - } -).extend( - { - vol.Optional(v, default=17): vol.Coerce(float) - for (k, v) in CONF_PRESETS_AWAY.items() - } -) +# Not used but can be useful in other context +# def schema_defaults(schema, **defaults): +# """Create a new schema with default values filled in.""" +# copy = schema.extend({}) +# for field, field_type in copy.schema.items(): +# if isinstance(field_type, vol.In): +# value = None +# +# if value in field_type.container: +# # field.default = vol.default_factory(value) +# field.description = {"suggested_value": value} +# continue +# +# if field.schema in defaults: +# # field.default = vol.default_factory(defaults[field]) +# field.description = {"suggested_value": defaults[field]} +# return copy +# -def schema_defaults(schema, **defaults): - """Create a new schema with default values filled in.""" - copy = schema.extend({}) - for field, field_type in copy.schema.items(): - if isinstance(field_type, vol.In): - value = None - # for dps in dps_list or []: - # if dps.startswith(f"{defaults.get(field)} "): - # value = dps - # break +def add_suggested_values_to_schema( + data_schema: vol.Schema, suggested_values: Mapping[str, Any] +) -> vol.Schema: + """Make a copy of the schema, populated with suggested values. - if value in field_type.container: - field.default = vol.default_factory(value) - continue - - if field.schema in defaults: - field.default = vol.default_factory(defaults[field]) - return copy + For each schema marker matching items in `suggested_values`, + the `suggested_value` will be set. The existing `suggested_value` will + be left untouched if there is no matching item. + """ + schema = {} + for key, val in data_schema.schema.items(): + new_key = key + if key in suggested_values and isinstance(key, vol.Marker): + # Copy the marker to not modify the flow schema + new_key = copy.copy(key) + new_key.description = {"suggested_value": suggested_values[key]} + schema[new_key] = val + _LOGGER.debug("add_suggested_values_to_schema: schema=%s", schema) + return vol.Schema(schema) class VersatileThermostatBaseConfigFlow(FlowHandler): @@ -145,6 +103,76 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): super().__init__() _LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos) self._infos = infos + self.STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HEATER): cv.string, + vol.Required(CONF_TEMP_SENSOR): cv.string, + vol.Required(CONF_EXTERNAL_TEMP_SENSOR): cv.string, + vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int, + vol.Required( + CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_TPI + ): vol.In( + [ + PROPORTIONAL_FUNCTION_TPI, + ] + ), + } + ) + + self.STEP_TPI_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float), + vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float), + } + ) + + self.STEP_PRESETS_DATA_SCHEMA = vol.Schema( + { + vol.Optional(v, default=0.0): vol.Coerce(float) + for (k, v) in CONF_PRESETS.items() + } + ) + + self.STEP_WINDOW_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_WINDOW_SENSOR): cv.string, + vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, + } + ) + + self.STEP_MOTION_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_MOTION_SENSOR): cv.string, + vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_MOTION_PRESET, default="comfort"): vol.In( + CONF_PRESETS_SELECTIONABLE + ), + vol.Optional(CONF_NO_MOTION_PRESET, default="eco"): vol.In( + CONF_PRESETS_SELECTIONABLE + ), + } + ) + + self.STEP_POWER_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_POWER_SENSOR): cv.string, + vol.Optional(CONF_MAX_POWER_SENSOR): cv.string, + vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float), + vol.Optional(CONF_PRESET_POWER): vol.Coerce(float), + } + ) + + self.STEP_PRESENCE_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PRESENCE_SENSOR): cv.string, + } + ).extend( + { + vol.Optional(v, default=17): vol.Coerce(float) + for (k, v) in CONF_PRESETS_AWAY.items() + } + ) async def validate_input(self, data: dict) -> dict[str]: """Validate the user input allows us to connect. @@ -171,6 +199,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): ) raise UnknownEntity(conf) + def merge_user_input(self, data_schema: vol.Schema, user_input: dict): + """For each schema entry not in user_input, set or remove values in infos""" + self._infos.update(user_input) + for key, _ in data_schema.schema.items(): + if key not in user_input and isinstance(key, vol.Marker): + _LOGGER.debug( + "add_empty_values_to_user_input: %s is not in user_input", key + ) + if key in self._infos: + self._infos.pop(key) + # else: This don't work but I don't know why. _infos seems broken after this (Not serializable exactly) + # self._infos[key] = user_input[key] + + _LOGGER.debug("merge_user_input: infos is now %s", self._infos) + async def generic_step(self, step_id, data_schema, user_input, next_step_function): """A generic method step""" _LOGGER.debug( @@ -190,11 +233,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - self._infos.update(user_input) + self.merge_user_input(data_schema, user_input) _LOGGER.debug("_info is now: %s", self._infos) return await next_step_function() - ds = schema_defaults(data_schema, **defaults) # pylint: disable=invalid-name + # ds = schema_defaults(data_schema, **defaults) # pylint: disable=invalid-name + ds = add_suggested_values_to_schema( + data_schema=data_schema, suggested_values=defaults + ) # pylint: disable=invalid-name return self.async_show_form(step_id=step_id, data_schema=ds, errors=errors) @@ -203,7 +249,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input) return await self.generic_step( - "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi + "user", self.STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi ) async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult: @@ -211,7 +257,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input) return await self.generic_step( - "tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets + "tpi", self.STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets ) async def async_step_presets(self, user_input: dict | None = None) -> FlowResult: @@ -219,7 +265,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input) return await self.generic_step( - "presets", STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window + "presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window ) async def async_step_window(self, user_input: dict | None = None) -> FlowResult: @@ -227,7 +273,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input) return await self.generic_step( - "window", STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion + "window", self.STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion ) async def async_step_motion(self, user_input: dict | None = None) -> FlowResult: @@ -235,7 +281,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): _LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input) return await self.generic_step( - "motion", STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power + "motion", self.STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power ) async def async_step_power(self, user_input: dict | None = None) -> FlowResult: @@ -244,7 +290,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): return await self.generic_step( "power", - STEP_POWER_DATA_SCHEMA, + self.STEP_POWER_DATA_SCHEMA, user_input, self.async_step_presence, ) @@ -255,7 +301,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler): return await self.generic_step( "presence", - STEP_PRESENCE_DATA_SCHEMA, + self.STEP_PRESENCE_DATA_SCHEMA, user_input, self.async_finalize, # pylint: disable=no-member ) @@ -279,7 +325,7 @@ class VersatileThermostatConfigFlow( async def async_finalize(self): """Finalization of the ConfigEntry creation""" - _LOGGER.debug("CTOR ConfigFlow.async_finalize") + _LOGGER.debug("ConfigFlow.async_finalize") return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos) @@ -318,7 +364,7 @@ class VersatileThermostatOptionsFlowHandler( ) return await self.generic_step( - "user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi + "user", self.STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi ) async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult: @@ -328,7 +374,7 @@ class VersatileThermostatOptionsFlowHandler( ) return await self.generic_step( - "tpi", STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets + "tpi", self.STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets ) async def async_step_presets(self, user_input: dict | None = None) -> FlowResult: @@ -338,7 +384,7 @@ class VersatileThermostatOptionsFlowHandler( ) return await self.generic_step( - "presets", STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window + "presets", self.STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window ) async def async_step_window(self, user_input: dict | None = None) -> FlowResult: @@ -348,7 +394,7 @@ class VersatileThermostatOptionsFlowHandler( ) return await self.generic_step( - "window", STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion + "window", self.STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion ) async def async_step_motion(self, user_input: dict | None = None) -> FlowResult: @@ -358,7 +404,7 @@ class VersatileThermostatOptionsFlowHandler( ) return await self.generic_step( - "motion", STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power + "motion", self.STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power ) async def async_step_power(self, user_input: dict | None = None) -> FlowResult: @@ -369,7 +415,7 @@ class VersatileThermostatOptionsFlowHandler( return await self.generic_step( "power", - STEP_POWER_DATA_SCHEMA, + self.STEP_POWER_DATA_SCHEMA, user_input, self.async_step_presence, # pylint: disable=no-member ) @@ -382,7 +428,7 @@ class VersatileThermostatOptionsFlowHandler( return await self.generic_step( "presence", - STEP_PRESENCE_DATA_SCHEMA, + self.STEP_PRESENCE_DATA_SCHEMA, user_input, self.async_finalize, # pylint: disable=no-member ) diff --git a/custom_components/versatile_thermostat/const.py b/custom_components/versatile_thermostat/const.py index dfdc2ea..c68a4bd 100644 --- a/custom_components/versatile_thermostat/const.py +++ b/custom_components/versatile_thermostat/const.py @@ -45,12 +45,14 @@ CONF_PRESETS = { ) } +PRESET_AWAY_SUFFIX = "_away" + CONF_PRESETS_AWAY = { p: f"{p}_temp" for p in ( - PRESET_ECO + "_away", - PRESET_BOOST + "_away", - PRESET_COMFORT + "_away", + PRESET_ECO + PRESET_AWAY_SUFFIX, + PRESET_BOOST + PRESET_AWAY_SUFFIX, + PRESET_COMFORT + PRESET_AWAY_SUFFIX, ) } @@ -91,3 +93,4 @@ CONF_FUNCTIONS = [ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE SERVICE_SET_PRESENCE = "set_presence" +SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature" diff --git a/custom_components/versatile_thermostat/services.yaml b/custom_components/versatile_thermostat/services.yaml new file mode 100644 index 0000000..7afc2f6 --- /dev/null +++ b/custom_components/versatile_thermostat/services.yaml @@ -0,0 +1,71 @@ +set_presence: + name: Set presence + description: Force the presence mode in thermostat + target: + entity: + multiple: true + integration: versatile_thermostat + fields: + presence: + name: Presence + description: Presence setting + required: true + advanced: false + example: "on" + default: "on" + selector: + select: + options: + - "on" + - "off" + - "home" + - "not_home" + +set_preset_temperature: + name: Set temperature preset + description: Change the target temperature of a preset + target: + entity: + multiple: true + integration: versatile_thermostat + fields: + preset: + name: Preset + description: Preset name + required: true + advanced: false + example: "comfort" + selector: + select: + options: + - "eco" + - "comfort" + - "boost" + temperature: + name: Temperature when present + description: Target temperature for the preset when present + required: false + advanced: false + example: "19.5" + default: "17" + selector: + number: + min: 7 + max: 35 + step: 0.1 + unit_of_measurement: ° + mode: slider + temperature_away: + name: Temperature when not present + description: Target temperature for the preset when not present + required: false + advanced: false + example: "17" + default: "15" + selector: + number: + min: 7 + max: 35 + step: 0.1 + unit_of_measurement: ° + mode: slider \ No newline at end of file diff --git a/custom_components/versatile_thermostat/strings.json b/custom_components/versatile_thermostat/strings.json index f7df394..ebde636 100644 --- a/custom_components/versatile_thermostat/strings.json +++ b/custom_components/versatile_thermostat/strings.json @@ -25,7 +25,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature", + "description": "For each presets, give the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", @@ -104,7 +104,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature", + "description": "For each presets, give the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", diff --git a/custom_components/versatile_thermostat/translations/en.json b/custom_components/versatile_thermostat/translations/en.json index f7df394..ebde636 100644 --- a/custom_components/versatile_thermostat/translations/en.json +++ b/custom_components/versatile_thermostat/translations/en.json @@ -25,7 +25,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature", + "description": "For each presets, give the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", @@ -104,7 +104,7 @@ }, "presets": { "title": "Presets", - "description": "For each presets, give the target temperature", + "description": "For each presets, give the target temperature (0 to ignore preset)", "data": { "eco_temp": "Temperature in Eco preset", "comfort_temp": "Temperature in Comfort preset", diff --git a/custom_components/versatile_thermostat/translations/fr.json b/custom_components/versatile_thermostat/translations/fr.json index c85cdf9..daa0741 100644 --- a/custom_components/versatile_thermostat/translations/fr.json +++ b/custom_components/versatile_thermostat/translations/fr.json @@ -25,7 +25,7 @@ }, "presets": { "title": "Presets", - "description": "Pour chaque preset, donnez la température cible", + "description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)", "data": { "eco_temp": "Température en preset Eco", "comfort_temp": "Température en preset Comfort", @@ -104,7 +104,7 @@ }, "presets": { "title": "Presets", - "description": "Pour chaque preset, donnez la température cible", + "description": "Pour chaque preset, donnez la température cible (0 pour ignorer le preset)", "data": { "eco_temp": "Température en preset Eco", "comfort_temp": "Température en preset Comfort",