Initialisation of component ok

This commit is contained in:
Jean-Marc Collin
2022-12-28 17:25:25 +01:00
parent b7018c29b9
commit 3a0a475810
19 changed files with 1480 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
"""The Versatile Thermostat integration."""
from __future__ import annotations
from typing import Dict
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .climate import VersatileThermostat
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Versatile Thermostat from a config entry."""
_LOGGER.debug(
"Calling async_setup_entry entry: entry_id='%s', value='%s'",
entry.entry_id,
entry.data,
)
# hass.data.setdefault(DOMAIN, {})
# TODO 1. Create API instance
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
if api is None:
api = VersatileThermostatAPI(hass)
# TODO 2. Validate the API connection (and authentication)
# TODO 3. Store an API object for your platforms to access
api.add_entry(entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
api: VersatileThermostatAPI = hass.data.get(DOMAIN)
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
if api:
api.remove_entry(entry)
return unload_ok
class VersatileThermostatAPI(Dict):
"""The VersatileThermostatAPI"""
_hass: HomeAssistant
# _entries: Dict(str, ConfigEntry)
def __init__(self, hass):
_LOGGER.debug("building a VersatileThermostatAPI")
super().__init__()
self._hass = hass
# self._entries = dict()
# Add the API in hass.data
self._hass.data[DOMAIN] = self
def add_entry(self, entry: ConfigEntry):
"""Add a new entry"""
_LOGGER.debug("Add the entry %s", entry.entry_id)
# self._entries[entry.entry_id] = entry
# Add the entry in hass.data
self._hass.data[DOMAIN][entry.entry_id] = entry
def remove_entry(self, entry: ConfigEntry):
"""Remove an entry"""
_LOGGER.debug("Remove the entry %s", entry.entry_id)
# self._entries.pop(entry.entry_id)
self._hass.data[DOMAIN].pop(entry.entry_id)
# If not more entries are preset, remove the API
if len(self) == 0:
_LOGGER.debug("No more entries-> Remove the API from DOMAIN")
self._hass.data.pop(DOMAIN)
@property
def hass(self):
"""Get the HomeAssistant object"""
return self._hass

View File

@@ -0,0 +1,691 @@
import math
from homeassistant.core import HomeAssistant, callback, CoreState
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
async_call_later,
)
import logging
_LOGGER = logging.getLogger(__name__)
from homeassistant.components.climate.const import (
ATTR_PRESET_MODE,
ATTR_FAN_MODE,
CURRENT_HVAC_COOL,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_ACTIVITY,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
PRESET_SLEEP,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import (
UnitOfTemperature,
ATTR_TEMPERATURE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
CONF_NAME,
CONF_UNIQUE_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
STATE_OFF,
STATE_ON,
EVENT_HOMEASSISTANT_START,
)
from .const import (
DOMAIN,
CONF_HEATER,
CONF_POWER_SENSOR,
CONF_TEMP_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_MOTION_SENSOR,
CONF_WINDOW_SENSOR,
CONF_DEVICE_POWER,
CONF_PRESETS,
SUPPORT_FLAGS,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VersatileThermostat thermostat with config flow."""
_LOGGER.debug(
"Calling async_setup_entry entry=%s, data=%s", entry.entry_id, entry.data
)
unique_id = entry.entry_id
name = entry.data.get(CONF_NAME)
heater_entity_id = entry.data.get(CONF_HEATER)
temp_sensor_entity_id = entry.data.get(CONF_TEMP_SENSOR)
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)
motion_sensor_entity_id = entry.data.get(CONF_MOTION_SENSOR)
device_power = entry.data.get(CONF_DEVICE_POWER)
presets = {}
for (key, value) in CONF_PRESETS.items():
_LOGGER.debug("looking for key=%s, value=%s", key, value)
if value in entry.data:
presets[key] = entry.data.get(value)
else:
_LOGGER.debug("value %s not found in Entry", value)
async_add_entities(
[
VersatileThermostat(
unique_id,
name,
heater_entity_id,
temp_sensor_entity_id,
power_sensor_entity_id,
max_power_sensor_entity_id,
window_sensor_entity_id,
motion_sensor_entity_id,
presets,
device_power,
)
],
True,
)
class VersatileThermostat(ClimateEntity, RestoreEntity):
"""Representation of a Versatile Thermostat device."""
_name: str
_heater_entity_id: str
def __init__(
self,
unique_id,
name,
heater_entity_id,
temp_sensor_entity_id,
power_sensor_entity_id,
max_power_sensor_entity_id,
window_sensor_entity_id,
motion_sensor_entity_id,
presets,
device_power,
) -> None:
"""Initialize the thermostat."""
super().__init__()
self._unique_id = unique_id
self._name = name
self._heater_entity_id = heater_entity_id
self._temp_sensor_entity_id = temp_sensor_entity_id
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._motion_sensor_entity_id = motion_sensor_entity_id
# if self.ac_mode:
# self.hvac_list = [HVAC_MODE_COOL, HVAC_MODE_OFF]
# else:
self._hvac_list = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
self._unit = TEMP_CELSIUS
# Will be restored if possible
self._hvac_mode = None # HVAC_MODE_OFF
self._saved_hvac_mode = self._hvac_mode
self._support_flags = SUPPORT_FLAGS
if len(presets):
self._support_flags = SUPPORT_FLAGS | SUPPORT_PRESET_MODE
self._attr_preset_modes = [PRESET_NONE] + list(presets.keys())
_LOGGER.debug("Set preset_modes to %s", self._attr_preset_modes)
else:
_LOGGER.debug("No preset_modes")
self._attr_preset_modes = [PRESET_NONE]
self._presets = presets
_LOGGER.debug("%s - presets are set to: %s", self, self._presets)
# Will be restored if possible
self._attr_preset_mode = None # PRESET_NONE
# Power management
self._device_power = device_power
if (
self._max_power_sensor_entity_id
and self._power_sensor_entity_id
and self._device_power
):
self._pmax_on = True
self._current_power = -1
self._current_power_max = -1
else:
self._pmax_on = False
# will be restored if possible
self._target_temp = None
self._saved_target_temp = self._target_temp
self._humidity = None
self._ac_mode = False
self._fan_mode = None
self._swing_mode = None
self._cur_temp = None
_LOGGER.debug(
"%s - Creation of a new VersatileThermostat entity: unique_id=%s heater_entity_id=%s",
self,
self.unique_id,
heater_entity_id,
)
def __str__(self):
return f"VersatileThermostat-{self.name}"
@property
def unique_id(self):
return self._unique_id
@property
def should_poll(self):
return False
@property
def name(self):
return self._name
@property
def hvac_modes(self):
"""List of available operation modes."""
return self._hvac_list
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit
@property
def hvac_mode(self):
"""Return current operation."""
return self._hvac_mode
@property
def hvac_action(self):
"""Return the current running hvac operation if supported.
Need to be one of CURRENT_HVAC_*.
"""
if self._hvac_mode == HVAC_MODE_OFF:
return CURRENT_HVAC_OFF
if not self._is_device_active:
return CURRENT_HVAC_IDLE
if self._ac_mode:
return CURRENT_HVAC_COOL
return CURRENT_HVAC_HEAT
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
@property
def supported_features(self):
"""Return the list of supported features."""
return self._support_flags
@property
def _is_device_active(self):
"""If the toggleable device is currently active."""
if not self.hass.states.get(self._heater_entity_id):
return None
return self.hass.states.is_state(self._heater_entity_id, STATE_ON)
@property
def current_temperature(self):
"""Return the sensor temperature."""
return self._cur_temp
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
_LOGGER.info("%s - Set hvac mode: %s", self, hvac_mode)
if hvac_mode == HVAC_MODE_HEAT:
self._hvac_mode = HVAC_MODE_HEAT
# TODO await self._async_control_heating(force=True)
elif hvac_mode == HVAC_MODE_COOL:
self._hvac_mode = HVAC_MODE_COOL
# TODO await self._async_control_heating(force=True)
elif hvac_mode == HVAC_MODE_OFF:
self._hvac_mode = HVAC_MODE_OFF
# TODO self.prop_current_phase = PROP_PHASE_NONE
# if self._is_device_active:
# await self._async_heater_turn_off()
else:
_LOGGER.error("Unrecognized hvac mode: %s", hvac_mode)
return
# Ensure we update the current operation after changing the mode
self.async_write_ha_state()
async def async_set_preset_mode(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
# TODO await self._async_control_heating(force=True)
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(force=True)
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]
# TODO await self._async_control_heating(force=True)
self.async_write_ha_state()
async def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
_LOGGER.info("%s - Set fan mode: %s", self, fan_mode)
if fan_mode is None:
return
self._fan_mode = fan_mode
self.async_write_ha_state()
async def async_set_humidity(self, humidity: int):
"""Set new target humidity."""
_LOGGER.info("%s - Set fan mode: %s", self, humidity)
if humidity is None:
return
self._humidity = humidity
async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
_LOGGER.info("%s - Set fan mode: %s", self, swing_mode)
if swing_mode is None:
return
self._swing_mode = swing_mode
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.info("%s - Set target temp: %s", self, temperature)
if temperature is None:
return
self._target_temp = temperature
self._attr_preset_mode = PRESET_NONE
# TODO await self._async_control_heating(force=True)
self.async_write_ha_state()
@callback
async def entry_update_listener(
self, hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Called when the entry have changed in ConfigFlow"""
_LOGGER.info("%s - Change entry with the values: %s", self, config_entry.data)
async def async_added_to_hass(self):
"""Run when entity about to be added."""
_LOGGER.debug("Calling async_added_to_hass")
await super().async_added_to_hass()
# Add listener
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._heater_entity_id], self._async_switch_changed
)
)
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._temp_sensor_entity_id],
self._async_temperature_changed,
)
)
if self._window_sensor_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._window_sensor_entity_id],
self._async_windows_changed,
)
)
if self._motion_sensor_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._motion_sensor_entity_id],
self._async_motion_changed,
)
)
# if self._keep_alive:
# self.async_on_remove(
# async_track_time_interval(
# self.hass, self._async_control_heating, self._keep_alive
# )
# )
if self._power_sensor_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._power_sensor_entity_id],
self._async_power_changed,
)
)
if self._max_power_sensor_entity_id:
self.async_on_remove(
async_track_state_change_event(
self.hass,
[self._max_power_sensor_entity_id],
self._async_max_power_changed,
)
)
await self.async_startup()
async def async_startup(self):
"""Triggered on startup, used to get old state and set internal states accordingly"""
_LOGGER.debug("%s - Calling async_startup", self)
@callback
def _async_startup_internal(*_):
_LOGGER.debug("%s - Calling async_startup_internal", self)
need_write_state = False
temperature_state = self.hass.states.get(self._temp_sensor_entity_id)
if temperature_state and temperature_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
_LOGGER.debug(
"%s - temperature sensor have been retrieved: %f",
self,
float(temperature_state.state),
)
# TODO self._async_update_temp(temperature_state)
need_write_state = True
switch_state = self.hass.states.get(self._heater_entity_id)
if switch_state and switch_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self.hass.create_task(self._check_switch_initial_state())
if self._pmax_on:
# try to acquire current power and power max
current_power_state = self.hass.states.get(self._power_sensor_entity_id)
if current_power_state and current_power_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power = float(current_power_state.state)
_LOGGER.debug(
"%s - Current power have been retrieved: %f",
self,
self._current_power,
)
need_write_state = True
# Try to acquire power max
current_power_max_state = self.hass.states.get(
self._max_power_sensor_entity_id
)
if current_power_max_state and current_power_max_state.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
self._current_power_max = float(current_power_max_state.state)
_LOGGER.debug(
"%s - Current power max have been retrieved: %f",
self,
self._current_power_max,
)
need_write_state = True
if need_write_state:
self.async_write_ha_state()
# TODO self.hass.create_task(self._async_control_heating())
if self.hass.state == CoreState.running:
_async_startup_internal()
else:
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, _async_startup_internal
)
await self.get_my_previous_state()
async def get_my_previous_state(self):
"""Try to get my previou state"""
# Check If we have an old state
old_state = await self.async_get_last_state()
_LOGGER.debug(
"%s - Calling get_my_previous_state old_state is %s", self, old_state
)
if old_state is not None:
# If we have no initial temperature, restore
if self._target_temp is None:
# If we have a previously saved temperature
if old_state.attributes.get(ATTR_TEMPERATURE) is None:
if self._ac_mode:
self._target_temp = self.max_temp
else:
self._target_temp = self.min_temp
_LOGGER.warning(
"%s - Undefined target temperature, falling back to %s",
self,
self._target_temp,
)
else:
self._target_temp = float(old_state.attributes[ATTR_TEMPERATURE])
if old_state.attributes.get(ATTR_PRESET_MODE) in self._attr_preset_modes:
self._attr_preset_mode = old_state.attributes.get(ATTR_PRESET_MODE)
if not self._hvac_mode and old_state.state:
self._hvac_mode = old_state.state
else:
# No previous state, try and restore defaults
if self._target_temp is None:
if self._ac_mode:
self._target_temp = self.max_temp
else:
self._target_temp = self.min_temp
_LOGGER.warning(
"No previously saved temperature, setting to %s", 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",
self,
self._target_temp,
self._attr_preset_mode,
self._hvac_mode,
)
@callback
async def _async_temperature_changed(self, event):
"""Handle temperature changes."""
new_state = event.data.get("new_state")
_LOGGER.info(
"%s - Temperature changed. Event.new_state is %s",
self,
new_state,
)
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return
self._async_update_temp(new_state)
# TODO await self._async_control_heating()
self.async_write_ha_state()
@callback
async def _async_windows_changed(self, event):
"""Handle window changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
_LOGGER.info(
"%s - Window changed. Event.new_state is %s, _hvac_mode=%s, _saved_hvac_mode=%s",
self,
new_state,
self._hvac_mode,
self._saved_hvac_mode,
)
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
@callback
async def _async_motion_changed(self, event):
"""Handle motion changes."""
new_state = event.data.get("new_state")
_LOGGER.info(
"%s - Motion changed. Event.new_state is %s, _attr_preset_mode=%s, activity=%s",
self,
new_state,
self._attr_preset_mode,
PRESET_ACTIVITY,
)
if self._attr_preset_mode != PRESET_ACTIVITY:
return
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)
@callback
async def _check_switch_initial_state(self):
"""Prevent the device from keep running if HVAC_MODE_OFF."""
_LOGGER.debug("%s - Calling _check_switch_initial_state", self)
if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active:
_LOGGER.warning(
"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()
@callback
def _async_switch_changed(self, event):
"""Handle heater switch state changes."""
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if new_state is None:
return
if old_state is None:
self.hass.create_task(self._check_switch_initial_state())
self.async_write_ha_state()
@callback
def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor."""
try:
cur_temp = float(state.state)
if math.isnan(cur_temp) or math.isinf(cur_temp):
raise ValueError(f"Sensor has illegal state {state.state}")
self._cur_temp = cur_temp
except ValueError as ex:
_LOGGER.error("Unable to update temperature from sensor: %s", ex)
@callback
async def _async_power_changed(self, event):
"""Handle power changes."""
_LOGGER.debug("Thermostat %s - Receive new Power event", self.name)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if new_state is None or new_state.state == old_state.state:
return
try:
current_power = float(new_state.state)
if math.isnan(current_power) or math.isinf(current_power):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power = current_power
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)
@callback
async def _async_max_power_changed(self, event):
"""Handle power max changes."""
_LOGGER.debug("Thermostat %s - Receive new Power Max event", self.name)
_LOGGER.debug(event)
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if new_state is None or new_state.state == old_state.state:
return
try:
current_power_max = float(new_state.state)
if math.isnan(current_power_max) or math.isinf(current_power_max):
raise ValueError(f"Sensor has illegal state {new_state.state}")
self._current_power_max = current_power_max
except ValueError as ex:
_LOGGER.error("Unable to update current_power from sensor: %s", ex)

View File

@@ -0,0 +1,195 @@
"""Config flow for Versatile Thermostat integration."""
from __future__ import annotations
from homeassistant.core import callback
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow as HAConfigFlow,
OptionsFlow,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from .const import (
DOMAIN,
CONF_NAME,
CONF_HEATER,
CONF_TEMP_SENSOR,
CONF_POWER_SENSOR,
CONF_MAX_POWER_SENSOR,
CONF_WINDOW_SENSOR,
CONF_MOTION_SENSOR,
CONF_DEVICE_POWER,
ALL_CONF,
CONF_PRESETS,
)
# from .climate import VersatileThermostat
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HEATER): cv.string,
vol.Required(CONF_TEMP_SENSOR): cv.string,
vol.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(
{vol.Optional(v, default=17): vol.Coerce(float) for (k, v) in CONF_PRESETS.items()}
)
def schema_defaults(schema, **defaults):
"""Create a new schema with default values filled in."""
copy = schema.extend({})
for field, field_type in copy.schema.items():
if isinstance(field_type, vol.In):
value = None
# for dps in dps_list or []:
# if dps.startswith(f"{defaults.get(field)} "):
# value = dps
# break
if value in field_type.container:
field.default = vol.default_factory(value)
continue
if field.schema in defaults:
field.default = vol.default_factory(defaults[field])
return copy
# 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."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry):
"""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
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class VersatileThermostatOptionsFlowHandler(OptionsFlow):
"""Handle options flow for Versatile Thermostat integration."""
def __init__(self, config_entry: ConfigEntry):
"""Initialize options flow."""
self.config_entry = config_entry
_LOGGER.debug(
"CTOR VersatileThermostatOptionsFlowHandler config_entry.data: %s, entry_id: %s",
config_entry.data,
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",
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,
)

View File

@@ -0,0 +1,47 @@
"""Constants for the Versatile Thermostat integration."""
from homeassistant.const import CONF_NAME
from homeassistant.components.climate.const import (
PRESET_ACTIVITY,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
SUPPORT_TARGET_TEMPERATURE,
)
PRESET_POWER = "power"
DOMAIN = "versatile_thermostat"
CONF_HEATER = "heater_entity_id"
CONF_TEMP_SENSOR = "temperature_sensor_entity_id"
CONF_POWER_SENSOR = "power_sensor_entity_id"
CONF_MAX_POWER_SENSOR = "max_power_sensor_entity_id"
CONF_WINDOW_SENSOR = "window_sensor_entity_id"
CONF_MOTION_SENSOR = "motion_sensor_entity_id"
CONF_DEVICE_POWER = "device_power"
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_PRESETS = {
p: f"{p}_temp"
for p in (
PRESET_ECO,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_POWER,
)
}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE

View File

@@ -0,0 +1,17 @@
{
"version": "0.0.1",
"domain": "versatile_thermostat",
"name": "Versatile Thermostat",
"config_flow": true,
"documentation": "https://github.com/jmcollin78/versatile_thermostat",
"requirements": [],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@jmcollin78"
],
"iot_class": "local_polling",
"integration_type": "device"
}

View File

@@ -0,0 +1,28 @@
{
"title": "Versatile Thermostat configuration",
"config": {
"step": {
"user": {
"title": "Add new Versatile Thermostat",
"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%]",
}
}
},
"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%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@@ -0,0 +1,26 @@
{
"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)"
}
}
}
}
}