Full functional and configured release
@@ -1,7 +1,7 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
{
|
||||
"image": "ghcr.io/ludeeus/devcontainer/integration:latest",
|
||||
"name": "Blueprint integration development",
|
||||
"name": "Versatile Thermostat integration",
|
||||
"context": "..",
|
||||
"appPort": [
|
||||
"9123:8123"
|
||||
@@ -18,11 +18,11 @@
|
||||
"editor.tabSize": 4,
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"Bash Profile": {
|
||||
"path": "bash",
|
||||
"args": [ ]
|
||||
"path": "bash",
|
||||
"args": []
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "Bash Profile",
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "Bash Profile",
|
||||
// "terminal.integrated.shell.linux": "/bin/bash",
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.autoSearchPaths": false,
|
||||
|
||||
2
.vscode/tasks.json
vendored
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"label": "Run Home Assistant configuration against /config",
|
||||
"type": "shell",
|
||||
"command": "container check",
|
||||
"command": "container check-config",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
|
||||
179
README.md
@@ -1 +1,178 @@
|
||||
# versatile_thermostat
|
||||
[![GitHub Release][releases-shield]][releases]
|
||||
[![GitHub Activity][commits-shield]][commits]
|
||||
[![License][license-shield]](LICENSE)
|
||||
[![hacs][hacsbadge]][hacs]
|
||||
|
||||
|
||||
_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.
|
||||
|
||||
## 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".
|
||||
This new component "Versatile thermostat" now manage the following use cases :
|
||||
- Configuration through GUI using Config Entry flow,
|
||||
- Explicitely define the temperature for all presets mode,
|
||||
- Unset the preset mode when the temperature is manually defined on a thermostat,
|
||||
- Turn off/on a thermostat when a door or windows is opened/closed after a certain delay,
|
||||
- Set a preset when an activity is detected in a room, and another one after no activity has been detected for a defined time,
|
||||
- Use a proportional algorithm with two function (see below),
|
||||
- Add power management to avoid exceeding a defined total power. When max power is exceeded, a new 'power' preset is set on the climate entity. When power goes below the max, the previous preset is restored.
|
||||
|
||||
## How to install this incredible thermostat
|
||||
|
||||
### HACS installation
|
||||
|
||||
1. Install [HACS](https://hacs.xyz/). That way you get updates automatically.
|
||||
2. Add this Github repository as custom repository in HACS settings.
|
||||
3. search and install "Versatile Thermostat" in HACS and click `install`.
|
||||
4. Restart Home Assistant,
|
||||
5. Then you can add an Versatile Thermostat integration in the integration page. You add as many Versatile Thermostat that you need (typically one per heater that should be managed)
|
||||
|
||||
### Manual installation
|
||||
|
||||
1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
|
||||
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
|
||||
3. In the `custom_components` directory (folder) create a new folder called `versatile_thermostat`.
|
||||
4. Download _all_ the files from the `custom_components/versatile_thermostat/` directory (folder) in this repository.
|
||||
5. Place the files you downloaded in the new directory (folder) you created.
|
||||
6. Restart Home Assistant
|
||||
7. Configure new Versatile Thermostat integration
|
||||
|
||||
## Minimum requirements
|
||||
|
||||
* This implementation can override or superseed the core generic thermostat
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration in configuration.yaml is needed because all configuration is done through the standard GUI when adding the integration.
|
||||
Click on Add integration button in the integration page
|
||||

|
||||
|
||||
Follow the configurations steps as follow:
|
||||
|
||||
### Minimal configuration update
|
||||

|
||||
|
||||
Give the main mandatory attributes:
|
||||
1. a name (will be the integration name and also the climate entity name)
|
||||
2. an equipment entity id which represent the heater. This equipment should be able to switch on or off,
|
||||
3. a sensor entity id which gives the temperature of the room in which the heater is installed,
|
||||
4. a cycle duration in minutes. At each cycle, the heater will be turned on then off for a calculated period in order to reach the targeted temperature (see presents below)
|
||||
5. a function used by the algorithm. 'linear' is the most common function. 'atan' is more aggressive and the targeted temperature will be reach sooner (but the power consumption is greater). Use it for room badly isolated,
|
||||
6. a bias value of type float. Proportional algorithm are known to never reach the targeted temperature. Depending of the room and heater configuration set a bias to reach the target. To evaluate the correct value, set it to 0, set the preset to a target temperature and see the current temperature reach. If it is below the target temperature, set the bias accordingly.
|
||||
|
||||
### Configure the preset temperature
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
Concerning the preset modes, you first have to know that, as defined in the core development documentation (https://developers.home-assistant.io/docs/core/entity/climate/), the preset mode handled are the following :
|
||||
- ECO : Device is running an energy-saving mode
|
||||
- AWAY : Device is in away mode
|
||||
- BOOST : Device turn all valve full up
|
||||
- COMFORT : Device is in comfort mode
|
||||
- POWER : An extra preset used when the power management detects an overpowering situation
|
||||
|
||||
'None' is always added in the list of modes, as it is a way to not use the presets modes but a manual temperature instead.
|
||||
|
||||
!!! IMPORTANT !!! Changing manually the target temperature, set the preset to None (no preset). This way you can always set a target temperature even if no preset are available.
|
||||
|
||||
### Configure the doors/windows turning on/off the thermostats
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
Give the following attributes:
|
||||
1. an entity id of a window/door sensor. This should be a binary_sensor or a input_boolean. The state of the entity should be 'on' or 'off'
|
||||
2. a delay in secondes before any change. This allow to quickly open a window without stopping the heater.
|
||||
|
||||
And that's it ! your thermostat will turn off when the windows is open and be turned back on when it's closed afer the delay.
|
||||
|
||||
Note 1 : this implementation is based on 'normal' door/windows behavior, that's mean it considers it's closed when the state is 'off' and open when the state is 'on'
|
||||
|
||||
Note 2 : If you want to use several door/windows sensors to automatize your thermostat, just create a group with the regular behavior (https://www.home-assistant.io/integrations/binary_sensor.group/).
|
||||
|
||||
### Configure the activity mode or motion detection
|
||||
Click on 'Validate' on the previous page and you will get there:
|
||||

|
||||
|
||||
We will now see how to configure the new Activity mode.
|
||||
What we need:
|
||||
- a motion sensor. The entity id of a motion sensor. Motion sensor states should be 'on' (motion detected) or 'off' (no motion detected)
|
||||
- a "motion delay" duration defining how many time we leave the temperature like in "motion" mode after the last motion is detected.
|
||||
- a target "motion" preset. We will used the same temperature than this preset when an activity is detected.
|
||||
- a target "no motion" preset. We will used the same temperature than this preset when no activity is detected.
|
||||
|
||||
So imagine we want to have the following behavior :
|
||||
- we have room with a thermostat set in activity mode, the "motion" mode chosen is comfort (21.5C), the "no motion" mode chosen is Eco (18.5 C) and the motion delay is 5 min.
|
||||
- the room is empty for a while (no activity detected), the temperature of this room is 18.5 C
|
||||
- somebody enters into the room, an activity is detected the temperature is set to 21.5 C
|
||||
- the person leaves the room, after 5 min the temperature is set back to 18.5 C
|
||||
|
||||
For this to work, the climate thermostat should be in 'activity' preset mode.
|
||||
|
||||
Be aware that as for the others preset modes, Activity will only be proposed if it's correctly configure. In other words, the 4 configuration keys have to be set if you want to see Activity in home assistant Interface
|
||||
|
||||
### Configure the power management
|
||||
This feature allows you to regulate the power consumption of your radiators. Give a sensor to the current power consumption of your house, a sensor to the max power that should not be exceeded, the power consumption of your radiator and the algorithm will not start a radiator if the max power will be exceeded after radiator starts.
|
||||
|
||||

|
||||
|
||||
Note that all power values should have the same units (kW or W for example).
|
||||
This allows you to change the max power along time using a Sceduler or whatever you like.
|
||||
|
||||
|
||||
## Algorithm
|
||||
This integration uses a proportional algorithm. A Proportional algorithm is useful to avoid the oscillation around the target temperature. This algorithm is based on a cycle which alternate heating and stop heating. The proportion of heating vs not heating is determined by the difference between the temperature and the target temperature. Bigger the difference is and bigger is the proportion of heating inside the cycle.
|
||||
This algorithm make the temperature converge and stop oscillating.
|
||||
|
||||
Depending of your area and heater, the convergente temperature can be under the targeted temperature. So a bias parameter is available to fix this. To find the right value of biais, just set it to 0 (no biais), let the temperature converge and see if it is near the targeted temperature. If not adjust the biais. A good value is 0.25 with my accumulator radiator (which are long to heat but keeps the heat for a long time).
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Enjoy !
|
||||
|
||||
## Even Better with Scheduler Component !
|
||||
|
||||
In order to enjoy the full power of Versatile Thermostat, I invite you to use it with https://github.com/nielsfaber/scheduler-component
|
||||
Indeed, the scheduler component porpose a management of the climate base on the preset modes. This feature has limited interest with the generic thermostat but it becomes highly powerfull with Awesome thermostat :
|
||||
|
||||
Starting here, I assume you have installed Awesome Thermostat and Scheduler Component.
|
||||
|
||||
In Scheduler, add a schedule :
|
||||
|
||||

|
||||
|
||||
Choose "climate" group, choose one (or multiple) entity/ies, select "MAKE SCHEME" and click next :
|
||||
(it is possible to choose "SET PRESET", but I prefer to use "MAKE SCHEME")
|
||||
|
||||

|
||||
|
||||
Set your mode scheme and save :
|
||||
|
||||
|
||||

|
||||
|
||||
In this example I set ECO mode during the night and the day when nobody's at home BOOST in the morning and COMFORT in the evening.
|
||||
|
||||
|
||||
I hope this example helps you, don't hesitate to give me your feedbacks !
|
||||
|
||||
## Contributions are welcome!
|
||||
|
||||
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
|
||||
|
||||
***
|
||||
|
||||
[integration_blueprint]: https://github.com/custom-components/integration_blueprint
|
||||
[versatile_thermostat]: https://github.com/jmcollin78/versatile_thermostat
|
||||
[commits-shield]: https://img.shields.io/github/commit-activity/y/jmcollin78/versatile_thermostat.svg?style=for-the-badge
|
||||
[commits]: https://github.com/jmcollin78/versatile_thermostat/commits/master
|
||||
[hacs]: https://github.com/custom-components/hacs
|
||||
[hacs_badge]: https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge
|
||||
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
|
||||
[forum]: https://community.home-assistant.io/
|
||||
[license-shield]: https://img.shields.io/github/license/jmcollin78/versatile_thermostat.svg?style=for-the-badge
|
||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge
|
||||
[releases-shield]: https://img.shields.io/github/release/jmcollin78/versatile_thermostat.svg?style=for-the-badge
|
||||
[releases]: https://github.com/jmcollin78/versatile_thermostat/releases
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import math
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, CoreState
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
callback,
|
||||
CoreState,
|
||||
DOMAIN as HA_DOMAIN,
|
||||
CALLBACK_TYPE,
|
||||
)
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,9 +21,8 @@ from homeassistant.helpers.event import (
|
||||
async_call_later,
|
||||
)
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_PRESET_MODE,
|
||||
@@ -51,6 +58,9 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
@@ -59,18 +69,25 @@ from .const import (
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_PRESETS,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
SUPPORT_FLAGS,
|
||||
PRESET_POWER,
|
||||
)
|
||||
|
||||
from .prop_algorithm import PropAlgorithm
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -92,7 +109,11 @@ async def async_setup_entry(
|
||||
power_sensor_entity_id = entry.data.get(CONF_POWER_SENSOR)
|
||||
max_power_sensor_entity_id = entry.data.get(CONF_MAX_POWER_SENSOR)
|
||||
window_sensor_entity_id = entry.data.get(CONF_WINDOW_SENSOR)
|
||||
window_delay_sec = entry.data.get(CONF_WINDOW_DELAY)
|
||||
motion_sensor_entity_id = entry.data.get(CONF_MOTION_SENSOR)
|
||||
motion_delay_sec = entry.data.get(CONF_MOTION_DELAY)
|
||||
motion_preset = entry.data.get(CONF_MOTION_PRESET)
|
||||
no_motion_preset = entry.data.get(CONF_NO_MOTION_PRESET)
|
||||
device_power = entry.data.get(CONF_DEVICE_POWER)
|
||||
|
||||
presets = {}
|
||||
@@ -116,7 +137,11 @@ async def async_setup_entry(
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
window_sensor_entity_id,
|
||||
window_delay_sec,
|
||||
motion_sensor_entity_id,
|
||||
motion_delay_sec,
|
||||
motion_preset,
|
||||
no_motion_preset,
|
||||
presets,
|
||||
device_power,
|
||||
)
|
||||
@@ -131,6 +156,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
_name: str
|
||||
_heater_entity_id: str
|
||||
_prop_algorithm: PropAlgorithm
|
||||
_async_cancel_cycle: CALLBACK_TYPE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -144,7 +170,11 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
power_sensor_entity_id,
|
||||
max_power_sensor_entity_id,
|
||||
window_sensor_entity_id,
|
||||
window_delay_sec,
|
||||
motion_sensor_entity_id,
|
||||
motion_delay_sec,
|
||||
motion_preset,
|
||||
no_motion_preset,
|
||||
presets,
|
||||
device_power,
|
||||
) -> None:
|
||||
@@ -162,9 +192,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._power_sensor_entity_id = power_sensor_entity_id
|
||||
self._max_power_sensor_entity_id = max_power_sensor_entity_id
|
||||
self._window_sensor_entity_id = window_sensor_entity_id
|
||||
self._window_delay_sec = window_delay_sec
|
||||
self._window_delay_sec = window_delay_sec
|
||||
self._motion_sensor_entity_id = motion_sensor_entity_id
|
||||
self._motion_delay_sec = motion_delay_sec
|
||||
self._motion_preset = motion_preset
|
||||
self._no_motion_preset = no_motion_preset
|
||||
|
||||
# if self.ac_mode:
|
||||
# TODO if self.ac_mode:
|
||||
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
|
||||
# else:
|
||||
self._hvac_list = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
@@ -176,7 +211,9 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
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())
|
||||
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")
|
||||
@@ -184,7 +221,8 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._presets = presets
|
||||
_LOGGER.debug("%s - presets are set to: %s", self, self._presets)
|
||||
# Will be restored if possible
|
||||
self._attr_preset_mode = None # PRESET_NONE
|
||||
self._attr_preset_mode = None
|
||||
self._saved_preset_mode = None
|
||||
|
||||
# Power management
|
||||
self._device_power = device_power
|
||||
@@ -201,7 +239,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
# will be restored if possible
|
||||
self._target_temp = None
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._saved_target_temp = PRESET_NONE
|
||||
self._humidity = None
|
||||
self._ac_mode = False
|
||||
self._fan_mode = None
|
||||
@@ -213,6 +251,10 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self._proportional_function, self._proportional_bias, self._cycle_min
|
||||
)
|
||||
|
||||
self._async_cancel_cycle = None
|
||||
self._window_call_cancel = None
|
||||
self._motion_call_cancel = None
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
|
||||
self,
|
||||
@@ -308,29 +350,36 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set new preset mode."""
|
||||
await self._async_set_preset_mode_internal(preset_mode)
|
||||
await self._async_control_heating()
|
||||
|
||||
async def _async_set_preset_mode_internal(self, preset_mode):
|
||||
"""Set new preset mode."""
|
||||
_LOGGER.info("%s - Set preset_mode: %s", self, preset_mode)
|
||||
if preset_mode not in (self._attr_preset_modes or []):
|
||||
raise ValueError(
|
||||
f"Got unsupported preset_mode {preset_mode}. Must be one of {self._attr_preset_modes}"
|
||||
)
|
||||
|
||||
if preset_mode == self._attr_preset_mode:
|
||||
# I don't think we need to call async_write_ha_state if we didn't change the state
|
||||
return
|
||||
if preset_mode == PRESET_NONE:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
self._target_temp = self._saved_target_temp
|
||||
await self._async_control_heating()
|
||||
if self._saved_target_temp:
|
||||
self._target_temp = self._saved_target_temp
|
||||
elif preset_mode == PRESET_ACTIVITY:
|
||||
self._attr_preset_mode = PRESET_ACTIVITY
|
||||
# TODO self._target_temp = self._presets[self.no_motion_mode]
|
||||
await self._async_control_heating()
|
||||
self._target_temp = self._presets[self._no_motion_preset]
|
||||
else:
|
||||
if self._attr_preset_mode == PRESET_NONE:
|
||||
self._saved_target_temp = self._target_temp
|
||||
self._attr_preset_mode = preset_mode
|
||||
self._target_temp = self._presets[preset_mode]
|
||||
await self._async_control_heating()
|
||||
|
||||
if preset_mode != PRESET_POWER:
|
||||
self._saved_preset_mode = self._attr_preset_mode
|
||||
|
||||
self.async_write_ha_state()
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
@@ -456,7 +505,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - temperature sensor have been retrieved: %f",
|
||||
"%s - temperature sensor have been retrieved: %.1f",
|
||||
self,
|
||||
float(temperature_state.state),
|
||||
)
|
||||
@@ -479,7 +528,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
):
|
||||
self._current_power = float(current_power_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Current power have been retrieved: %f",
|
||||
"%s - Current power have been retrieved: %.3f",
|
||||
self,
|
||||
self._current_power,
|
||||
)
|
||||
@@ -495,7 +544,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
):
|
||||
self._current_power_max = float(current_power_max_state.state)
|
||||
_LOGGER.debug(
|
||||
"%s - Current power max have been retrieved: %f",
|
||||
"%s - Current power max have been retrieved: %.3f",
|
||||
self,
|
||||
self._current_power_max,
|
||||
)
|
||||
@@ -556,12 +605,14 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"No previously saved temperature, setting to %s", self._target_temp
|
||||
)
|
||||
|
||||
self._saved_target_temp = self._target_temp
|
||||
|
||||
# Set default state to off
|
||||
if not self._hvac_mode:
|
||||
self._hvac_mode = HVAC_MODE_OFF
|
||||
|
||||
_LOGGER.info(
|
||||
"%s - restored state is target_temp=%f, preset_mode=%s, hvac_mode=%s",
|
||||
"%s - restored state is target_temp=%.1f, preset_mode=%s, hvac_mode=%s",
|
||||
self,
|
||||
self._target_temp,
|
||||
self._attr_preset_mode,
|
||||
@@ -582,7 +633,6 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
|
||||
self._async_update_temp(new_state)
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
# TODO Not sure we need this - await self._async_control_heating()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@@ -599,15 +649,49 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
)
|
||||
if new_state is None or old_state is None or new_state.state == old_state.state:
|
||||
return
|
||||
if not self._saved_hvac_mode:
|
||||
self._saved_hvac_mode = self._hvac_mode
|
||||
if new_state.state == STATE_OFF:
|
||||
await self.async_set_hvac_mode(self._saved_hvac_mode)
|
||||
elif new_state.state == STATE_ON:
|
||||
self._saved_hvac_mode = self._hvac_mode
|
||||
await self.async_set_hvac_mode(HVAC_MODE_OFF)
|
||||
else:
|
||||
return
|
||||
|
||||
# Check delay condition
|
||||
async def try_window_condition(_):
|
||||
try:
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._window_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=self._window_delay_sec),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Window delay condition is not satisfied. Ignore window event"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Window delay condition is satisfied", self)
|
||||
if not self._saved_hvac_mode:
|
||||
self._saved_hvac_mode = self._hvac_mode
|
||||
|
||||
if new_state.state == STATE_OFF:
|
||||
_LOGGER.info(
|
||||
"%s - Window is closed. Restoring hvac_mode '%s'",
|
||||
self,
|
||||
self._saved_hvac_mode,
|
||||
)
|
||||
await self.async_set_hvac_mode(self._saved_hvac_mode)
|
||||
elif new_state.state == STATE_ON:
|
||||
_LOGGER.info(
|
||||
"%s - Window is open. Set hvac_mode to '%s'", self, HVAC_MODE_OFF
|
||||
)
|
||||
self._saved_hvac_mode = self._hvac_mode
|
||||
await self.async_set_hvac_mode(HVAC_MODE_OFF)
|
||||
|
||||
if self._window_call_cancel:
|
||||
self._window_call_cancel()
|
||||
self._window_call_cancel = None
|
||||
self._window_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=self._window_delay_sec), try_window_condition
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _async_motion_changed(self, event):
|
||||
@@ -625,30 +709,45 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
if new_state is None or new_state.state not in (STATE_OFF, STATE_ON):
|
||||
return
|
||||
|
||||
# if self.motion_delay:
|
||||
# if new_state.state == STATE_ON:
|
||||
# self._target_temp = self._presets[self.motion_mode]
|
||||
# await self._async_control_heating()
|
||||
# self.async_write_ha_state()
|
||||
# else:
|
||||
# async def try_no_motion_condition(_):
|
||||
# if self._attr_preset_mode != PRESET_ACTIVITY:
|
||||
# return
|
||||
# try:
|
||||
# long_enough = condition.state(
|
||||
# self.hass,
|
||||
# self.motion_entity_id,
|
||||
# STATE_OFF,
|
||||
# self.motion_delay,
|
||||
# )
|
||||
# except ConditionError:
|
||||
# long_enough = False
|
||||
# if long_enough:
|
||||
# self._target_temp = self._presets[self.no_motion_mode]
|
||||
# await self._async_control_heating()
|
||||
# self.async_write_ha_state()
|
||||
#
|
||||
# async_call_later(self.hass, self.motion_delay, try_no_motion_condition)
|
||||
# Check delay condition
|
||||
async def try_motion_condition(_):
|
||||
try:
|
||||
long_enough = condition.state(
|
||||
self.hass,
|
||||
self._motion_sensor_entity_id,
|
||||
new_state.state,
|
||||
timedelta(seconds=self._motion_delay_sec),
|
||||
)
|
||||
except ConditionError:
|
||||
long_enough = False
|
||||
|
||||
if not long_enough:
|
||||
_LOGGER.debug(
|
||||
"Motion delay condition is not satisfied. Ignore motion event"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug("%s - Motion delay condition is satisfied", self)
|
||||
new_preset = (
|
||||
self._motion_preset
|
||||
if new_state.state == STATE_ON
|
||||
else self._no_motion_preset
|
||||
)
|
||||
_LOGGER.info(
|
||||
"%s - Motion condition have changes. New preset temp will be %s",
|
||||
self,
|
||||
new_preset,
|
||||
)
|
||||
self._target_temp = self._presets[new_preset]
|
||||
self._prop_algorithm.calculate(self._target_temp, self._cur_temp)
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self._motion_call_cancel:
|
||||
self._motion_call_cancel()
|
||||
self._motion_call_cancel = None
|
||||
self._motion_call_cancel = async_call_later(
|
||||
self.hass, timedelta(seconds=self._motion_delay_sec), try_motion_condition
|
||||
)
|
||||
|
||||
@callback
|
||||
async def _check_switch_initial_state(self):
|
||||
@@ -659,7 +758,7 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
"The climate mode is OFF, but the switch device is ON. Turning off device %s",
|
||||
self._heater_entity_id,
|
||||
)
|
||||
# TODO await self._async_heater_turn_off()
|
||||
await self._async_heater_turn_off()
|
||||
|
||||
@callback
|
||||
def _async_switch_changed(self, event):
|
||||
@@ -721,14 +820,112 @@ class VersatileThermostat(ClimateEntity, RestoreEntity):
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
|
||||
|
||||
async def _async_heater_turn_on(self):
|
||||
"""Turn heater toggleable device on."""
|
||||
data = {ATTR_ENTITY_ID: self._heater_entity_id}
|
||||
await self.hass.services.async_call(
|
||||
HA_DOMAIN, SERVICE_TURN_ON, data, context=self._context
|
||||
)
|
||||
|
||||
async def _async_heater_turn_off(self):
|
||||
"""Turn heater toggleable device off."""
|
||||
data = {ATTR_ENTITY_ID: self._heater_entity_id}
|
||||
await self.hass.services.async_call(
|
||||
HA_DOMAIN, SERVICE_TURN_OFF, data, context=self._context
|
||||
)
|
||||
|
||||
async def check_overpowering(self) -> bool:
|
||||
"""Check the overpowering condition
|
||||
Turn the preset_mode of the heater to 'power' if power conditions are exceeded
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"%s - overpowering check: power=%.3f, max_power=%.3f heater power=%.3f",
|
||||
self,
|
||||
self._current_power,
|
||||
self._current_power_max,
|
||||
self._device_power,
|
||||
)
|
||||
overpowering: bool = (
|
||||
self._current_power + self._device_power >= self._current_power_max
|
||||
)
|
||||
if overpowering:
|
||||
_LOGGER.warning(
|
||||
"%s - overpowering is detected. Heater preset will be set to 'power'",
|
||||
self,
|
||||
)
|
||||
await self._async_set_preset_mode_internal(PRESET_POWER)
|
||||
return overpowering
|
||||
|
||||
# Check if we need to remove the POWER preset
|
||||
if self._attr_preset_mode == PRESET_POWER and not overpowering:
|
||||
_LOGGER.warning(
|
||||
"%s - end of overpowering is detected. Heater preset will be restored to '%s'",
|
||||
self,
|
||||
self._saved_preset_mode,
|
||||
)
|
||||
await self.async_set_preset_mode(
|
||||
self._saved_preset_mode if self._saved_preset_mode else PRESET_NONE
|
||||
)
|
||||
|
||||
async def _async_control_heating(self, time=None):
|
||||
"""The main function used to run the calculation at each cycle"""
|
||||
on_time_sec = self._prop_algorithm.on_time_sec
|
||||
off_time_sec = self._prop_algorithm.off_time_sec
|
||||
|
||||
overpowering: bool = await self.check_overpowering()
|
||||
if overpowering:
|
||||
_LOGGER.debug(
|
||||
"%s - The max power is exceeded. Heater will not be started. preset_mode is now '%s'",
|
||||
self,
|
||||
self._attr_preset_mode,
|
||||
)
|
||||
return
|
||||
|
||||
on_time_sec: int = self._prop_algorithm.on_time_sec
|
||||
off_time_sec: int = self._prop_algorithm.off_time_sec
|
||||
_LOGGER.info(
|
||||
"%s - Running new cycle at %s. on_time_sec=%f, off_time_sec=%f",
|
||||
"%s - Running new cycle at %s. on_time_sec=%.0f, off_time_sec=%.0f",
|
||||
self,
|
||||
time,
|
||||
on_time_sec,
|
||||
off_time_sec,
|
||||
)
|
||||
|
||||
# Cancel eventual previous cycle if any
|
||||
if self._async_cancel_cycle is not None:
|
||||
_LOGGER.debug("Cancelling the previous cycle that was running")
|
||||
self._async_cancel_cycle()
|
||||
self._async_cancel_cycle = None
|
||||
await self._async_heater_turn_off()
|
||||
|
||||
if self._hvac_mode == HVAC_MODE_HEAT:
|
||||
_LOGGER.info(
|
||||
"%s - start heating for %d min %d sec ",
|
||||
self,
|
||||
on_time_sec // 60,
|
||||
on_time_sec % 60,
|
||||
)
|
||||
|
||||
await self._async_heater_turn_on()
|
||||
|
||||
async def _turn_off(_):
|
||||
_LOGGER.info(
|
||||
"%s - stop heating for %d min %d sec",
|
||||
self,
|
||||
off_time_sec // 60,
|
||||
off_time_sec % 60,
|
||||
)
|
||||
await self._async_heater_turn_off()
|
||||
self._async_cancel_cycle = None
|
||||
|
||||
# Program turn off
|
||||
self._async_cancel_cycle = async_call_later(
|
||||
self.hass,
|
||||
on_time_sec,
|
||||
_turn_off,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self):
|
||||
"""update the entity if the config entry have been updated
|
||||
Note: this don't work either
|
||||
"""
|
||||
_LOGGER.info("%s - The config entry have been updated.")
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow as HAConfigFlow,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowHandler
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -26,11 +27,16 @@ from .const import (
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
ALL_CONF,
|
||||
CONF_PRESETS,
|
||||
CONF_PRESETS_SELECTIONABLE,
|
||||
CONF_FUNCTIONS,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
@@ -46,21 +52,92 @@ 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),
|
||||
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.Optional(CONF_MOTION_SENSOR): cv.string,
|
||||
vol.Optional(CONF_DEVICE_POWER): vol.Coerce(float),
|
||||
}
|
||||
).extend(
|
||||
)
|
||||
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(
|
||||
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
|
||||
)
|
||||
PRESETS_DATA_CONF = [v for (_, v) in CONF_PRESETS.items()]
|
||||
|
||||
STEP_WINDOW_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_WINDOW_SENSOR): cv.string,
|
||||
vol.Optional(CONF_WINDOW_DELAY, default=30): cv.positive_int,
|
||||
}
|
||||
)
|
||||
WINDOW_DATA_CONF = [CONF_WINDOW_SENSOR, CONF_WINDOW_DELAY]
|
||||
|
||||
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
|
||||
),
|
||||
}
|
||||
)
|
||||
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):
|
||||
@@ -83,124 +160,437 @@ def schema_defaults(schema, **defaults):
|
||||
return copy
|
||||
|
||||
|
||||
# class PlaceholderHub:
|
||||
# """Placeholder class to make tests pass.
|
||||
#
|
||||
# TODO Remove this placeholder class and replace with things from your PyPI package.
|
||||
# """
|
||||
#
|
||||
# def __init__(self, name: str, heater_entity_id: str) -> None:
|
||||
# """Initialize."""
|
||||
# self.name = name
|
||||
# self.heater_entity_id = heater_entity_id
|
||||
#
|
||||
# # async def authenticate(self, username: str, password: str) -> bool:
|
||||
# # """Test if we can authenticate with the host."""
|
||||
# # return True
|
||||
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant, data: dict[str, str, str, Any]
|
||||
) -> dict[str]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
# If your PyPI package is not built with async, pass your methods
|
||||
# to the executor:
|
||||
# await hass.async_add_executor_job(
|
||||
# your_validate_func, data["username"], data["password"]
|
||||
# )
|
||||
|
||||
# hub = PlaceholderHub(data["name"], data["heater_id"])
|
||||
|
||||
# if not await hub.authenticate(data["username"], data["password"]):
|
||||
# raise InvalidAuth
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": data["name"]}
|
||||
|
||||
|
||||
class VersatileThermostatConfigFlow(HAConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Versatile Thermostat."""
|
||||
class VersatileThermostatBaseConfigFlow(FlowHandler):
|
||||
"""The base Config flow class. Used to put some code in commons."""
|
||||
|
||||
VERSION = 1
|
||||
_infos: dict
|
||||
|
||||
def __init__(self, infos) -> None:
|
||||
super().__init__()
|
||||
_LOGGER.debug("CTOR BaseConfigFlow infos: %s", infos)
|
||||
self._infos = infos
|
||||
|
||||
async def validate_input(self, data: dict) -> dict[str]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_*_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
# check the heater_entity_id
|
||||
for conf in [
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
]:
|
||||
d = data.get(conf, None)
|
||||
if d is not None and self.hass.states.get(d) is None:
|
||||
_LOGGER.error(
|
||||
"Entity id %s doesn't have any state. We cannot use it in the Versatile Thermostat configuration",
|
||||
d,
|
||||
)
|
||||
raise UnknownEntity(conf)
|
||||
|
||||
async def generic_step(self, step_id, data_schema, user_input, next_step_function):
|
||||
"""A generic method step"""
|
||||
_LOGGER.debug(
|
||||
"Into ConfigFlow.async_step_%s user_input=%s", step_id, user_input
|
||||
)
|
||||
|
||||
defaults = self._infos.copy()
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
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 next_step_function()
|
||||
|
||||
ds = schema_defaults(data_schema, **defaults)
|
||||
|
||||
return self.async_show_form(step_id=step_id, data_schema=ds, errors=errors)
|
||||
|
||||
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_user user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
# defaults = self._infos.copy()
|
||||
# errors = {}
|
||||
|
||||
#
|
||||
# if user_input is not None:
|
||||
# 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:
|
||||
"""Handle the presets flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_presets user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"presets", STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window
|
||||
)
|
||||
|
||||
# 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:
|
||||
"""Handle the window sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_window user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"window", STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion
|
||||
)
|
||||
|
||||
# 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:
|
||||
"""Handle the window and motion sensor flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_motion user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"motion", STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power
|
||||
)
|
||||
|
||||
# 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:
|
||||
"""Handle the power management flow steps"""
|
||||
_LOGGER.debug("Into ConfigFlow.async_step_power user_input=%s", user_input)
|
||||
|
||||
return await self.generic_step(
|
||||
"power",
|
||||
STEP_POWER_DATA_SCHEMA,
|
||||
user_input,
|
||||
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(
|
||||
VersatileThermostatBaseConfigFlow, HAConfigFlow, domain=DOMAIN
|
||||
):
|
||||
"""Handle a config flow for Versatile Thermostat."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# self._info = dict()
|
||||
super().__init__(dict())
|
||||
_LOGGER.debug("CTOR ConfigFlow")
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry):
|
||||
"""Get options flow for this handler."""
|
||||
"""Get options flow for this handler"""
|
||||
return VersatileThermostatOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
async def async_finalize(self):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
_LOGGER.debug("CTOR ConfigFlow.async_finalize")
|
||||
return self.async_create_entry(title=self._infos[CONF_NAME], data=self._infos)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
class UnknownEntity(HomeAssistantError):
|
||||
"""Error to indicate there is an unknown entity_id given."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class VersatileThermostatOptionsFlowHandler(OptionsFlow):
|
||||
class VersatileThermostatOptionsFlowHandler(
|
||||
VersatileThermostatBaseConfigFlow, OptionsFlow
|
||||
):
|
||||
"""Handle options flow for Versatile Thermostat integration."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry):
|
||||
"""Initialize options flow."""
|
||||
super().__init__(config_entry.data.copy())
|
||||
self.config_entry = config_entry
|
||||
_LOGGER.debug(
|
||||
"CTOR VersatileThermostatOptionsFlowHandler config_entry.data: %s, entry_id: %s",
|
||||
config_entry.data,
|
||||
"CTOR VersatileThermostatOptionsFlowHandler info: %s, entry_id: %s",
|
||||
self._infos,
|
||||
config_entry.entry_id,
|
||||
)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage basic options."""
|
||||
_LOGGER.debug(
|
||||
"Into VersatileThermostatOptionsFlowHandler.async_step_init user_input =%s, config_entry.data=%s",
|
||||
"Into OptionsFlowHandler.async_step_init user_input =%s",
|
||||
user_input,
|
||||
self.config_entry.data,
|
||||
)
|
||||
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 ALL_CONF:
|
||||
data[conf] = user_input.get(conf)
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
return self.async_create_entry(title=None, data=None)
|
||||
else:
|
||||
defaults = self.config_entry.data.copy()
|
||||
defaults.update(user_input or {})
|
||||
user_data_schema = schema_defaults(STEP_USER_DATA_SCHEMA, **defaults)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=user_data_schema,
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_user user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"user", STEP_USER_DATA_SCHEMA, user_input, self.async_step_presets
|
||||
)
|
||||
|
||||
async def async_step_presets(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the presets flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_presets user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"presets", STEP_PRESETS_DATA_SCHEMA, user_input, self.async_step_window
|
||||
)
|
||||
|
||||
async def async_step_window(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the window sensor flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_window user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"window", STEP_WINDOW_DATA_SCHEMA, user_input, self.async_step_motion
|
||||
)
|
||||
|
||||
async def async_step_motion(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the window and motion sensor flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_motion user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"motion", STEP_MOTION_DATA_SCHEMA, user_input, self.async_step_power
|
||||
)
|
||||
|
||||
async def async_step_power(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle the power management flow steps"""
|
||||
_LOGGER.debug(
|
||||
"Into OptionsFlowHandler.async_step_power user_input=%s", user_input
|
||||
)
|
||||
|
||||
return await self.generic_step(
|
||||
"power",
|
||||
STEP_POWER_DATA_SCHEMA,
|
||||
user_input,
|
||||
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):
|
||||
"""Finalization of the ConfigEntry creation"""
|
||||
_LOGGER.debug(
|
||||
"CTOR ConfigFlow.async_finalize - updating entry with: %s", self._infos
|
||||
)
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=self._infos)
|
||||
return self.async_create_entry(title=None, data=None)
|
||||
|
||||
@@ -26,20 +26,10 @@ CONF_DEVICE_POWER = "device_power"
|
||||
CONF_CYCLE_MIN = "cycle_min"
|
||||
CONF_PROP_FUNCTION = "proportional_function"
|
||||
CONF_PROP_BIAS = "proportional_bias"
|
||||
|
||||
ALL_CONF = [
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
]
|
||||
CONF_WINDOW_DELAY = "window_delay"
|
||||
CONF_MOTION_DELAY = "motion_delay"
|
||||
CONF_MOTION_PRESET = "motion_preset"
|
||||
CONF_NO_MOTION_PRESET = "no_motion_preset"
|
||||
|
||||
CONF_PRESETS = {
|
||||
p: f"{p}_temp"
|
||||
@@ -52,6 +42,28 @@ CONF_PRESETS = {
|
||||
)
|
||||
}
|
||||
|
||||
CONF_PRESETS_SELECTIONABLE = [PRESET_ECO, PRESET_COMFORT, PRESET_AWAY, PRESET_BOOST]
|
||||
|
||||
CONF_PRESETS_VALUES = list(CONF_PRESETS.values())
|
||||
|
||||
ALL_CONF = [
|
||||
CONF_NAME,
|
||||
CONF_HEATER,
|
||||
CONF_TEMP_SENSOR,
|
||||
CONF_POWER_SENSOR,
|
||||
CONF_MAX_POWER_SENSOR,
|
||||
CONF_WINDOW_SENSOR,
|
||||
CONF_WINDOW_DELAY,
|
||||
CONF_MOTION_SENSOR,
|
||||
CONF_MOTION_DELAY,
|
||||
CONF_MOTION_PRESET,
|
||||
CONF_NO_MOTION_PRESET,
|
||||
CONF_DEVICE_POWER,
|
||||
CONF_CYCLE_MIN,
|
||||
CONF_PROP_FUNCTION,
|
||||
CONF_PROP_BIAS,
|
||||
] + CONF_PRESETS_VALUES
|
||||
|
||||
CONF_FUNCTIONS = [PROPORTIONAL_FUNCTION_LINEAR, PROPORTIONAL_FUNCTION_ATAN]
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"name": "Versatile Thermostat",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
|
||||
"issue_tracker": "https://github.com/jmcollin78/versatile_thermostat/issues",
|
||||
"requirements": [],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
@@ -12,6 +13,7 @@
|
||||
"codeowners": [
|
||||
"@jmcollin78"
|
||||
],
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "calculated",
|
||||
"integration_type": "device"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ class PropAlgorithm:
|
||||
# calculated on_time duration in seconds
|
||||
if on_percent > 1:
|
||||
on_percent = 1
|
||||
if on_percent < 0:
|
||||
on_percent = 0
|
||||
self._on_time_sec = on_percent * self._cycle_min * 60
|
||||
|
||||
# Do not heat for less than xx sec
|
||||
@@ -66,18 +68,20 @@ class PropAlgorithm:
|
||||
self._off_time_sec = (1.0 - on_percent) * self._cycle_min * 60
|
||||
|
||||
_LOGGER.debug(
|
||||
"heating percent calculated is %f, on_time is %f (sec), off_time is %s (sec)",
|
||||
"heating percent calculated for current_temp %.1f and target_temp %.1f is %.2f, on_time is %d (sec), off_time is %d (sec)",
|
||||
current_temp if current_temp else -1.0,
|
||||
target_temp if target_temp else -1.0,
|
||||
on_percent,
|
||||
self._on_time_sec,
|
||||
self._off_time_sec,
|
||||
self.on_time_sec,
|
||||
self.off_time_sec,
|
||||
)
|
||||
|
||||
@property
|
||||
def on_time_sec(self):
|
||||
def on_time_sec(self) -> int:
|
||||
"""Returns the calculated time in sec the heater must be ON"""
|
||||
return self._on_time_sec
|
||||
return int(self._on_time_sec)
|
||||
|
||||
@property
|
||||
def off_time_sec(self):
|
||||
def off_time_sec(self) -> int:
|
||||
"""Returns the calculated time in sec the heater must be OFF"""
|
||||
return self._off_time_sec
|
||||
return int(self._off_time_sec)
|
||||
|
||||
@@ -1,31 +1,127 @@
|
||||
{
|
||||
"title": "Versatile Thermostat configuration",
|
||||
"config": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat",
|
||||
"title": "Add new Versatile Thermostat2",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "[%key:config::data::name%]",
|
||||
"heater_entity_id": "[%key:config::data::heater_entity_id%]",
|
||||
"temperature_sensor_entity_id": "[%key:config::data::temperature_sensor_entity_id%]",
|
||||
"power_sensor_entity_id": "[%key:config::data::power_sensor_entity_id%]",
|
||||
"max_power_sensor_entity_id": "[%key:config::data::max_power_sensor_entity_id%]",
|
||||
"window_sensor_entity_id": "[%key:config::data::window_sensor_entity_id%]",
|
||||
"motion_sensor_entity_id": "[%key:config::data::motion_sensor_entity_id%]",
|
||||
"device_power": "[%key:config::data::device_power%]",
|
||||
"cycle_min": "[%key:config::data::cycle_min%]",
|
||||
"proportional_function": "[%key:config::data::proportional_function%]",
|
||||
"proportional_bias": "[%key:config::data::proportional_bias%]"
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"away_temp": "Temperature in Away preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"power_temp": "Temperature in Power (overpowering) preset"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor 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",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"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.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Change a Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"away_temp": "Temperature in Away preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"power_temp": "Temperature in Power (overpowering) preset"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor 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",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"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.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,127 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"device_power": "Device power (kW)",
|
||||
"cycle_min": "Cycle duration (min)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
}
|
||||
"title": "Versatile Thermostat configuration",
|
||||
"config": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add new Versatile Thermostat2",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"away_temp": "Temperature in Away preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"power_temp": "Temperature in Power (overpowering) preset"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor 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",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"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.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Change a Versatile Thermostat",
|
||||
"description": "Main mandatory attributes",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"heater_entity_id": "Heater entity id",
|
||||
"temperature_sensor_entity_id": "Temperature sensor entity id",
|
||||
"cycle_min": "Cycle duration (minutes)",
|
||||
"proportional_function": "Function to use in proportional algorithm (atan is more aggressive)",
|
||||
"proportional_bias": "A bias to use in proportional algorithm"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "For each presets, give the target temperature",
|
||||
"data": {
|
||||
"eco_temp": "Temperature in Eco preset",
|
||||
"away_temp": "Temperature in Away preset",
|
||||
"comfort_temp": "Temperature in Comfort preset",
|
||||
"boost_temp": "Temperature in Boost preset",
|
||||
"power_temp": "Temperature in Power (overpowering) preset"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Window management",
|
||||
"description": "Open window management.\nLeave corresponding entity_id empty if not used.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Window sensor entity id",
|
||||
"window_delay": "Window delay (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Motion sensor 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",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Motion sensor entity id",
|
||||
"motion_delay": "Motion delay (seconds)",
|
||||
"motion_preset": "Preset to use when motion is detected",
|
||||
"no_motion_preset": "Preset to use when no motion is detected"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"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.",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Power sensor entity id",
|
||||
"max_power_sensor_entity_id": "Max power sensor entity id",
|
||||
"device_power": "Device power (kW)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Unexpected error",
|
||||
"unknown_entity": "Unknown entity id"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
}
|
||||
}
|
||||
127
custom_components/versatile_thermostat/translations/fr.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"title": "Versatile Thermostat configuration",
|
||||
"config": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Ajout d'un nouveau thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)",
|
||||
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible",
|
||||
"data": {
|
||||
"eco_temp": "Température en preset Eco",
|
||||
"away_temp": "Température en preset Away",
|
||||
"comfort_temp": "Température en preset Comfort",
|
||||
"boost_temp": "Température en preset Boost",
|
||||
"power_temp": "Température en preset Power (overpowering)"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ouverture sensor entity id",
|
||||
"window_delay": "Délai avant extinction (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_delay": "Délai avant changement (seconds)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestion de l'énergie",
|
||||
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"device_power": "Puissance de l'équipement"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"flow_title": "Versatile Thermostat configuration",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Configuration d'un thermostat",
|
||||
"description": "Principaux attributs obligatoires",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"heater_entity_id": "Radiateur entity id",
|
||||
"temperature_sensor_entity_id": "Température sensor entity id",
|
||||
"cycle_min": "Durée du cycle (minutes)",
|
||||
"proportional_function": "Fonction de l'algorithm proportionnel à utiliser (atan est plus aggressive)",
|
||||
"proportional_bias": "Un biais à utiliser dans l'algorithm proportionnel"
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"description": "Pour chaque preset, donnez la température cible",
|
||||
"data": {
|
||||
"eco_temp": "Température en preset Eco",
|
||||
"away_temp": "Température en preset Away",
|
||||
"comfort_temp": "Température en preset Comfort",
|
||||
"boost_temp": "Température en preset Boost",
|
||||
"power_temp": "Température en preset Power (overpowering)"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"title": "Gestion d'une ouverture",
|
||||
"description": "Coupe le radiateur si l'ouverture est ouverte.\nLaissez l'entity id vide si non utilisé.",
|
||||
"data": {
|
||||
"window_sensor_entity_id": "Ouverture sensor entity id",
|
||||
"window_delay": "Délai avant extinction (seconds)"
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"title": "Gestion de la détection de mouvement",
|
||||
"description": "Le preset s'ajuste automatiquement si un mouvement est détecté\nLaissez l'entity id vide si non utilisé.\n'Preset mouvement' et 'Preset sans mouvement' doivent être choisis avec les preset à utiliser.",
|
||||
"data": {
|
||||
"motion_sensor_entity_id": "Détecteur de mouvement entity id",
|
||||
"motion_delay": "Délai avant changement (seconds)",
|
||||
"motion_preset": "Preset à utiliser si mouvement détecté",
|
||||
"no_motion_preset": "Preset à utiliser si pas de mouvement détecté"
|
||||
}
|
||||
},
|
||||
"power": {
|
||||
"title": "Gestion de l'énergie",
|
||||
"description": "Sélectionne automatiquement le preset 'power' si la puissance consommée est supérieure à un maximum.\nDonnez les entity id des capteurs qui mesurent la puissance totale et la puissance max autorisée.\nEnsuite donnez la puissance de l'équipement.\nTous les capteurs et la puissance consommée par l'équipement doivent avoir la même unité de mesure (kW ou W).",
|
||||
"data": {
|
||||
"power_sensor_entity_id": "Capteur de puissance totale (entity id)",
|
||||
"max_power_sensor_entity_id": "Capteur de puissance Max (entity id)",
|
||||
"device_power": "Puissance de l'équipement"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "Erreur inattendue",
|
||||
"unknown_entity": "entity id inconnu"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Le device est déjà configuré"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
hacs.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Versatile Thermostat",
|
||||
"content_in_root": false,
|
||||
"render_readme": true,
|
||||
"hide_default_branch": false,
|
||||
"homeassistant": "2022.2.0"
|
||||
}
|
||||
BIN
images/add-an-integration.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
images/config-page-1.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
images/config-page-2.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/config-page-3.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/config-page-4.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
images/config-page-5.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
images/icon.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
images/icon@2x.png
Normal file
|
After Width: | Height: | Size: 217 KiB |