Skip to content

Commit

Permalink
Merge pull request #86 from Alexwijn/develop
Browse files Browse the repository at this point in the history
More bugfixes
  • Loading branch information
Alexwijn authored Jan 19, 2025
2 parents 85ac04a + 1f528d3 commit deb70c1
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 43 deletions.
14 changes: 8 additions & 6 deletions custom_components/sat/area.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant, State

from .const import CONF_ROOMS
from .heating_curve import HeatingCurve
from .helpers import float_value
from .pid import PID
Expand Down Expand Up @@ -86,12 +87,10 @@ async def async_control_heating_loop(self, _time=None) -> None:


class Areas:
def __init__(self, config_data: MappingProxyType[str, Any], config_options: MappingProxyType[str, Any], entity_ids: list[str]):
def __init__(self, config_data: MappingProxyType[str, Any], config_options: MappingProxyType[str, Any]):
"""Initialize Areas with multiple Area instances using shared config data and options."""
self._entity_ids: list[str] = entity_ids
self._config_data: MappingProxyType[str, Any] = config_data
self._config_options: MappingProxyType[str, Any] = config_options
self._areas: list[Area] = [Area(config_data, config_options, entity_id) for entity_id in entity_ids]
self._entity_ids: list[str] = config_data.get(CONF_ROOMS) or []
self._areas: list[Area] = [Area(config_data, config_options, entity_id) for entity_id in self._entity_ids]

@property
def errors(self) -> List[float]:
Expand All @@ -108,6 +107,9 @@ def pids(self):
"""Return an interface to reset PID controllers for all areas."""
return Areas._PIDs(self._areas)

def items(self) -> list[str]:
return self._entity_ids

async def async_added_to_hass(self, hass: HomeAssistant):
for area in self._areas:
await area.async_added_to_hass(hass)
Expand Down Expand Up @@ -141,4 +143,4 @@ def update(self, boiler_temperature: float) -> None:
def reset(self) -> None:
"""Reset PID controllers for all areas."""
for area in self.areas:
area.pid.reset()
area.pid.reset()
49 changes: 25 additions & 24 deletions custom_components/sat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class SatClimate(SatEntity, ClimateEntity, RestoreEntity):
def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEntry, unit: str):
super().__init__(coordinator, config_entry)

# Setup some public variables
self.thermostat = config_entry.data.get(CONF_THERMOSTAT)

# Get some sensor entity IDs
self.inside_sensor_entity_id = config_entry.data.get(CONF_INSIDE_SENSOR_ENTITY_ID)
self.humidity_sensor_entity_id = config_entry.data.get(CONF_HUMIDITY_SENSOR_ENTITY_ID)
Expand Down Expand Up @@ -154,9 +157,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn
self._attr_name = str(config_entry.data.get(CONF_NAME))
self._attr_id = str(config_entry.data.get(CONF_NAME)).lower()

self.thermostat = config_entry.data.get(CONF_THERMOSTAT)
self._climates = config_entry.data.get(CONF_SECONDARY_CLIMATES) or []
self._main_climates = config_entry.data.get(CONF_MAIN_CLIMATES) or []
self._radiators = config_entry.data.get(CONF_RADIATORS) or []
self._window_sensors = config_entry.options.get(CONF_WINDOW_SENSORS) or []

self._simulation = bool(config_entry.data.get(CONF_SIMULATION))
Expand Down Expand Up @@ -185,7 +186,7 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn
self.pid = create_pid_controller(config_options)

# Create Area controllers
self._areas = Areas(config_entry.data, config_options, self._climates)
self.areas = Areas(config_entry.data, config_options)

# Create Relative Modulation controller
self._relative_modulation = RelativeModulation(coordinator, self._heating_system)
Expand Down Expand Up @@ -220,7 +221,7 @@ async def async_added_to_hass(self) -> None:

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

if self.hass.state is not CoreState.running:
Expand All @@ -234,7 +235,7 @@ async def async_added_to_hass(self) -> None:
await self._register_services()

# Initialize the area system
await self._areas.async_added_to_hass(self.hass)
await self.areas.async_added_to_hass(self.hass)

# Let the coordinator know we are ready
await self._coordinator.async_added_to_hass()
Expand Down Expand Up @@ -279,13 +280,13 @@ async def _register_event_listeners(self, _time: Optional[datetime] = None):

self.async_on_remove(
async_track_state_change_event(
self.hass, self._main_climates, self._async_main_climate_changed
self.hass, self._radiators, self._async_main_climate_changed
)
)

self.async_on_remove(
async_track_state_change_event(
self.hass, self._climates, self._async_climate_changed
self.hass, self.areas.items(), self._async_climate_changed
)
)

Expand All @@ -300,8 +301,8 @@ async def _register_event_listeners(self, _time: Optional[datetime] = None):
)
)

for climate_id in self._climates:
state = self.hass.states.get(climate_id)
for entity_id in self.areas.items():
state = self.hass.states.get(entity_id)
if state is not None and (sensor_temperature_id := state.attributes.get(SENSOR_TEMPERATURE_ID)):
await self.async_track_sensor_temperature(sensor_temperature_id)

Expand Down Expand Up @@ -364,7 +365,7 @@ async def _register_services(self):
async def reset_integral(_call: ServiceCall):
"""Service to reset the integral part of the PID controller."""
self.pid.reset()
self._areas.pids.reset()
self.areas.pids.reset()

self.hass.services.async_register(DOMAIN, SERVICE_RESET_INTEGRAL, reset_integral)

Expand Down Expand Up @@ -500,7 +501,7 @@ def max_error(self) -> float:
if self._heating_mode == HEATING_MODE_ECO:
return self.error

return max([self.error] + self._areas.errors)
return max([self.error] + self.areas.errors)

@property
def setpoint(self) -> float | None:
Expand All @@ -516,9 +517,9 @@ def requested_setpoint(self) -> float:

@property
def valves_open(self) -> bool:
"""Determine if any of the controlled thermostats have open valves."""
# Get the list of all controlled thermostats
climates = self._climates + self._main_climates
"""Determine if any of the controlled climates have open valves."""
# Get the list of all controlled climates
climates = self._radiators + self.areas.items()

# If there are no thermostats, we can safely assume the valves are open
if len(climates) == 0:
Expand Down Expand Up @@ -768,14 +769,14 @@ async def _async_control_pid(self, reset: bool = False) -> None:
# Reset the PID controller if the sensor data is too old
if self._sensor_max_value_age != 0 and monotonic() - self.pid.last_updated > self._sensor_max_value_age:
self.pid.reset()
self._areas.pids.reset()
self.areas.pids.reset()

# Calculate the maximum error between the current temperature and the target temperature of all climates
max_error = self.max_error

# Make sure we use the latest heating curve value
if self.target_temperature is not None:
self._areas.heating_curves.update(self.current_outside_temperature)
self.areas.heating_curves.update(self.current_outside_temperature)
self.heating_curve.update(self.target_temperature, self.current_outside_temperature)

# Update the PID controller with the maximum error
Expand All @@ -792,7 +793,7 @@ async def _async_control_pid(self, reset: bool = False) -> None:

# Update our PID controllers if we have valid values
if self._coordinator.boiler_temperature_filtered is not None:
self._areas.pids.update(self._coordinator.boiler_temperature_filtered)
self.areas.pids.update(self._coordinator.boiler_temperature_filtered)

if self.heating_curve.value is not None:
self.pid.update(max_error, self.heating_curve.value, self._coordinator.boiler_temperature_filtered)
Expand Down Expand Up @@ -873,7 +874,7 @@ async def _async_update_rooms_from_climates(self) -> None:
self._rooms = {}

# Iterate through each climate entity
for entity_id in self._climates:
for entity_id in self.areas.items():
state = self.hass.states.get(entity_id)

# Skip any entities that are unavailable or have an unknown state
Expand Down Expand Up @@ -970,7 +971,7 @@ async def async_control_heating_loop(self, _time: Optional[datetime] = None) ->
self.pid.update_integral(self.max_error, self.heating_curve.value)

# Control our areas
await self._areas.async_control_heating_loops()
await self.areas.async_control_heating_loops()

# Control our dynamic minimum setpoint (version 1)
if not self._coordinator.hot_water_active and self._coordinator.flame_active:
Expand Down Expand Up @@ -1039,9 +1040,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode, cascade: bool = True) -
await self.reset_control_state()

# Collect which climates to control
climates = self._main_climates[:]
climates = self._radiators[:]
if self._sync_climates_with_mode:
climates += self._climates
climates += self.areas.items()

if cascade:
# Set the hvac mode for those climate devices
Expand Down Expand Up @@ -1082,7 +1083,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:

# Set the temperature for each room, when enabled
if self._sync_climates_with_preset:
for entity_id in self._climates:
for entity_id in self.areas.items():
state = self.hass.states.get(entity_id)
if state is None or state.state == HVACMode.OFF:
continue
Expand All @@ -1104,7 +1105,7 @@ async def async_set_target_temperature(self, temperature: float, cascade: bool =

if cascade:
# Set the target temperature for each main climate
for entity_id in self._main_climates:
for entity_id in self._radiators:
data = {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: temperature}
await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, data, blocking=True)

Expand Down
8 changes: 4 additions & 4 deletions custom_components/sat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,10 @@ async def async_step_areas(self, _user_input: dict[str, Any] | None = None):
vol.Optional(CONF_THERMOSTAT): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN)
),
vol.Optional(CONF_MAIN_CLIMATES): selector.EntitySelector(
vol.Optional(CONF_RADIATORS): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True)
),
vol.Optional(CONF_SECONDARY_CLIMATES): selector.EntitySelector(
vol.Optional(CONF_ROOMS): selector.EntitySelector(
selector.EntitySelectorConfig(domain=CLIMATE_DOMAIN, multiple=True)
),
}), self.data)
Expand Down Expand Up @@ -400,7 +400,7 @@ async def start_calibration():
await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True)

# Make sure all climate valves are open
for entity_id in self.data.get(CONF_MAIN_CLIMATES, []) + self.data.get(CONF_SECONDARY_CLIMATES, []):
for entity_id in self.data.get(CONF_RADIATORS, []) + self.data.get(CONF_ROOMS, []):
data = {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVACMode.HEAT}
await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True)

Expand Down Expand Up @@ -591,7 +591,7 @@ async def async_step_general(self, _user_input: dict[str, Any] | None = None):
])
)

if len(self._config_entry.data.get(CONF_SECONDARY_CLIMATES, [])) > 0:
if len(self._config_entry.data.get(CONF_ROOMS, [])) > 0:
schema[vol.Required(CONF_HEATING_MODE, default=str(options[CONF_HEATING_MODE]))] = selector.SelectSelector(
selector.SelectSelectorConfig(mode=SelectSelectorMode.DROPDOWN, options=[
selector.SelectOptionDict(value=HEATING_MODE_COMFORT, label="Comfort"),
Expand Down
8 changes: 4 additions & 4 deletions custom_components/sat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
CONF_MINIMUM_SETPOINT = "minimum_setpoint"
CONF_MAXIMUM_SETPOINT = "maximum_setpoint"
CONF_MAXIMUM_RELATIVE_MODULATION = "maximum_relative_modulation"
CONF_SECONDARY_CLIMATES = "secondary_climates"
CONF_ROOMS = "secondary_climates"
CONF_MQTT_TOPIC = "mqtt_topic"
CONF_MAIN_CLIMATES = "main_climates"
CONF_RADIATORS = "main_climates"
CONF_WINDOW_SENSORS = "window_sensors"
CONF_PUSH_SETPOINT_TO_THERMOSTAT = "push_setpoint_to_thermostat"
CONF_WINDOW_MINIMUM_OPEN_TIME = "window_minimum_open_time"
Expand Down Expand Up @@ -109,8 +109,8 @@
CONF_DYNAMIC_MINIMUM_SETPOINT: False,
CONF_MINIMUM_SETPOINT_ADJUSTMENT_FACTOR: 0.2,

CONF_MAIN_CLIMATES: [],
CONF_SECONDARY_CLIMATES: [],
CONF_RADIATORS: [],
CONF_ROOMS: [],

CONF_SIMULATION: False,
CONF_WINDOW_SENSORS: [],
Expand Down
2 changes: 1 addition & 1 deletion custom_components/sat/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def device_status(self):

if self.device_active:
if self.boiler_temperature_cold is not None and self.boiler_temperature_cold > self.boiler_temperature:
if self.boiler_temperature_derivative < 0:
if self.boiler_temperature_derivative is not None and self.boiler_temperature_derivative < 0:
return DeviceStatus.PUMP_STARTING

if self._boiler_temperature_tracker.active and self.setpoint > self.boiler_temperature:
Expand Down
3 changes: 3 additions & 0 deletions custom_components/sat/manufacturer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
"Ideal": {"module": "ideal", "class": "Ideal", "id": 6},
"Ferroli": {"module": "ferroli", "class": "Ferroli", "id": 9},
"DeDietrich": {"module": "dedietrich", "class": "DeDietrich", "id": 11},
"Vaillant": {"module": "vaillant", "class": "Vaillant", "id": 24},
"Immergas": {"module": "immergas", "class": "Immergas", "id": 27},
"Sime": {"module": "sime", "class": "Sime", "id": 27},
"Viessmann": {"module": "viessmann", "class": "Viessmann", "id": 33},
"Radiant": {"module": "radiant", "class": "Radiant", "id": 41},
"Nefit": {"module": "nefit", "class": "Nefit", "id": 131},
"Intergas": {"module": "intergas", "class": "Intergas", "id": 173},
}
Expand Down
7 changes: 7 additions & 0 deletions custom_components/sat/manufacturers/radiant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ..manufacturer import Manufacturer


class Radiant(Manufacturer):
@property
def name(self) -> str:
return 'Radiant'
7 changes: 7 additions & 0 deletions custom_components/sat/manufacturers/vaillant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ..manufacturer import Manufacturer


class Vaillant(Manufacturer):
@property
def name(self) -> str:
return 'Vaillant'
7 changes: 7 additions & 0 deletions custom_components/sat/manufacturers/viessmann.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ..manufacturer import Manufacturer


class Viessmann(Manufacturer):
@property
def name(self) -> str:
return 'Viessmann'
8 changes: 4 additions & 4 deletions custom_components/sat/serial/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ def __init__(self, hass: HomeAssistant, port: str, data: Mapping[str, Any], opti
"""Initialize."""
super().__init__(hass, data, options)

self.data = DEFAULT_STATUS
self.data: dict = DEFAULT_STATUS

async def async_coroutine(event):
self.async_set_updated_data(event)

self._port = port
self._api = OpenThermGateway()
self._port: str = port
self._api: OpenThermGateway = OpenThermGateway()
self._api.subscribe(async_coroutine)

@property
Expand Down Expand Up @@ -161,7 +161,7 @@ def get(self, key: str) -> Optional[Any]:

async def async_connect(self) -> SatSerialCoordinator:
try:
await self._api.connect(port=int(self._port), timeout=5)
await self._api.connect(port=self._port, timeout=5)
except (asyncio.TimeoutError, ConnectionError, SerialException) as exception:
raise ConfigEntryNotReady(f"Could not connect to gateway at {self._port}: {exception}") from exception

Expand Down

0 comments on commit deb70c1

Please sign in to comment.