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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
71
custom_components/versatile_thermostat/services.yaml
Normal file
71
custom_components/versatile_thermostat/services.yaml
Normal 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user