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,
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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": {
|
"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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user