Skip to content

Commit

Permalink
More cleaning
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexwijn committed May 11, 2023
1 parent 26d6ef1 commit 41779e4
Show file tree
Hide file tree
Showing 18 changed files with 515 additions and 562 deletions.
44 changes: 18 additions & 26 deletions custom_components/sat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import asyncio
import logging
import sys

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant

from . import mqtt, serial, switch
from .config_store import SatConfigStore
from .const import *
from .coordinator import SatDataUpdateCoordinatorFactory

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand All @@ -23,32 +27,25 @@ async def async_setup(_hass: HomeAssistant, __config: Config):

async def async_setup_entry(_hass: HomeAssistant, _entry: ConfigEntry):
"""
Set up this integration using UI.
Set up this integration using the UI.
This function is called by Home Assistant when the integration is set up using the UI.
This function is called by Home Assistant when the integration is set up with the UI.
"""

# Create a new dictionary for this entry if it doesn't exist
if _hass.data.get(DOMAIN) is None:
_hass.data.setdefault(DOMAIN, {})
# Create a new dictionary
_hass.data[DOMAIN] = {_entry.entry_id: {}}

# Create a new config store for this entry and initialize it
_hass.data[DOMAIN][_entry.entry_id] = {CONFIG_STORE: SatConfigStore(_hass, _entry)}
_hass.data[DOMAIN][_entry.entry_id][CONFIG_STORE] = store = SatConfigStore(_hass, _entry)
await _hass.data[DOMAIN][_entry.entry_id][CONFIG_STORE].async_initialize()

# Retrieve the defaults and override it with the user options
options = OPTIONS_DEFAULTS.copy()
options.update(_entry.data)

# Get the module name from the config entry data and import it dynamically
module = getattr(sys.modules[__name__], options.get(CONF_MODE))

# Call the async_setup_entry function of the module
await _hass.async_add_job(module.async_setup_entry, _hass, _entry)
# Resolve the coordinator by using the factory according to the mode
_hass.data[DOMAIN][_entry.entry_id][COORDINATOR] = await SatDataUpdateCoordinatorFactory().resolve(
hass=_hass, store=store, mode=store.options.get(CONF_MODE), device=store.options.get(CONF_DEVICE)
)

# Forward entry setup for climate and other platforms
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setup(_entry, CLIMATE))
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setups(_entry, [SENSOR, NUMBER, BINARY_SENSOR]))
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setup(_entry, CLIMATE_DOMAIN))
await _hass.async_add_job(_hass.config_entries.async_forward_entry_setups(_entry, [SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN]))

# Add an update listener for this entry
_entry.async_on_unload(_entry.add_update_listener(async_reload_entry))
Expand All @@ -62,15 +59,11 @@ async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> bool:
This function is called by Home Assistant when the integration is being removed.
"""

# Retrieve the defaults and override it with the user options
options = OPTIONS_DEFAULTS.copy()
options.update(_entry.data)

# Unload the entry and its dependent components
unloaded = all(
await asyncio.gather(
_hass.config_entries.async_unload_platforms(_entry, [CLIMATE, SENSOR, NUMBER, BINARY_SENSOR]),
_hass.data[DOMAIN][_entry.entry_id][COORDINATOR].async_will_remove_from_hass(_hass.data[DOMAIN][_entry.entry_id][CLIMATE]),
_hass.config_entries.async_unload_platforms(_entry, [CLIMATE, SENSOR_DOMAIN, NUMBER_DOMAIN, BINARY_SENSOR_DOMAIN]),
)
)

Expand All @@ -87,7 +80,6 @@ async def async_reload_entry(_hass: HomeAssistant, _entry: ConfigEntry) -> None:
This function is called by Home Assistant when the integration configuration is updated.
"""

# Unload the entry and its dependent components
await async_unload_entry(_hass, _entry)

Expand Down
43 changes: 10 additions & 33 deletions custom_components/sat/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
from __future__ import annotations

import logging
import typing

from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass
from homeassistant.components.climate import HVACAction
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import CONF_MODE, MODE_SERIAL, OPTIONS_DEFAULTS, CONF_NAME, DOMAIN, COORDINATOR, CLIMATE
from .entity import SatEntity
from .const import CONF_MODE, MODE_SERIAL, CONF_NAME, DOMAIN, COORDINATOR, CLIMATE
from .entity import SatClimateEntity
from .serial import binary_sensor as serial_binary_sensor

if typing.TYPE_CHECKING:
from .climate import SatClimate

_LOGGER: logging.Logger = logging.getLogger(__name__)


Expand All @@ -26,26 +22,17 @@ async def async_setup_entry(_hass: HomeAssistant, _config_entry: ConfigEntry, _a
climate = _hass.data[DOMAIN][_config_entry.entry_id][CLIMATE]
coordinator = _hass.data[DOMAIN][_config_entry.entry_id][COORDINATOR]

# Retrieve the defaults and override it with the user options
options = OPTIONS_DEFAULTS.copy()
options.update(_config_entry.data)

# Check if integration is set to use the serial protocol
if options.get(CONF_MODE) == MODE_SERIAL:
# Call function to set up OpenTherm binary sensors
if coordinator.store.options.get(CONF_MODE) == MODE_SERIAL:
await serial_binary_sensor.async_setup_entry(_hass, _config_entry, _async_add_entities)

_async_add_entities([
SatControlSetpointSynchroSensor(coordinator, climate, _config_entry),
SatCentralHeatingSynchroSensor(coordinator, climate, _config_entry),
])
if coordinator.supports_setpoint_management:
_async_add_entities([SatControlSetpointSynchroSensor(coordinator, climate, _config_entry)])

_async_add_entities([SatCentralHeatingSynchroSensor(coordinator, climate, _config_entry)])

class SatControlSetpointSynchroSensor(SatEntity, BinarySensorEntity):

def __init__(self, coordinator, climate: SatClimate, config_entry: ConfigEntry):
super().__init__(coordinator, config_entry)
self._climate = climate
class SatControlSetpointSynchroSensor(SatClimateEntity, BinarySensorEntity):

@property
def name(self):
Expand All @@ -60,30 +47,20 @@ def device_class(self):
@property
def available(self):
"""Return availability of the sensor."""
return self._climate is not None and self._coordinator.setpoint is not None
return self._climate.setpoint is not None and self._coordinator.setpoint is not None

@property
def is_on(self):
"""Return the state of the sensor."""
coordinator_setpoint = self._coordinator.setpoint
climate_setpoint = float(self._climate.extra_state_attributes.get("setpoint") or coordinator_setpoint)

return not (
self._climate.state_attributes.get("hvac_action") != HVACAction.HEATING or
round(climate_setpoint, 1) == round(coordinator_setpoint, 1)
)
return round(self._climate.setpoint, 1) != round(self._coordinator.setpoint, 1)

@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self._config_entry.data.get(CONF_NAME).lower()}-control-setpoint-synchro"


class SatCentralHeatingSynchroSensor(SatEntity, BinarySensorEntity):
def __init__(self, coordinator, climate: SatClimate, config_entry: ConfigEntry) -> None:
"""Initialize the Central Heating Synchro sensor."""
super().__init__(coordinator, config_entry)
self._climate = climate
class SatCentralHeatingSynchroSensor(SatClimateEntity, BinarySensorEntity):

@property
def name(self) -> str:
Expand Down
19 changes: 13 additions & 6 deletions custom_components/sat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ async def async_added_to_hass(self) -> None:
# Restore previous state if available, or set default values
await self._restore_previous_state_or_set_defaults()

# Update heating curve if outside temperature is available
# Update a heating curve if outside temperature is available
if self.current_outside_temperature is not None:
self._heating_curve.update(self.target_temperature, self.current_outside_temperature)

Expand Down Expand Up @@ -426,6 +426,10 @@ def hvac_action(self):
def max_error(self) -> float:
return max([self.error] + self.climate_errors)

@property
def setpoint(self) -> float | None:
return self._setpoint

@property
def climate_errors(self) -> List[float]:
"""Calculate the temperature difference between the current temperature and target temperature for all connected climates."""
Expand Down Expand Up @@ -515,6 +519,9 @@ def pulse_width_modulation_enabled(self) -> bool:
return False

def _get_requested_setpoint(self):
if self._heating_curve.value is None:
return MINIMUM_SETPOINT

return max(self._heating_curve.value + self._pid.output, MINIMUM_SETPOINT)

def _calculate_control_setpoint(self) -> float:
Expand All @@ -525,7 +532,7 @@ def _calculate_control_setpoint(self) -> float:
# Combine the heating curve value and the calculated output from the pid controller
requested_setpoint = self._get_requested_setpoint()

# Make sure we are above the base setpoint when we are below target temperature
# Make sure we are above the base setpoint when we are below the target temperature
if self.max_error > 0:
requested_setpoint = max(requested_setpoint, self._heating_curve.value)

Expand Down Expand Up @@ -598,7 +605,7 @@ async def _async_climate_changed(self, event: Event) -> None:
elif new_attrs.get("temperature") != old_attrs.get("temperature"):
await self._async_control_pid(True)

# If current temperature has changed, update the PID controller
# If the current temperature has changed, update the PID controller
elif not hasattr(new_state.attributes, SENSOR_TEMPERATURE_ID) and new_attrs.get("current_temperature") != old_attrs.get("current_temperature"):
await self._async_control_pid(False)

Expand Down Expand Up @@ -658,7 +665,7 @@ async def _async_window_sensor_changed(self, event: Event) -> None:

async def _async_control_heating_loop(self, _time=None) -> None:
"""Control the heating based on current temperature, target temperature, and outside temperature."""
# If overshoot protection is active, we are not doing anything since we already have task running in async
# If overshoot protection is active, we are not doing anything since we already have a task running in async
if self.overshoot_protection_calculate:
return

Expand Down Expand Up @@ -717,15 +724,15 @@ async def _async_control_pid(self, reset: bool = False):
if not reset:
_LOGGER.info(f"Updating error value to {max_error} (Reset: False)")

# Calculate optimal heating curve when we are in the deadband
# Calculate an optimal heating curve when we are in the deadband
if -0.1 <= max_error <= 0.1:
self._heating_curve.autotune(
setpoint=self._get_requested_setpoint(),
target_temperature=self.target_temperature,
outside_temperature=self.current_outside_temperature
)

# Since we are in the deadband we can safely assume we are not warming up anymore
# Since we are in the deadband, we can safely assume we are not warming up anymore
if self._warming_up and max_error <= 0.1:
self._warming_up = False
_LOGGER.info("Reached deadband, turning off warming up.")
Expand Down
4 changes: 2 additions & 2 deletions custom_components/sat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ async def async_step_switch(self, _user_input=None):
self._data.update(_user_input)
self._data[CONF_MODE] = MODE_SWITCH

await self.async_set_unique_id(self._data[CONF_SWITCH], raise_on_progress=False)
await self.async_set_unique_id(self._data[CONF_DEVICE], raise_on_progress=False)

self._abort_if_unique_id_configured()

Expand All @@ -123,7 +123,7 @@ async def async_step_switch(self, _user_input=None):
errors=self._errors,
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(CONF_SWITCH): selector.EntitySelector(
vol.Required(CONF_DEVICE): selector.EntitySelector(
selector.EntitySelectorConfig(domain=[SWITCH_DOMAIN])
)
}),
Expand Down
14 changes: 1 addition & 13 deletions custom_components/sat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
NAME = "Smart Autotune Thermostat"
DOMAIN = "sat"
VERSION = "2.1.0"
CLIMATE = "climate"
COORDINATOR = "coordinator"
CONFIG_STORE = "config_store"

Expand All @@ -17,23 +18,10 @@
OVERSHOOT_PROTECTION_MAX_RELATIVE_MOD = 0
OVERSHOOT_PROTECTION_REQUIRED_DATASET = 40

# Icons
ICON = "mdi:format-quote-close"

# Device classes
BINARY_SENSOR_DEVICE_CLASS = "connectivity"

# Platforms
SENSOR = "sensor"
NUMBER = "number"
CLIMATE = "climate"
BINARY_SENSOR = "binary_sensor"

# Configuration and options
CONF_MODE = "mode"
CONF_NAME = "name"
CONF_DEVICE = "device"
CONF_SWITCH = "switch"
CONF_SETPOINT = "setpoint"
CONF_CLIMATES = "climates"
CONF_MQTT_TOPIC = "mqtt_topic"
Expand Down
24 changes: 23 additions & 1 deletion custom_components/sat/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ class DeviceState(Enum):
OFF = "off"


class SatDataUpdateCoordinatorFactory:
@staticmethod
async def resolve(hass: HomeAssistant, store: SatConfigStore, mode: str, device: str) -> DataUpdateCoordinator:
if mode == MODE_MQTT:
from .mqtt import SatMqttCoordinator
return SatMqttCoordinator(hass, store, device)

if mode == MODE_SERIAL:
from .serial import SatSerialCoordinator
return await SatSerialCoordinator(hass, store, device).async_connect()

if mode == MODE_SWITCH:
from .switch import SatSwitchCoordinator
return SatSwitchCoordinator(hass, store, device)

raise Exception(f'Invalid mode[{mode}]')


class SatDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, store: SatConfigStore) -> None:
"""Initialize."""
Expand Down Expand Up @@ -76,7 +94,7 @@ def device_active(self) -> bool:

@property
def flame_active(self) -> bool:
return True
return self.device_active

@property
def hot_water_active(self) -> bool:
Expand Down Expand Up @@ -183,6 +201,10 @@ async def async_added_to_hass(self, climate: SatClimate) -> None:
partial(set_overshoot_protection_value, self)
)

async def async_will_remove_from_hass(self, climate: SatClimate) -> None:
"""Run when entity will be removed from hass."""
pass

async def async_control_heating_loop(self, climate: SatClimate, _time=None) -> None:
"""Control the heating loop for the device."""
if climate.hvac_mode == HVACMode.OFF and self.device_active:
Expand Down
16 changes: 14 additions & 2 deletions custom_components/sat/entity.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"""SatEntity class"""
from __future__ import annotations

import logging
import typing

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, NAME, VERSION, CONF_NAME
from .coordinator import SatDataUpdateCoordinator

_LOGGER: logging.Logger = logging.getLogger(__name__)

if typing.TYPE_CHECKING:
from .climate import SatClimate
from .coordinator import SatDataUpdateCoordinator


class SatEntity(CoordinatorEntity):
def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEntry):
Expand All @@ -25,3 +30,10 @@ def device_info(self):
"manufacturer": NAME,
"identifiers": {(DOMAIN, self._config_entry.data.get(CONF_NAME))},
}


class SatClimateEntity(SatEntity):
def __init__(self, coordinator, climate: SatClimate, config_entry: ConfigEntry):
super().__init__(coordinator, config_entry)

self._climate = climate
Loading

0 comments on commit 41779e4

Please sign in to comment.