Issue #12 - Cannot remove entity_id in configuration

Issue #09 - Hide preset that are not configured
Issue #04 - Add service to update the temperature of a preset
Issue #02 - Limits the usable presets
This commit is contained in:
Jean-Marc Collin
2023-01-14 12:28:26 +01:00
parent c605c5e3ae
commit 10189a59a7
7 changed files with 344 additions and 131 deletions

View File

@@ -94,6 +94,8 @@ from .const import (
PRESET_POWER, PRESET_POWER,
PROPORTIONAL_FUNCTION_TPI, PROPORTIONAL_FUNCTION_TPI,
SERVICE_SET_PRESENCE, SERVICE_SET_PRESENCE,
SERVICE_SET_PRESET_TEMPERATURE,
PRESET_AWAY_SUFFIX,
) )
from .prop_algorithm import PropAlgorithm from .prop_algorithm import PropAlgorithm
@@ -191,6 +193,16 @@ async def async_setup_entry(
"service_set_presence", "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): class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Representation of a Versatile Thermostat device.""" """Representation of a Versatile Thermostat device."""
@@ -199,6 +211,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
_heater_entity_id: str _heater_entity_id: str
_prop_algorithm: PropAlgorithm _prop_algorithm: PropAlgorithm
_async_cancel_cycle: CALLBACK_TYPE _async_cancel_cycle: CALLBACK_TYPE
_attr_preset_modes: list[str] | None
def __init__( def __init__(
self, self,
@@ -249,6 +262,12 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._motion_delay_sec = motion_delay_sec self._motion_delay_sec = motion_delay_sec
self._motion_preset = motion_preset self._motion_preset = motion_preset
self._no_motion_preset = no_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_int = tpi_coef_int
self._tpi_coef_ext = tpi_coef_ext self._tpi_coef_ext = tpi_coef_ext
self._presence_sensor_entity_id = presence_sensor_entity_id 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._saved_hvac_mode = self._hvac_mode
self._support_flags = SUPPORT_FLAGS 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 = presets
self._presets_away = presets_away self._presets_away = presets_away
@@ -344,6 +355,27 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self._overpowering_state = None self._overpowering_state = None
self._presence_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( _LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s", "%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
self, self,
@@ -442,15 +474,18 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
await self._async_set_preset_mode_internal(preset_mode) await self._async_set_preset_mode_internal(preset_mode)
await self._async_control_heating() 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.""" """Set new preset mode."""
_LOGGER.info("%s - Set preset_mode: %s", self, preset_mode) _LOGGER.info("%s - Set preset_mode: %s force=%s", self, preset_mode, force)
if preset_mode not in (self._attr_preset_modes or []): if (
preset_mode not in (self._attr_preset_modes or [])
and preset_mode != PRESET_POWER
):
raise ValueError( raise ValueError(
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}" # pylint: disable=line-too-long 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 # I don't think we need to call async_write_ha_state if we didn't change the state
return return
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
@@ -464,7 +499,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
if self._attr_preset_mode == PRESET_NONE: if self._attr_preset_mode == PRESET_NONE:
self._saved_target_temp = self._target_temp self._saved_target_temp = self._target_temp
self._attr_preset_mode = preset_mode 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 # 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: if preset_mode != PRESET_POWER:
@@ -472,6 +510,17 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
self.recalculate() 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): async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode.""" """Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, 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) _LOGGER.debug("%s - Updating presence. New state is %s", self, new_state)
self._presence_state = new_state self._presence_state = new_state
if self._attr_preset_mode == PRESET_POWER or self._presence_on is False: 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 return
if new_state is None or new_state not in ( if new_state is None or new_state not in (
STATE_OFF, STATE_OFF,
@@ -1082,7 +1135,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
new_temp, new_temp,
) )
else: 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( _LOGGER.info(
"%s - No one is at home. Apply temperature %.2f", "%s - No one is at home. Apply temperature %.2f",
self, self,
@@ -1277,9 +1332,13 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
"eco_temp": self._presets[PRESET_ECO], "eco_temp": self._presets[PRESET_ECO],
"boost_temp": self._presets[PRESET_BOOST], "boost_temp": self._presets[PRESET_BOOST],
"comfort_temp": self._presets[PRESET_COMFORT], "comfort_temp": self._presets[PRESET_COMFORT],
"eco_away_temp": self._presets_away[PRESET_ECO + "_away"], "eco_away_temp": self._presets_away[self.get_preset_away_name(PRESET_ECO)],
"boost_away_temp": self._presets_away[PRESET_BOOST + "_away"], "boost_away_temp": self._presets_away[
"comfort_away_temp": self._presets_away[PRESET_COMFORT + "_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, "power_temp": self._power_temp,
"on_percent": self._prop_algorithm.on_percent, "on_percent": self._prop_algorithm.on_percent,
"on_time_sec": self._prop_algorithm.on_time_sec, "on_time_sec": self._prop_algorithm.on_time_sec,
@@ -1316,11 +1375,45 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
async def service_set_presence(self, presence): async def service_set_presence(self, presence):
"""Called by a service call: """Called by a service call:
service: versatile_thermostat.set_presence service: versatile_thermostat.set_presence
entity_id: climate.xxxxxx data:
data: { presence: "off"
presence: 'on' target:
} entity_id: climate.thermostat_1
""" """
_LOGGER.info("%s - Calling service_set_presence, presence: %s", self, presence) _LOGGER.info("%s - Calling service_set_presence, presence: %s", self, presence)
# Do something
self._update_presence(presence) 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)

View File

@@ -2,9 +2,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import copy
import voluptuous as vol import voluptuous as vol
from collections.abc import Mapping
from typing import Any
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
@@ -48,91 +51,46 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _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( # Not used but can be useful in other context
{ # def schema_defaults(schema, **defaults):
vol.Required(CONF_TPI_COEF_INT, default=0.6): vol.Coerce(float), # """Create a new schema with default values filled in."""
vol.Required(CONF_TPI_COEF_EXT, default=0.01): vol.Coerce(float), # copy = schema.extend({})
} # for field, field_type in copy.schema.items():
) # if isinstance(field_type, vol.In):
# value = None
STEP_PRESETS_DATA_SCHEMA = vol.Schema( #
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()} # if value in field_type.container:
) # # field.default = vol.default_factory(value)
# field.description = {"suggested_value": value}
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # continue
{ #
vol.Optional(CONF_WINDOW_SENSOR): cv.string, # if field.schema in defaults:
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, # # field.default = vol.default_factory(defaults[field])
} # field.description = {"suggested_value": defaults[field]}
) # return copy
#
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()
}
)
def schema_defaults(schema, **defaults): def add_suggested_values_to_schema(
"""Create a new schema with default values filled in.""" data_schema: vol.Schema, suggested_values: Mapping[str, Any]
copy = schema.extend({}) ) -> vol.Schema:
for field, field_type in copy.schema.items(): """Make a copy of the schema, populated with suggested values.
if isinstance(field_type, vol.In):
value = None
# for dps in dps_list or []:
# if dps.startswith(f"{defaults.get(field)} "):
# value = dps
# break
if value in field_type.container: For each schema marker matching items in `suggested_values`,
field.default = vol.default_factory(value) the `suggested_value` will be set. The existing `suggested_value` will
continue be left untouched if there is no matching item.
"""
if field.schema in defaults: schema = {}
field.default = vol.default_factory(defaults[field]) for key, val in data_schema.schema.items():
return copy 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): class VersatileThermostatBaseConfigFlow(FlowHandler):
@@ -145,6 +103,76 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
super().__init__() super().__init__()
_LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos) _LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos)
self._infos = 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]: async def validate_input(self, data: dict) -> dict[str]:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
@@ -171,6 +199,21 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
) )
raise UnknownEntity(conf) 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): async def generic_step(self, step_id, data_schema, user_input, next_step_function):
"""A generic method step""" """A generic method step"""
_LOGGER.debug( _LOGGER.debug(
@@ -190,11 +233,14 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
self._infos.update(user_input) self.merge_user_input(data_schema, user_input)
_LOGGER.debug("_info is now: %s", self._infos) _LOGGER.debug("_info is now: %s", self._infos)
return await next_step_function() 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) 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) _LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
return await self.generic_step( 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: 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) _LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
return await self.generic_step( 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: 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) _LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input)
return await self.generic_step( 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: 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) _LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
return await self.generic_step( 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: 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) _LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
return await self.generic_step( 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: async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
@@ -244,7 +290,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step( return await self.generic_step(
"power", "power",
STEP_POWER_DATA_SCHEMA, self.STEP_POWER_DATA_SCHEMA,
user_input, user_input,
self.async_step_presence, self.async_step_presence,
) )
@@ -255,7 +301,7 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
return await self.generic_step( return await self.generic_step(
"presence", "presence",
STEP_PRESENCE_DATA_SCHEMA, self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_finalize, # pylint: disable=no-member self.async_finalize, # pylint: disable=no-member
) )
@@ -279,7 +325,7 @@ class VersatileThermostatConfigFlow(
async def async_finalize(self): async def async_finalize(self):
"""Finalization of the ConfigEntry creation""" """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) return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos)
@@ -318,7 +364,7 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( 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: async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
@@ -328,7 +374,7 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( 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: async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
@@ -338,7 +384,7 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( 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: async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
@@ -348,7 +394,7 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( 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: async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
@@ -358,7 +404,7 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( 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: async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
@@ -369,7 +415,7 @@ class VersatileThermostatOptionsFlowHandler(
return await self.generic_step( return await self.generic_step(
"power", "power",
STEP_POWER_DATA_SCHEMA, self.STEP_POWER_DATA_SCHEMA,
user_input, user_input,
self.async_step_presence, # pylint: disable=no-member self.async_step_presence, # pylint: disable=no-member
) )
@@ -382,7 +428,7 @@ class VersatileThermostatOptionsFlowHandler(
return await self.generic_step( return await self.generic_step(
"presence", "presence",
STEP_PRESENCE_DATA_SCHEMA, self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_finalize, # pylint: disable=no-member self.async_finalize, # pylint: disable=no-member
) )

View File

@@ -45,12 +45,14 @@ CONF_PRESETS = {
) )
} }
PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = { CONF_PRESETS_AWAY = {
p: f"{p}_temp" p: f"{p}_temp"
for p in ( for p in (
PRESET_ECO + "_away", PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + "_away", PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + "_away", PRESET_COMFORT + PRESET_AWAY_SUFFIX,
) )
} }
@@ -91,3 +93,4 @@ CONF_FUNCTIONS = [
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence" SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"

View File

@@ -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

View File

@@ -25,7 +25,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each presets, give the target temperature", "description": "For each presets, give the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
@@ -104,7 +104,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each presets, give the target temperature", "description": "For each presets, give the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",

View File

@@ -25,7 +25,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each presets, give the target temperature", "description": "For each presets, give the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
@@ -104,7 +104,7 @@
}, },
"presets": { "presets": {
"title": "Presets", "title": "Presets",
"description": "For each presets, give the target temperature", "description": "For each presets, give the target temperature (0 to ignore preset)",
"data": { "data": {
"eco_temp": "Temperature in Eco preset", "eco_temp": "Temperature in Eco preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",

View File

@@ -25,7 +25,7 @@
}, },
"presets": { "presets": {
"title": "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": { "data": {
"eco_temp": "Température en preset Eco", "eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort", "comfort_temp": "Température en preset Comfort",
@@ -104,7 +104,7 @@
}, },
"presets": { "presets": {
"title": "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": { "data": {
"eco_temp": "Température en preset Eco", "eco_temp": "Température en preset Eco",
"comfort_temp": "Température en preset Comfort", "comfort_temp": "Température en preset Comfort",