Compare commits

..

10 Commits

Author SHA1 Message Date
Jean-Marc Collin
10189a59a7 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
2023-01-14 12:28:26 +01:00
Jean-Marc Collin
c605c5e3ae issue #11 - Rename coeff_c and coeff_t to coeff_int et coeff_ext
issue #10 - Add a service to force the presence or non presence
issue #07 - Make external temperature sensor mandatory for TPI mode
issue #06 - Use person.xxx as presence detector directly
issue #02 - Limits the usable presets
issue #01 - Add a temperature configuration for present / not present for each preset
2023-01-11 23:11:04 +01:00
Jean-Marc Collin
018343ec53 FIX power float conversion when state unknown 2023-01-08 13:15:02 +01:00
Jean-Marc Collin
89984b3dfc Add presence management
Fix many bugs on states actualisation
Add extra attributes with all internal states
2023-01-08 12:29:50 +01:00
Jean-Marc Collin
5fb148d445 FIX restore preset after power preset 2023-01-07 19:44:43 +01:00
Jean-Marc Collin
62a5e05a7a FIX power management not intialized 2023-01-07 18:30:46 +01:00
Jean-Marc Collin
a6a5fd38a0 Add custom attributes
Fix initialization temperature bug
2023-01-07 17:00:10 +01:00
Jean-Marc Collin
44174f23eb Add TPI algorithm 2023-01-07 10:14:29 +00:00
Jean-Marc Collin
db4052d93b typo in README 2023-01-03 17:18:16 +01:00
Jean-Marc Collin
a0486a3f54 FIX exception when no power sensor are configured
Add more explantions on README
2023-01-03 17:15:29 +01:00
13 changed files with 1162 additions and 549 deletions

View File

@@ -13,8 +13,14 @@ input_number:
name: Temperature name: Temperature
min: 0 min: 0
max: 35 max: 35
step: 1 step: .1
icon: mdi:thermometer icon: mdi:thermometer
fake_external_temperature_sensor1:
name: Ext Temperature
min: -10
max: 35
step: .1
icon: mdi:home-thermometer
fake_current_power: fake_current_power:
name: Current power name: Current power
min: 0 min: 0
@@ -36,9 +42,19 @@ input_boolean:
icon: mdi:window-closed-variant icon: mdi:window-closed-variant
# input_boolean to simulate the heater entity switch. Only for development environment. # input_boolean to simulate the heater entity switch. Only for development environment.
fake_heater_switch1: fake_heater_switch1:
name: Heater 1 name: Heater 1 (Linear)
icon: mdi:radiator
fake_heater_switch2:
name: Heater (TPI with presence preset)
icon: mdi:radiator
fake_heater_switch3:
name: Heater (TPI with offset)
icon: mdi:radiator icon: mdi:radiator
# input_boolean to simulate the motion sensor entity. Only for development environment. # input_boolean to simulate the motion sensor entity. Only for development environment.
fake_motion_sensor1: fake_motion_sensor1:
name: Motion Sensor 1 name: Motion Sensor 1
icon: mdi:run icon: mdi:run
# input_boolean to simulate the presence sensor entity. Only for development environment.
fake_presence_sensor1:
name: Presence Sensor 1
icon: mdi:home

View File

@@ -6,7 +6,15 @@
_Component developed by using the amazing development template [blueprint][blueprint]._ _Component developed by using the amazing development template [blueprint][blueprint]._
This custom component for Home Assistant is an upgrade and complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features. This custom component for Home Assistant is an upgrade and is a complete rewrite of the component "Awesome thermostat" (see [Github](https://github.com/dadge/awesome_thermostat)) with addition of features.
## When to use / not use
This thermostat aims to command a heater which works only in on/off mode. The minimal needed configuration to use this thermostat is:
1. an equipement like a heater (a switch),
2. a temperature sensor for the room (or an input_number),
3. an external temperature sensor (think of the meteo integration if you don't have one)
Because this integration aims to command the heater considering the preset configured and the room temperature, those informations are mandatory.
## Why another thermostat implementation ? ## Why another thermostat implementation ?
For my personnal usage, I needed to add a couple of features and also to update the behavior that I implemented in my previous component "Awesome thermostat". For my personnal usage, I needed to add a couple of features and also to update the behavior that I implemented in my previous component "Awesome thermostat".
@@ -129,6 +137,13 @@ Depending of your area and heater, the convergente temperature can be under the
A function parameter is available. Set it to "Linear" to have a linéar growth of temperature or set it to "Atan" to have a more aggressive curve to target temperature depending of your need. A function parameter is available. Set it to "Linear" to have a linéar growth of temperature or set it to "Atan" to have a more aggressive curve to target temperature depending of your need.
### Some results
Convergence of temperature to target configured by preset:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/results-1.png?raw=true)
Cycle of on/off calculated by the integration:
![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/results-2.png?raw=true)
Enjoy ! Enjoy !
@@ -158,6 +173,35 @@ In this example I set ECO mode during the night and the day when nobody's at hom
I hope this example helps you, don't hesitate to give me your feedbacks ! I hope this example helps you, don't hesitate to give me your feedbacks !
## Even / even better with custom:simple-thermostat front integration
The custom:simple-thermostat (see https://home.clouderial.fr/hacs/repository/158654878) is a great integration which allow some customisation which fits well with this thermostat.
You can have something like that very easily ![image](https://github.com/jmcollin78/versatile_thermostat/blob/dev/images/simple-thermostat.png?raw=true)
Example configuration:
```
type: custom:simple-thermostat
entity: climate.thermostat_sam2
layout:
step: row
label:
temperature: T°
state: Etat
hide:
state: false
control:
hvac:
_name: Mode
preset:
_name: Preset
sensors:
- entity: sensor.total_puissance_radiateur_sam2
icon: mdi:lightning-bolt-outline
header:
toggle:
entity: input_boolean.etat_ouverture_porte_sam
name: Porte sam
```
## Contributions are welcome! ## Contributions are welcome!
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,20 @@
"""Config flow for Versatile Thermostat integration.""" """Config flow for Versatile Thermostat integration."""
from __future__ import annotations from __future__ import annotations
from homeassistant.core import callback
import logging import logging
from typing import Any 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.config_entries import ( from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow as HAConfigFlow, ConfigFlow as HAConfigFlow,
OptionsFlow, OptionsFlow,
) )
from homeassistant.data_entry_flow import FlowHandler from homeassistant.data_entry_flow import FlowHandler
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@@ -24,6 +25,7 @@ from .const import (
CONF_NAME, CONF_NAME,
CONF_HEATER, CONF_HEATER,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR, CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR, CONF_WINDOW_SENSOR,
@@ -34,130 +36,61 @@ from .const import (
CONF_NO_MOTION_PRESET, CONF_NO_MOTION_PRESET,
CONF_DEVICE_POWER, CONF_DEVICE_POWER,
CONF_CYCLE_MIN, CONF_CYCLE_MIN,
ALL_CONF, CONF_PRESET_POWER,
CONF_PRESETS, CONF_PRESETS,
CONF_PRESETS_AWAY,
CONF_PRESETS_SELECTIONABLE, CONF_PRESETS_SELECTIONABLE,
CONF_FUNCTIONS,
CONF_PROP_FUNCTION, CONF_PROP_FUNCTION,
CONF_PROP_BIAS, CONF_TPI_COEF_EXT,
PROPORTIONAL_FUNCTION_ATAN, CONF_TPI_COEF_INT,
PROPORTIONAL_FUNCTION_LINEAR, CONF_PRESENCE_SENSOR,
PROPORTIONAL_FUNCTION_TPI,
) )
# from .climate import VersatileThermostat # from .climate import VersatileThermostat
_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_CYCLE_MIN, default=5): cv.positive_int,
vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_LINEAR): vol.In(
[PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
),
vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
}
)
USER_DATA_CONF = [
CONF_NAME,
CONF_HEATER,
CONF_TEMP_SENSOR,
CONF_CYCLE_MIN,
CONF_PROP_FUNCTION,
CONF_PROP_BIAS,
]
STEP_PRESETS_DATA_SCHEMA = vol.Schema( # Not used but can be useful in other context
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()} # def schema_defaults(schema, **defaults):
) # """Create a new schema with default values filled in."""
PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()] # copy = schema.extend({})
# for field, field_type in copy.schema.items():
STEP_WINDOW_DATA_SCHEMA = vol.Schema( # if isinstance(field_type, vol.In):
{ # value = None
vol.Optional(CONF_WINDOW_SENSOR): cv.string, #
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int, # if value in field_type.container:
} # # field.default = vol.default_factory(value)
) # field.description = {"suggested_value": value}
WINDOW_DATA_CONF = [CONF_WINDOW_SENSOR, CONF_WINDOW_DELAY] # continue
#
STEP_MOTION_DATA_SCHEMA = vol.Schema( # if field.schema in defaults:
{ # # field.default = vol.default_factory(defaults[field])
vol.Optional(CONF_MOTION_SENSOR): cv.string, # field.description = {"suggested_value": defaults[field]}
vol.Optional(CONF_MOTION_DELAY, default=30): cv.positive_int, # return copy
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
),
}
)
MOTION_DATA_CONF = [
CONF_MOTION_SENSOR,
CONF_MOTION_DELAY,
CONF_MOTION_PRESET,
CONF_NO_MOTION_PRESET,
]
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),
}
)
POWER_DATA_CONF = [CONF_POWER_SENSOR, CONF_MAX_POWER_SENSOR, CONF_DEVICE_POWER]
# STEP_USER_DATA_SCHEMA = vol.Schema(
# {
# vol.Required(CONF_NAME): cv.string,
# vol.Required(CONF_HEATER): cv.string,
# vol.Required(CONF_CYCLE_MIN, default=5): cv.positive_int,
# vol.Required(CONF_PROP_FUNCTION, default=PROPORTIONAL_FUNCTION_LINEAR): vol.In(
# [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
# ),
# vol.Required(CONF_PROP_BIAS, default=0.25): vol.Coerce(float),
# vol.Required(CONF_TEMP_SENSOR): cv.string,
# vol.Optional(CONF_POWER_SENSOR): cv.string,
# vol.Optional(CONF_MAX_POWER_SENSOR): cv.string,
# vol.Optional(CONF_WINDOW_SENSOR): cv.string,
# vol.Required(CONF_WINDOW_DELAY, default=30): cv.positive_int,
# vol.Optional(CONF_MOTION_SENSOR): cv.string,
# vol.Required(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
# ),
# vol.Required(CONF_MOTION_DELAY, default=30): cv.positive_int,
# vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float),
# }
# ).extend(
# {vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.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):
@@ -170,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.
@@ -181,19 +184,36 @@ class VersatileThermostatBaseConfigFlow(FlowHandler):
for conf in [ for conf in [
CONF_HEATER, CONF_HEATER,
CONF_TEMP_SENSOR, CONF_TEMP_SENSOR,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_WINDOW_SENSOR, CONF_WINDOW_SENSOR,
CONF_MOTION_SENSOR, CONF_MOTION_SENSOR,
CONF_POWER_SENSOR, CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR, CONF_MAX_POWER_SENSOR,
CONF_PRESENCE_SENSOR,
]: ]:
d = data.get(conf, None) d = data.get(conf, None) # pylint: disable=invalid-name
if d is not None and self.hass.states.get(d) is None: if d is not None and self.hass.states.get(d) is None:
_LOGGER.error( _LOGGER.error(
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", "Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration", # pylint: disable=line-too-long
d, d,
) )
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(
@@ -213,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) # 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)
@@ -226,160 +249,64 @@ 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_presets "user", self.STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi
) )
# defaults = self._infos.copy() async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
# errors = {} """Handle the flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_tpi user_input=%s", user_input)
# return await self.generic_step(
# if user_input is not None: "tpi", self.STEP_TPI_DATA_SCHEMA, user_input, self.async_step_presets
# defaults.update(user_input or {}) )
# try:
# await self.validate_input(user_input)
# except UnknownEntity as err:
# errors[str(err)] = "unknown_entity"
# except Exception: # pylint: disable=broad-except
# _LOGGER.exception("Unexpected exception")
# errors["base"] = "unknown"
# else:
# self._infos.update(user_input)
# _LOGGER.debug("_info is now: %s", self._infos)
# return await self.async_step_presets()
#
# user_data_schema = schema_defaults(STEP_USER_DATA_SCHEMA, **defaults)
#
# return self.async_show_form(
# step_id="user", data_schema=user_data_schema, errors=errors
# )
async def async_step_presets(self, user_input: dict | None = None) -> FlowResult: async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
"""Handle the presets flow steps""" """Handle the presets flow steps"""
_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
) )
# if user_input is None:
# return self.async_show_form(
# step_id="presets", data_schema=STEP_PRESETS_DATA_SCHEMA
# )
#
# self._infos.update(user_input)
# _LOGGER.debug("_info is now: %s", self._infos)
#
# return await 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:
"""Handle the window sensor flow steps""" """Handle the window sensor flow steps"""
_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
) )
# if user_input is None:
# return self.async_show_form(
# step_id="window", data_schema=STEP_WINDOW_DATA_SCHEMA
# )
#
# errors = {}
#
# try:
# await self.validate_input(user_input)
# except UnknownEntity as err:
# errors[str(err)] = "unknown_entity"
# except Exception: # pylint: disable=broad-except
# _LOGGER.exception("Unexpected exception")
# errors["base"] = "unknown"
# else:
# self._infos.update(user_input)
# _LOGGER.debug("_info is now: %s", self._infos)
#
# return await self.async_step_motion()
#
# return self.async_show_form(
# step_id="window",
# data_schema=schema_defaults(STEP_WINDOW_DATA_SCHEMA, **user_input),
# errors=errors,
# )
async def async_step_motion(self, user_input: dict | None = None) -> FlowResult: async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
"""Handle the window and motion sensor flow steps""" """Handle the window and motion sensor flow steps"""
_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
) )
# if user_input is None:
# return self.async_show_form(
# step_id="motion", data_schema=STEP_MOTION_DATA_SCHEMA
# )
#
# errors = {}
#
# try:
# await self.validate_input(user_input)
# except UnknownEntity as err:
# errors[str(err)] = "unknown_entity"
# except Exception: # pylint: disable=broad-except
# _LOGGER.exception("Unexpected exception")
# errors["base"] = "unknown"
# else:
# self._infos.update(user_input)
# _LOGGER.debug("_info is now: %s", self._infos)
#
# return await self.async_step_power()
#
# return self.async_show_form(
# step_id="motion",
# data_schema=schema_defaults(STEP_MOTION_DATA_SCHEMA, **user_input),
# errors=errors,
# )
async def async_step_power(self, user_input: dict | None = None) -> FlowResult: async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
"""Handle the power management flow steps""" """Handle the power management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input) _LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
return await self.generic_step( return await self.generic_step(
"power", "power",
STEP_POWER_DATA_SCHEMA, self.STEP_POWER_DATA_SCHEMA,
user_input,
self.async_step_presence,
)
async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
"""Handle the presence management flow steps"""
_LOGGER.debug("Into ConfigFlow.async_step_presence user_input=%s", user_input)
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_finalize, # pylint: disable=no-member self.async_finalize, # pylint: disable=no-member
) )
# if user_input is None:
# return self.async_show_form(
# step_id="power", data_schema=STEP_POWER_DATA_SCHEMA
# )
#
# errors = {}
#
# try:
# await self.validate_input(user_input)
# except UnknownEntity as err:
# errors[str(err)] = "unknown_entity"
# except Exception: # pylint: disable=broad-except
# _LOGGER.exception("Unexpected exception")
# errors["base"] = "unknown"
# else:
# self._infos.update(user_input)
# _LOGGER.debug("_info is now: %s", self._infos)
#
# return self.async_create_entry(
# title=self._infos[CONF_NAME], data=self._infos
# )
#
# return self.async_show_form(
# step_id="power",
# data_schema=schema_defaults(STEP_POWER_DATA_SCHEMA, **user_input),
# errors=errors,
# )
class VersatileThermostatConfigFlow( class VersatileThermostatConfigFlow(
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
): ):
@@ -398,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)
@@ -437,7 +364,17 @@ class VersatileThermostatOptionsFlowHandler(
) )
return await self.generic_step( return await self.generic_step(
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_presets "user", self.STEP_USER_DATA_SCHEMA, user_input, self.async_step_tpi
)
async def async_step_tpi(self, user_input: dict | None = None) -> FlowResult:
"""Handle the tpi flow steps"""
_LOGGER.debug(
"Into OptionsFlowHandler.async_step_tpi user_input=%s", user_input
)
return await self.generic_step(
"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:
@@ -447,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:
@@ -457,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:
@@ -467,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:
@@ -478,115 +415,24 @@ 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,
self.async_step_presence, # pylint: disable=no-member
)
async def async_step_presence(self, user_input: dict | None = None) -> FlowResult:
"""Handle the presence management flow steps"""
_LOGGER.debug(
"Into OptionsFlowHandler.async_step_presence user_input=%s", user_input
)
return await self.generic_step(
"presence",
self.STEP_PRESENCE_DATA_SCHEMA,
user_input, user_input,
self.async_finalize, # pylint: disable=no-member self.async_finalize, # pylint: disable=no-member
) )
#
# async def async_step_presets(self, user_input=None):
# """Manage presets options."""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_presets user_input =%s",
# user_input,
# )
#
# if user_input is not None:
# _LOGGER.debug("We receive the new values: %s", user_input)
# # data = dict(self.config_entry.data)
# for conf in PRESETS_DATA_CONF:
# self._info[conf] = user_input.get(conf)
#
# _LOGGER.debug("updating entry with: %s", self._info)
# return await self.async_step_window()
# else:
# defaults = self._info.copy()
# defaults.update(user_input or {})
# presets_data_schema = schema_defaults(STEP_PRESETS_DATA_SCHEMA, **defaults)
#
# return self.async_show_form(
# step_id="presets",
# data_schema=presets_data_schema,
# )
#
# async def async_step_window(self, user_input=None):
# """Manage window options."""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_window user_input =%s",
# user_input,
# )
#
# if user_input is not None:
# _LOGGER.debug("We receive the new values: %s", user_input)
# # data = dict(self.config_entry.data)
# for conf in WINDOW_DATA_CONF:
# self._info[conf] = user_input.get(conf)
#
# _LOGGER.debug("updating entry with: %s", self._info)
# return await self.async_step_motion()
# else:
# defaults = self._info.copy()
# defaults.update(user_input or {})
# window_data_schema = schema_defaults(STEP_WINDOW_DATA_SCHEMA, **defaults)
#
# return self.async_show_form(
# step_id="window",
# data_schema=window_data_schema,
# )
#
# async def async_step_motion(self, user_input=None):
# """Manage motion options."""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_motion user_input =%s",
# user_input,
# )
#
# if user_input is not None:
# _LOGGER.debug("We receive the new values: %s", user_input)
# # data = dict(self.config_entry.data)
# for conf in MOTION_DATA_CONF:
# self._info[conf] = user_input.get(conf)
#
# _LOGGER.debug("updating entry with: %s", self._info)
# return await self.async_step_power()
# else:
# defaults = self._info.copy()
# defaults.update(user_input or {})
# motion_data_schema = schema_defaults(STEP_MOTION_DATA_SCHEMA, **defaults)
#
# return self.async_show_form(
# step_id="motion",
# data_schema=motion_data_schema,
# )
#
# async def async_step_power(self, user_input=None):
# """Manage power options."""
# _LOGGER.debug(
# "Into OptionsFlowHandler.async_step_power user_input =%s",
# user_input,
# )
#
# if user_input is not None:
# _LOGGER.debug("We receive the new values: %s", user_input)
# # data = dict(self.config_entry.data)
# for conf in POWER_DATA_CONF:
# self._info[conf] = user_input.get(conf)
#
# _LOGGER.debug("updating entry with: %s", self._info)
# self.hass.config_entries.async_update_entry(
# self.config_entry, data=self._info
# )
# return self.async_create_entry(title=None, data=None)
# else:
# defaults = self._info.copy()
# defaults.update(user_input or {})
# power_data_schema = schema_defaults(STEP_POWER_DATA_SCHEMA, **defaults)
#
# return self.async_show_form(
# step_id="power",
# data_schema=power_data_schema,
# )
#
async def async_finalize(self): async def async_finalize(self):
"""Finalization of the ConfigEntry creation""" """Finalization of the ConfigEntry creation"""
_LOGGER.debug( _LOGGER.debug(

View File

@@ -2,15 +2,16 @@
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
PRESET_ACTIVITY, # PRESET_ACTIVITY,
PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
PRESET_COMFORT, PRESET_COMFORT,
PRESET_ECO, PRESET_ECO,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from .prop_algorithm import PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR from .prop_algorithm import (
PROPORTIONAL_FUNCTION_TPI,
)
PRESET_POWER = "power" PRESET_POWER = "power"
@@ -18,6 +19,7 @@ DOMAIN = "versatile_thermostat"
CONF_HEATER = "heater_entity_id" CONF_HEATER = "heater_entity_id"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id" CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_EXTERNAL_TEMP_SENSOR = "external_temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id" CONF_POWER_SENSOR = "power_sensor_entity_id"
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id" CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
CONF_WINDOW_SENSOR = "window_sensor_entity_id" CONF_WINDOW_SENSOR = "window_sensor_entity_id"
@@ -25,45 +27,70 @@ CONF_MOTION_SENSOR = "motion_sensor_entity_id"
CONF_DEVICE_POWER = "device_power" CONF_DEVICE_POWER = "device_power"
CONF_CYCLE_MIN = "cycle_min" CONF_CYCLE_MIN = "cycle_min"
CONF_PROP_FUNCTION = "proportional_function" CONF_PROP_FUNCTION = "proportional_function"
CONF_PROP_BIAS = "proportional_bias"
CONF_WINDOW_DELAY = "window_delay" CONF_WINDOW_DELAY = "window_delay"
CONF_MOTION_DELAY = "motion_delay" CONF_MOTION_DELAY = "motion_delay"
CONF_MOTION_PRESET = "motion_preset" CONF_MOTION_PRESET = "motion_preset"
CONF_NO_MOTION_PRESET = "no_motion_preset" CONF_NO_MOTION_PRESET = "no_motion_preset"
CONF_TPI_COEF_INT = "tpi_coef_int"
CONF_TPI_COEF_EXT = "tpi_coef_ext"
CONF_PRESENCE_SENSOR = "presence_sensor_entity_id"
CONF_PRESET_POWER = "power_temp"
CONF_PRESETS = { CONF_PRESETS = {
p: f"{p}_temp" p: f"{p}_temp"
for p in ( for p in (
PRESET_ECO, PRESET_ECO,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT, PRESET_COMFORT,
PRESET_POWER, PRESET_BOOST,
) )
} }
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_AWAY, PRESET_BOOST] PRESET_AWAY_SUFFIX = "_away"
CONF_PRESETS_AWAY = {
p: f"{p}_temp"
for p in (
PRESET_ECO + PRESET_AWAY_SUFFIX,
PRESET_BOOST + PRESET_AWAY_SUFFIX,
PRESET_COMFORT + PRESET_AWAY_SUFFIX,
)
}
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
CONF_PRESETS_VALUES = list(CONF_PRESETS.values()) CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
CONF_PRESETS_AWAY_VALUES = list(CONF_PRESETS_AWAY.values())
ALL_CONF = [ ALL_CONF = (
CONF_NAME, [
CONF_HEATER, CONF_NAME,
CONF_TEMP_SENSOR, CONF_HEATER,
CONF_POWER_SENSOR, CONF_TEMP_SENSOR,
CONF_MAX_POWER_SENSOR, CONF_EXTERNAL_TEMP_SENSOR,
CONF_WINDOW_SENSOR, CONF_POWER_SENSOR,
CONF_WINDOW_DELAY, CONF_MAX_POWER_SENSOR,
CONF_MOTION_SENSOR, CONF_WINDOW_SENSOR,
CONF_MOTION_DELAY, CONF_WINDOW_DELAY,
CONF_MOTION_PRESET, CONF_MOTION_SENSOR,
CONF_NO_MOTION_PRESET, CONF_MOTION_DELAY,
CONF_DEVICE_POWER, CONF_MOTION_PRESET,
CONF_CYCLE_MIN, CONF_NO_MOTION_PRESET,
CONF_PROP_FUNCTION, CONF_DEVICE_POWER,
CONF_PROP_BIAS, CONF_CYCLE_MIN,
] + CONF_PRESETS_VALUES CONF_PROP_FUNCTION,
CONF_TPI_COEF_INT,
CONF_TPI_COEF_EXT,
CONF_PRESENCE_SENSOR,
]
+ CONF_PRESETS_VALUES
+ CONF_PRESETS_AWAY_VALUES,
)
CONF_FUNCTIONS = [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN] CONF_FUNCTIONS = [
PROPORTIONAL_FUNCTION_TPI,
]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
SERVICE_SET_PRESENCE = "set_presence"
SERVICE_SET_PRESET_TEMPERATURE = "set_preset_temperature"

View File

@@ -5,6 +5,7 @@ _LOGGER = logging.getLogger(__name__)
PROPORTIONAL_FUNCTION_ATAN = "atan" PROPORTIONAL_FUNCTION_ATAN = "atan"
PROPORTIONAL_FUNCTION_LINEAR = "linear" PROPORTIONAL_FUNCTION_LINEAR = "linear"
PROPORTIONAL_FUNCTION_TPI = "tpi"
PROPORTIONAL_MIN_DURATION_SEC = 10 PROPORTIONAL_MIN_DURATION_SEC = 10
@@ -14,47 +15,63 @@ FUNCTION_TYPE = [PROPORTIONAL_FUNCTION_ATAN, PROPORTIONAL_FUNCTION_LINEAR]
class PropAlgorithm: class PropAlgorithm:
"""This class aims to do all calculation of the Proportional alogorithm""" """This class aims to do all calculation of the Proportional alogorithm"""
def __init__(self, function_type: str, bias: float, cycle_min: int): def __init__(
self,
function_type: str,
tpi_coef_int,
tpi_coef_ext,
cycle_min: int,
):
"""Initialisation of the Proportional Algorithm""" """Initialisation of the Proportional Algorithm"""
_LOGGER.debug( _LOGGER.debug(
"Creation new PropAlgorithm function_type: %s, bias: %f, cycle_min:%d", "Creation new PropAlgorithm function_type: %s, tpi_coef_int: %s, tpi_coef_ext: %s, cycle_min:%d",
function_type, function_type,
bias, tpi_coef_int,
tpi_coef_ext,
cycle_min, cycle_min,
) )
# TODO test function_type, bias, cycle_min # TODO test function_type, bias, cycle_min
self._function = function_type self._function = function_type
self._bias = bias self._tpi_coef_int = tpi_coef_int
self._tpi_coef_ext = tpi_coef_ext
self._cycle_min = cycle_min self._cycle_min = cycle_min
self._on_percent = 0
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = self._cycle_min * 60 self._off_time_sec = self._cycle_min * 60
def calculate(self, target_temp: float, current_temp: float): def calculate(
self, target_temp: float, current_temp: float, ext_current_temp: float
):
"""Do the calculation of the duration""" """Do the calculation of the duration"""
if target_temp is None or current_temp is None: if target_temp is None or current_temp is None:
_LOGGER.warning( _LOGGER.warning(
"Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" "Proportional algorithm: calculation is not possible cause target_temp or current_temp is null. Heating will be disabled" # pylint: disable=line-too-long
) )
on_percent = 0 self._on_percent = 0
else: else:
delta_temp = target_temp - current_temp delta_temp = target_temp - current_temp
if self._function == PROPORTIONAL_FUNCTION_LINEAR: delta_ext_temp = (
on_percent = 0.25 * delta_temp + self._bias target_temp - ext_current_temp if ext_current_temp is not None else 0
elif self._function == PROPORTIONAL_FUNCTION_ATAN: )
on_percent = math.atan(delta_temp + self._bias) / 1.4
if self._function == PROPORTIONAL_FUNCTION_TPI:
self._on_percent = (
self._tpi_coef_int * delta_temp
+ self._tpi_coef_ext * delta_ext_temp
)
else: else:
_LOGGER.warning( _LOGGER.warning(
"Proportional algorithm: unknown %s function. Heating will be disabled", "Proportional algorithm: unknown %s function. Heating will be disabled",
self._function, self._function,
) )
on_percent = 0 self._on_percent = 0
# calculated on_time duration in seconds # calculated on_time duration in seconds
if on_percent > 1: if self._on_percent > 1:
on_percent = 1 self._on_percent = 1
if on_percent < 0: if self._on_percent < 0:
on_percent = 0 self._on_percent = 0
self._on_time_sec = on_percent * self._cycle_min * 60 self._on_time_sec = self._on_percent * self._cycle_min * 60
# Do not heat for less than xx sec # Do not heat for less than xx sec
if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC: if self._on_time_sec < PROPORTIONAL_MIN_DURATION_SEC:
@@ -65,17 +82,23 @@ class PropAlgorithm:
) )
self._on_time_sec = 0 self._on_time_sec = 0
self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60 self._off_time_sec = self._cycle_min * 60 - self._on_time_sec
_LOGGER.debug( _LOGGER.debug(
"heating percent calculated for current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", "heating percent calculated for current_temp %.1f, ext_current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)", # pylint: disable=line-too-long
current_temp if current_temp else -1.0, current_temp if current_temp else -9999.0,
target_temp if target_temp else -1.0, ext_current_temp if ext_current_temp else -9999.0,
on_percent, target_temp if target_temp else -9999.0,
self._on_percent,
self.on_time_sec, self.on_time_sec,
self.off_time_sec, self.off_time_sec,
) )
@property
def on_percent(self) -> float:
"""Returns the percentage the heater must be ON (1 means the heater will be always on, 0 never on)""" # pylint: disable=line-too-long
return round(self._on_percent, 2)
@property @property
def on_time_sec(self) -> int: def on_time_sec(self) -> int:
"""Returns the calculated time in sec the heater must be ON""" """Returns the calculated time in sec the heater must be ON"""

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

@@ -4,26 +4,32 @@
"flow_title": "Versatile Thermostat configuration", "flow_title": "Versatile Thermostat configuration",
"step": { "step": {
"user": { "user": {
"title": "Add new Versatile Thermostat2", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"heater_entity_id": "Heater entity id", "heater_entity_id": "Heater entity id",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Function to use (atan is more aggressive)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"proportional_bias": "A bias to use in proportional algorithm" }
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
} }
}, },
"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",
"away_temp": "Temperature in Away preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset", "boost_temp": "Temperature in Boost preset"
"power_temp": "Temperature in Power (overpowering) preset"
} }
}, },
"window": { "window": {
@@ -35,7 +41,7 @@
} }
}, },
"motion": { "motion": {
"title": "Motion sensor management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -46,11 +52,22 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
} }
} }
}, },
@@ -66,26 +83,32 @@
"flow_title": "Versatile Thermostat configuration", "flow_title": "Versatile Thermostat configuration",
"step": { "step": {
"user": { "user": {
"title": "Change a Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"heater_entity_id": "Heater entity id", "heater_entity_id": "Heater entity id",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"proportional_bias": "A bias to use in proportional algorithm" }
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
} }
}, },
"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",
"away_temp": "Temperature in Away preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset", "boost_temp": "Temperature in Boost preset"
"power_temp": "Temperature in Power (overpowering) preset"
} }
}, },
"window": { "window": {
@@ -97,7 +120,7 @@
} }
}, },
"motion": { "motion": {
"title": "Motion sensor management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -108,11 +131,22 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
} }
} }
}, },

View File

@@ -4,26 +4,32 @@
"flow_title": "Versatile Thermostat configuration", "flow_title": "Versatile Thermostat configuration",
"step": { "step": {
"user": { "user": {
"title": "Add new Versatile Thermostat2", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"heater_entity_id": "Heater entity id", "heater_entity_id": "Heater entity id",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Function to use (atan is more aggressive)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"proportional_bias": "A bias to use in proportional algorithm" }
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
} }
}, },
"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",
"away_temp": "Temperature in Away preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset", "boost_temp": "Temperature in Boost preset"
"power_temp": "Temperature in Power (overpowering) preset"
} }
}, },
"window": { "window": {
@@ -35,7 +41,7 @@
} }
}, },
"motion": { "motion": {
"title": "Motion sensor management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -46,11 +52,22 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
} }
} }
}, },
@@ -66,26 +83,32 @@
"flow_title": "Versatile Thermostat configuration", "flow_title": "Versatile Thermostat configuration",
"step": { "step": {
"user": { "user": {
"title": "Change a Versatile Thermostat", "title": "Add new Versatile Thermostat",
"description": "Main mandatory attributes", "description": "Main mandatory attributes",
"data": { "data": {
"name": "Name", "name": "Name",
"heater_entity_id": "Heater entity id", "heater_entity_id": "Heater entity id",
"temperature_sensor_entity_id": "Temperature sensor entity id", "temperature_sensor_entity_id": "Temperature sensor entity id",
"external_temperature_sensor_entity_id": "External temperature sensor entity id",
"cycle_min": "Cycle duration (minutes)", "cycle_min": "Cycle duration (minutes)",
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)", "proportional_function": "Algorithm to use (TPI is the only one for now)"
"proportional_bias": "A bias to use in proportional algorithm" }
},
"tpi": {
"title": "TPI",
"description": "Time Proportional Integral attributes",
"data": {
"tpi_coef_int": "Coefficient to use for internal temperature delta",
"tpi_coef_ext": "Coefficient to use for external temperature delta"
} }
}, },
"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",
"away_temp": "Temperature in Away preset",
"comfort_temp": "Temperature in Comfort preset", "comfort_temp": "Temperature in Comfort preset",
"boost_temp": "Temperature in Boost preset", "boost_temp": "Temperature in Boost preset"
"power_temp": "Temperature in Power (overpowering) preset"
} }
}, },
"window": { "window": {
@@ -97,7 +120,7 @@
} }
}, },
"motion": { "motion": {
"title": "Motion sensor management", "title": "Motion management",
"description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name", "description": "Motion sensor management. Preset can switch automatically depending of a motion detection\nLeave corresponding entity_id empty if not used.\nmotion_preset and no_motion_preset should be set to the corresponding preset name",
"data": { "data": {
"motion_sensor_entity_id": "Motion sensor entity id", "motion_sensor_entity_id": "Motion sensor entity id",
@@ -108,11 +131,22 @@
}, },
"power": { "power": {
"title": "Power management", "title": "Power management",
"description": "Power management attributes.\nGives the power and max power sensor pf your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.", "description": "Power management attributes.\nGives the power and max power sensor of your home.\nThen specify the power consumption of the heater when on.\nAll sensors and device power should have the same unit (kW or W).\nLeave corresponding entity_id empty if not used.",
"data": { "data": {
"power_sensor_entity_id": "Power sensor entity id", "power_sensor_entity_id": "Power sensor entity id",
"max_power_sensor_entity_id": "Max power sensor entity id", "max_power_sensor_entity_id": "Max power sensor entity id",
"device_power": "Device power (kW)" "device_power": "Device power (kW)",
"power_temp": "Temperature for Power shedding"
}
},
"presence": {
"title": "Presence management",
"description": "Presence management attributes.\nGives the a presence sensor of your home (true is someone is present).\nThen specify either the preset to use when presence sensor is false or the offset in temperature to apply.\nIf preset is given, the offset will not be used.\nLeave corresponding entity_id empty if not used.",
"data": {
"presence_sensor_entity_id": "Presence sensor entity id (true is present)",
"eco_away_temp": "Temperature in Eco preset when no presence",
"comfort_away_temp": "Temperature in Comfort preset when no presence",
"boost_away_temp": "Temperature in Boost preset when no presence"
} }
} }
}, },

View File

@@ -10,20 +10,26 @@
"name": "Nom", "name": "Nom",
"heater_entity_id": "Radiateur entity id", "heater_entity_id": "Radiateur entity id",
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)", "proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)"
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel" }
},
"tpi": {
"title": "TPI",
"description": "Attributs de l'algo Time Proportional Integral",
"data": {
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
"tpi_coef_ext": "coeff_ext : Coefficient à utiliser pour le delta de température externe"
} }
}, },
"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",
"away_temp": "Température en preset Away",
"comfort_temp": "Température en preset Comfort", "comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost", "boost_temp": "Température en preset Boost"
"power_temp": "Température en preset Power (overpowering)"
} }
}, },
"window": { "window": {
@@ -50,7 +56,18 @@
"data": { "data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)", "power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement" "device_power": "Puissance de l'équipement",
"power_temp": "Température si délestaqe"
}
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
"data": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
} }
} }
}, },
@@ -66,26 +83,32 @@
"flow_title": "Versatile Thermostat configuration", "flow_title": "Versatile Thermostat configuration",
"step": { "step": {
"user": { "user": {
"title": "Configuration d'un thermostat", "title": "Ajout d'un nouveau thermostat",
"description": "Principaux attributs obligatoires", "description": "Principaux attributs obligatoires",
"data": { "data": {
"name": "Nom", "name": "Nom",
"heater_entity_id": "Radiateur entity id", "heater_entity_id": "Radiateur entity id",
"temperature_sensor_entity_id": "Température sensor entity id", "temperature_sensor_entity_id": "Température sensor entity id",
"external_temperature_sensor_entity_id": "Temperature exterieure sensor entity id",
"cycle_min": "Durée du cycle (minutes)", "cycle_min": "Durée du cycle (minutes)",
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)", "proportional_function": "Algorithm à utiliser (Seul TPI est disponible pour l'instant)"
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel" }
},
"tpi": {
"title": "TPI",
"description": "Attributs de l'algo Time Proportional Integral",
"data": {
"tpi_coef_int": "coeff_int : Coefficient à utiliser pour le delta de température interne",
"tpi_coef_ext": "coeff_ext : Coefficient à utiliser pour le delta de température externe"
} }
}, },
"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",
"away_temp": "Température en preset Away",
"comfort_temp": "Température en preset Comfort", "comfort_temp": "Température en preset Comfort",
"boost_temp": "Température en preset Boost", "boost_temp": "Température en preset Boost"
"power_temp": "Température en preset Power (overpowering)"
} }
}, },
"window": { "window": {
@@ -112,7 +135,18 @@
"data": { "data": {
"power_sensor_entity_id": "Capteur de puissance totale (entity id)", "power_sensor_entity_id": "Capteur de puissance totale (entity id)",
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)", "max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
"device_power": "Puissance de l'équipement" "device_power": "Puissance de l'équipement",
"power_temp": "Température si délestaqe"
}
},
"presence": {
"title": "Gestion de la présence",
"description": "Donnez un capteur de présence (true si quelqu'un est présent).\nEnsuite spécifiez soit un preset à utiliser, soit un offset de température à appliquer lorsque personne n'est présent.\nSi le préset est utilisé, l'offset ne sera pas pris en compte.\nLaissez l'entity id vide si la gestion de la présence est non utilisée.",
"data": {
"presence_sensor_entity_id": "Capteur de présence entity id (true si quelqu'un est présent)",
"eco_away_temp": "Température en preset Eco en cas d'absence",
"comfort_away_temp": "Température en preset Comfort en cas d'absence",
"boost_away_temp": "Température en preset Boost en cas d'absence"
} }
} }
}, },

BIN
images/results-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/results-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB