640 lines
22 KiB
Python
640 lines
22 KiB
Python
import logging
|
|
import voluptuous as vol
|
|
from collections import OrderedDict
|
|
|
|
from .utils.bridge import load_adapter
|
|
|
|
from .utils.helpers import get_device_model, get_trv_intigration
|
|
|
|
from .const import (
|
|
CONF_PROTECT_OVERHEATING,
|
|
CONF_CALIBRATION,
|
|
CONF_CHILD_LOCK,
|
|
CONF_HEAT_AUTO_SWAPPED,
|
|
CONF_HEATER,
|
|
CONF_HOMATICIP,
|
|
CONF_HUMIDITY,
|
|
CONF_MODEL,
|
|
CONF_NO_SYSTEM_MODE_OFF,
|
|
CONF_OFF_TEMPERATURE,
|
|
CONF_OUTDOOR_SENSOR,
|
|
CONF_SENSOR,
|
|
CONF_SENSOR_WINDOW,
|
|
CONF_VALVE_MAINTENANCE,
|
|
CONF_WEATHER,
|
|
CONF_WINDOW_TIMEOUT,
|
|
CONF_CALIBRATION_MODE,
|
|
CONF_TOLERANCE,
|
|
CalibrationMode,
|
|
CalibrationType,
|
|
)
|
|
from homeassistant import config_entries
|
|
from homeassistant.const import CONF_NAME
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers import selector
|
|
from homeassistant.components.climate.const import HVACMode
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
|
|
from . import DOMAIN # pylint:disable=unused-import
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CALIBRATION_TYPE_SELECTOR = selector.SelectSelector(
|
|
selector.SelectSelectorConfig(
|
|
options=[
|
|
selector.SelectOptionDict(
|
|
value=CalibrationType.TARGET_TEMP_BASED,
|
|
label="Target Temperature Based",
|
|
)
|
|
],
|
|
mode=selector.SelectSelectorMode.DROPDOWN,
|
|
)
|
|
)
|
|
|
|
CALIBRATION_TYPE_ALL_SELECTOR = selector.SelectSelector(
|
|
selector.SelectSelectorConfig(
|
|
options=[
|
|
selector.SelectOptionDict(
|
|
value=CalibrationType.TARGET_TEMP_BASED,
|
|
label="Target Temperature Based",
|
|
),
|
|
selector.SelectOptionDict(
|
|
value=CalibrationType.LOCAL_BASED, label="Offset Based"
|
|
),
|
|
],
|
|
mode=selector.SelectSelectorMode.DROPDOWN,
|
|
)
|
|
)
|
|
|
|
CALIBRATION_MODE_SELECTOR = selector.SelectSelector(
|
|
selector.SelectSelectorConfig(
|
|
options=[
|
|
selector.SelectOptionDict(value=CalibrationMode.DEFAULT, label="Normal"),
|
|
selector.SelectOptionDict(
|
|
value=CalibrationMode.FIX_CALIBRATION, label="Agressive"
|
|
),
|
|
selector.SelectOptionDict(
|
|
value=CalibrationMode.HEATING_POWER_CALIBRATION, label="AI Time Based"
|
|
),
|
|
selector.SelectOptionDict(
|
|
value=CalibrationMode.NO_CALIBRATION, label="No Calibration"
|
|
),
|
|
],
|
|
mode=selector.SelectSelectorMode.DROPDOWN,
|
|
)
|
|
)
|
|
|
|
|
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
VERSION = 5
|
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
|
|
|
def __init__(self):
|
|
"""Initialize the config flow."""
|
|
self.name = ""
|
|
self.data = None
|
|
self.model = None
|
|
self.heater_entity_id = None
|
|
self.trv_bundle = []
|
|
self.integration = None
|
|
self.i = 0
|
|
|
|
@staticmethod
|
|
@callback
|
|
def async_get_options_flow(
|
|
config_entry: config_entries.ConfigEntry,
|
|
) -> config_entries.OptionsFlow:
|
|
"""Get the options flow for this handler."""
|
|
return OptionsFlowHandler(config_entry)
|
|
"""Get the options flow for this handler."""
|
|
|
|
async def async_step_confirm(self, user_input=None, confirm_type=None):
|
|
"""Handle user-confirmation of discovered node."""
|
|
errors = {}
|
|
self.data[CONF_HEATER] = self.trv_bundle
|
|
if user_input is not None:
|
|
if self.data is not None:
|
|
_LOGGER.debug("Confirm: %s", self.data[CONF_HEATER])
|
|
await self.async_set_unique_id(self.data["name"])
|
|
return self.async_create_entry(title=self.data["name"], data=self.data)
|
|
if confirm_type is not None:
|
|
errors["base"] = confirm_type
|
|
_trvs = ",".join([x["trv"] for x in self.data[CONF_HEATER]])
|
|
return self.async_show_form(
|
|
step_id="confirm",
|
|
errors=errors,
|
|
description_placeholders={"name": self.data[CONF_NAME], "trv": _trvs},
|
|
)
|
|
|
|
async def async_step_advanced(self, user_input=None, _trv_config=None):
|
|
"""Handle options flow."""
|
|
if user_input is not None:
|
|
self.trv_bundle[self.i]["advanced"] = user_input
|
|
self.trv_bundle[self.i]["adapter"] = None
|
|
|
|
self.i += 1
|
|
if len(self.trv_bundle) > self.i:
|
|
return await self.async_step_advanced(None, self.trv_bundle[self.i])
|
|
|
|
_has_off_mode = True
|
|
for trv in self.trv_bundle:
|
|
if HVACMode.OFF not in self.hass.states.get(
|
|
trv.get("trv")
|
|
).attributes.get("hvac_modes"):
|
|
_has_off_mode = False
|
|
|
|
if _has_off_mode is False:
|
|
return await self.async_step_confirm(None, "no_off_mode")
|
|
return await self.async_step_confirm()
|
|
|
|
user_input = user_input or {}
|
|
homematic = False
|
|
if _trv_config.get("integration").find("homematic") != -1:
|
|
homematic = True
|
|
|
|
fields = OrderedDict()
|
|
|
|
_default_calibration = "target_temp_based"
|
|
_adapter = _trv_config.get("adapter", None)
|
|
if _adapter is not None:
|
|
_info = await _adapter.get_info(self, _trv_config.get("trv"))
|
|
|
|
if _info.get("support_offset", False):
|
|
_default_calibration = "local_calibration_based"
|
|
|
|
if _default_calibration == "local_calibration_based":
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION,
|
|
default=user_input.get(CONF_CALIBRATION, _default_calibration),
|
|
)
|
|
] = CALIBRATION_TYPE_ALL_SELECTOR
|
|
else:
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION,
|
|
default=user_input.get(CONF_CALIBRATION, _default_calibration),
|
|
)
|
|
] = CALIBRATION_TYPE_SELECTOR
|
|
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION_MODE,
|
|
default=user_input.get(
|
|
CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION
|
|
),
|
|
)
|
|
] = CALIBRATION_MODE_SELECTOR
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_PROTECT_OVERHEATING,
|
|
default=user_input.get(CONF_PROTECT_OVERHEATING, False),
|
|
)
|
|
] = bool
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_NO_SYSTEM_MODE_OFF,
|
|
default=user_input.get(CONF_NO_SYSTEM_MODE_OFF, False),
|
|
)
|
|
] = bool
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_HEAT_AUTO_SWAPPED,
|
|
default=user_input.get(CONF_HEAT_AUTO_SWAPPED, False),
|
|
)
|
|
] = bool
|
|
|
|
if _info.get("support_valve", False):
|
|
fields[
|
|
vol.Optional(
|
|
CONF_VALVE_MAINTENANCE,
|
|
default=user_input.get(CONF_VALVE_MAINTENANCE, False),
|
|
)
|
|
] = bool
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_CHILD_LOCK, default=user_input.get(CONF_CHILD_LOCK, False)
|
|
)
|
|
] = bool
|
|
fields[
|
|
vol.Optional(
|
|
CONF_HOMATICIP, default=user_input.get(CONF_HOMATICIP, homematic)
|
|
)
|
|
] = bool
|
|
|
|
return self.async_show_form(
|
|
step_id="advanced",
|
|
data_schema=vol.Schema(fields),
|
|
last_step=False,
|
|
description_placeholders={"trv": _trv_config.get("trv")},
|
|
)
|
|
|
|
async def async_step_user(self, user_input=None):
|
|
errors = {}
|
|
|
|
if user_input is not None:
|
|
if self.data is None:
|
|
self.data = user_input
|
|
self.heater_entity_id = self.data[CONF_HEATER]
|
|
if self.data[CONF_NAME] == "":
|
|
errors["base"] = "no_name"
|
|
if CONF_SENSOR_WINDOW not in self.data:
|
|
self.data[CONF_SENSOR_WINDOW] = None
|
|
if CONF_HUMIDITY not in self.data:
|
|
self.data[CONF_HUMIDITY] = None
|
|
if CONF_OUTDOOR_SENSOR not in self.data:
|
|
self.data[CONF_OUTDOOR_SENSOR] = None
|
|
if CONF_WEATHER not in self.data:
|
|
self.data[CONF_WEATHER] = None
|
|
|
|
if CONF_WINDOW_TIMEOUT in self.data:
|
|
self.data[CONF_WINDOW_TIMEOUT] = (
|
|
int(
|
|
cv.time_period_dict(
|
|
user_input.get(CONF_WINDOW_TIMEOUT, None)
|
|
).total_seconds()
|
|
)
|
|
or 0
|
|
)
|
|
else:
|
|
self.data[CONF_WINDOW_TIMEOUT] = 0
|
|
|
|
if "base" not in errors:
|
|
for trv in self.heater_entity_id:
|
|
_intigration = await get_trv_intigration(self, trv)
|
|
self.trv_bundle.append(
|
|
{
|
|
"trv": trv,
|
|
"integration": _intigration,
|
|
"model": await get_device_model(self, trv),
|
|
"adapter": load_adapter(self, _intigration, trv),
|
|
}
|
|
)
|
|
self.data[CONF_MODEL] = "/".join([x["model"] for x in self.trv_bundle])
|
|
return await self.async_step_advanced(None, self.trv_bundle[0])
|
|
|
|
user_input = user_input or {}
|
|
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=vol.Schema(
|
|
{
|
|
vol.Optional(CONF_NAME, default=user_input.get(CONF_NAME, "")): str,
|
|
vol.Required(CONF_HEATER): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(domain="climate", multiple=True)
|
|
),
|
|
vol.Required(CONF_SENSOR): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "number", "input_number"],
|
|
device_class="temperature",
|
|
multiple=False,
|
|
)
|
|
),
|
|
vol.Optional(CONF_HUMIDITY): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "number", "input_number"],
|
|
device_class="humidity",
|
|
multiple=False,
|
|
)
|
|
),
|
|
vol.Optional(CONF_OUTDOOR_SENSOR): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "input_number", "number"],
|
|
device_class="temperature",
|
|
multiple=False,
|
|
)
|
|
),
|
|
vol.Optional(CONF_SENSOR_WINDOW): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=[
|
|
"group",
|
|
"sensor",
|
|
"input_boolean",
|
|
"binary_sensor",
|
|
],
|
|
multiple=False,
|
|
)
|
|
),
|
|
vol.Optional(CONF_WEATHER): selector.EntitySelector(
|
|
selector.EntitySelectorConfig(domain="weather", multiple=False)
|
|
),
|
|
vol.Optional(CONF_WINDOW_TIMEOUT): selector.DurationSelector(),
|
|
vol.Optional(
|
|
CONF_OFF_TEMPERATURE,
|
|
default=user_input.get(CONF_OFF_TEMPERATURE, 20),
|
|
): int,
|
|
vol.Optional(
|
|
CONF_TOLERANCE,
|
|
default=user_input.get(CONF_TOLERANCE, 0.0),
|
|
): float,
|
|
}
|
|
),
|
|
errors=errors,
|
|
last_step=False,
|
|
)
|
|
|
|
|
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
|
"""Handle a option flow for a config entry."""
|
|
|
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
|
"""Initialize options flow."""
|
|
self.config_entry = config_entry
|
|
self.options = dict(config_entry.options)
|
|
self.i = 0
|
|
self.trv_bundle = []
|
|
self.name = ""
|
|
self._last_step = False
|
|
self.updated_config = {}
|
|
|
|
async def async_step_init(self, user_input=None):
|
|
"""Manage the options."""
|
|
return await self.async_step_user()
|
|
|
|
async def async_step_advanced(
|
|
self, user_input=None, _trv_config=None, _update_config=None
|
|
):
|
|
"""Manage the advanced options."""
|
|
if user_input is not None:
|
|
self.trv_bundle[self.i]["advanced"] = user_input
|
|
self.trv_bundle[self.i]["adapter"] = None
|
|
|
|
self.i += 1
|
|
if len(self.trv_bundle) - 1 >= self.i:
|
|
self._last_step = True
|
|
|
|
if len(self.trv_bundle) > self.i:
|
|
return await self.async_step_advanced(
|
|
None, self.trv_bundle[self.i], _update_config
|
|
)
|
|
|
|
self.updated_config[CONF_HEATER] = self.trv_bundle
|
|
_LOGGER.debug("Updated config: %s", self.updated_config)
|
|
self.hass.config_entries.async_update_entry(
|
|
self.config_entry, data=self.updated_config
|
|
)
|
|
return self.async_create_entry(
|
|
title=self.updated_config["name"], data=self.updated_config
|
|
)
|
|
|
|
user_input = user_input or {}
|
|
homematic = False
|
|
if _trv_config.get("integration").find("homematic") != -1:
|
|
homematic = True
|
|
|
|
fields = OrderedDict()
|
|
|
|
_default_calibration = "target_temp_based"
|
|
self.name = user_input.get(CONF_NAME, "-")
|
|
|
|
_adapter = load_adapter(
|
|
self, _trv_config.get("integration"), _trv_config.get("trv")
|
|
)
|
|
if _adapter is not None:
|
|
_info = await _adapter.get_info(self, _trv_config.get("trv"))
|
|
|
|
if _info.get("support_offset", False):
|
|
_default_calibration = "local_calibration_based"
|
|
|
|
if _default_calibration == "local_calibration_based":
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION,
|
|
default=user_input.get(
|
|
CONF_CALIBRATION,
|
|
_trv_config["advanced"].get(
|
|
CONF_CALIBRATION, _default_calibration
|
|
),
|
|
),
|
|
)
|
|
] = CALIBRATION_TYPE_ALL_SELECTOR
|
|
else:
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION,
|
|
default=user_input.get(
|
|
CONF_CALIBRATION,
|
|
_trv_config["advanced"].get(
|
|
CONF_CALIBRATION, _default_calibration
|
|
),
|
|
),
|
|
)
|
|
] = CALIBRATION_TYPE_SELECTOR
|
|
|
|
fields[
|
|
vol.Required(
|
|
CONF_CALIBRATION_MODE,
|
|
default=_trv_config["advanced"].get(
|
|
CONF_CALIBRATION_MODE, CalibrationMode.HEATING_POWER_CALIBRATION
|
|
),
|
|
)
|
|
] = CALIBRATION_MODE_SELECTOR
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_PROTECT_OVERHEATING,
|
|
default=_trv_config["advanced"].get(CONF_PROTECT_OVERHEATING, False),
|
|
)
|
|
] = bool
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_NO_SYSTEM_MODE_OFF,
|
|
default=_trv_config["advanced"].get(CONF_NO_SYSTEM_MODE_OFF, False),
|
|
)
|
|
] = bool
|
|
|
|
has_auto = False
|
|
trv = self.hass.states.get(_trv_config.get("trv"))
|
|
if HVACMode.AUTO in trv.attributes.get("hvac_modes"):
|
|
has_auto = True
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_HEAT_AUTO_SWAPPED,
|
|
default=_trv_config["advanced"].get(CONF_HEAT_AUTO_SWAPPED, has_auto),
|
|
)
|
|
] = bool
|
|
|
|
if _info.get("support_valve", False):
|
|
fields[
|
|
vol.Optional(
|
|
CONF_VALVE_MAINTENANCE,
|
|
default=_trv_config["advanced"].get(CONF_VALVE_MAINTENANCE, False),
|
|
)
|
|
] = bool
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_CHILD_LOCK,
|
|
default=_trv_config["advanced"].get(CONF_CHILD_LOCK, False),
|
|
)
|
|
] = bool
|
|
fields[
|
|
vol.Optional(
|
|
CONF_HOMATICIP,
|
|
default=_trv_config["advanced"].get(CONF_HOMATICIP, homematic),
|
|
)
|
|
] = bool
|
|
|
|
return self.async_show_form(
|
|
step_id="advanced",
|
|
data_schema=vol.Schema(fields),
|
|
last_step=self._last_step,
|
|
description_placeholders={"trv": _trv_config.get("trv")},
|
|
)
|
|
|
|
async def async_step_user(self, user_input=None):
|
|
if user_input is not None:
|
|
current_config = self.config_entry.data
|
|
self.updated_config = dict(current_config)
|
|
self.updated_config[CONF_SENSOR] = user_input.get(CONF_SENSOR, None)
|
|
self.updated_config[CONF_SENSOR_WINDOW] = user_input.get(
|
|
CONF_SENSOR_WINDOW, None
|
|
)
|
|
self.updated_config[CONF_HUMIDITY] = user_input.get(CONF_HUMIDITY, None)
|
|
self.updated_config[CONF_OUTDOOR_SENSOR] = user_input.get(
|
|
CONF_OUTDOOR_SENSOR, None
|
|
)
|
|
self.updated_config[CONF_WEATHER] = user_input.get(CONF_WEATHER, None)
|
|
|
|
if CONF_WINDOW_TIMEOUT in self.updated_config:
|
|
self.updated_config[CONF_WINDOW_TIMEOUT] = (
|
|
int(
|
|
cv.time_period_dict(
|
|
user_input.get(CONF_WINDOW_TIMEOUT, None)
|
|
).total_seconds()
|
|
)
|
|
or 0
|
|
)
|
|
else:
|
|
self.updated_config[CONF_WINDOW_TIMEOUT] = 0
|
|
|
|
self.updated_config[CONF_OFF_TEMPERATURE] = user_input.get(
|
|
CONF_OFF_TEMPERATURE
|
|
)
|
|
|
|
self.updated_config[CONF_TOLERANCE] = user_input.get(CONF_TOLERANCE, 0.0)
|
|
|
|
for trv in self.updated_config[CONF_HEATER]:
|
|
trv["adapter"] = None
|
|
self.trv_bundle.append(trv)
|
|
|
|
return await self.async_step_advanced(
|
|
None, self.trv_bundle[0], self.updated_config
|
|
)
|
|
|
|
fields = OrderedDict()
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_SENSOR,
|
|
description={
|
|
"suggested_value": self.config_entry.data.get(CONF_SENSOR, "")
|
|
},
|
|
)
|
|
] = selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "number", "input_number"],
|
|
device_class="temperature",
|
|
multiple=False,
|
|
)
|
|
)
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_HUMIDITY,
|
|
description={
|
|
"suggested_value": self.config_entry.data.get(CONF_HUMIDITY, "")
|
|
},
|
|
)
|
|
] = selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "number", "input_number"],
|
|
device_class="humidity",
|
|
multiple=False,
|
|
)
|
|
)
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_SENSOR_WINDOW,
|
|
description={
|
|
"suggested_value": self.config_entry.data.get(
|
|
CONF_SENSOR_WINDOW, ""
|
|
)
|
|
},
|
|
)
|
|
] = selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["group", "sensor", "input_boolean", "binary_sensor"],
|
|
multiple=False,
|
|
)
|
|
)
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_OUTDOOR_SENSOR,
|
|
description={
|
|
"suggested_value": self.config_entry.data.get(
|
|
CONF_OUTDOOR_SENSOR, ""
|
|
)
|
|
},
|
|
)
|
|
] = selector.EntitySelector(
|
|
selector.EntitySelectorConfig(
|
|
domain=["sensor", "input_number", "number"],
|
|
device_class="temperature",
|
|
multiple=False,
|
|
)
|
|
)
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_WEATHER,
|
|
description={
|
|
"suggested_value": self.config_entry.data.get(CONF_WEATHER, "")
|
|
},
|
|
)
|
|
] = selector.EntitySelector(
|
|
selector.EntitySelectorConfig(domain="weather", multiple=False)
|
|
)
|
|
|
|
_timeout = self.config_entry.data.get(CONF_WINDOW_TIMEOUT, 0)
|
|
_timeout = str(cv.time_period_seconds(_timeout))
|
|
_timeout = {
|
|
"hours": int(_timeout.split(":", maxsplit=1)[0]),
|
|
"minutes": int(_timeout.split(":")[1]),
|
|
"seconds": int(_timeout.split(":")[2]),
|
|
}
|
|
fields[
|
|
vol.Optional(
|
|
CONF_WINDOW_TIMEOUT,
|
|
default=_timeout,
|
|
description={"suggested_value": _timeout},
|
|
)
|
|
] = selector.DurationSelector()
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_OFF_TEMPERATURE,
|
|
default=self.config_entry.data.get(CONF_OFF_TEMPERATURE, 5),
|
|
)
|
|
] = int
|
|
|
|
fields[
|
|
vol.Optional(
|
|
CONF_TOLERANCE,
|
|
default=self.config_entry.data.get(CONF_TOLERANCE, 0.0),
|
|
)
|
|
] = float
|
|
|
|
return self.async_show_form(
|
|
step_id="user", data_schema=vol.Schema(fields), last_step=False
|
|
)
|