Skip to content

Commit

Permalink
Merge branch 'Alexwijn:develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeantd83 authored Jan 11, 2025
2 parents 37f76e3 + d851475 commit dee213e
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 20 deletions.
31 changes: 23 additions & 8 deletions custom_components/sat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ def __init__(self, coordinator: SatDataUpdateCoordinator, config_entry: ConfigEn
# Add features based on compatibility
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE

# Conditionally add TURN_ON if it exists
if hasattr(ClimateEntityFeature, 'TURN_ON'):
self._attr_supported_features |= ClimateEntityFeature.TURN_ON

# Conditionally add TURN_OFF if it exists
if hasattr(ClimateEntityFeature, 'TURN_OFF'):
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
Expand Down Expand Up @@ -462,7 +466,7 @@ def hvac_action(self):
if self._hvac_mode == HVACMode.OFF:
return HVACAction.OFF

if self._coordinator.device_state == DeviceState.OFF:
if not self._coordinator.device_active:
return HVACAction.IDLE

return HVACAction.HEATING
Expand Down Expand Up @@ -690,7 +694,7 @@ async def _async_window_sensor_changed(self, event: Event) -> None:

try:
self._window_sensor_handle = asyncio.create_task(asyncio.sleep(self._window_minimum_open_time))
self._pre_activity_temperature = self.target_temperature
self._pre_activity_temperature = self.target_temperature or self.min_temp

await self._window_sensor_handle
await self.async_set_preset_mode(PRESET_ACTIVITY)
Expand Down Expand Up @@ -725,15 +729,16 @@ async def _async_control_pid(self, reset: bool = False) -> None:
max_error = self.max_error

# Make sure we use the latest heating curve value
self.heating_curve.update(self.target_temperature, self.current_outside_temperature)
self._areas.heating_curves.update(self.current_outside_temperature)
if self.target_temperature is not None:
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
if not reset:
_LOGGER.info(f"Updating error value to {max_error} (Reset: False)")

# Calculate an optimal heating curve when we are in the deadband
if -DEADBAND <= max_error <= DEADBAND:
if self.target_temperature is not None and -DEADBAND <= max_error <= DEADBAND:
self.heating_curve.autotune(
setpoint=self.requested_setpoint,
target_temperature=self.target_temperature,
Expand All @@ -752,8 +757,13 @@ async def _async_control_pid(self, reset: bool = False) -> None:
_LOGGER.info("Reached deadband, turning off warming up.")
self._warming_up_data = None

self._areas.pids.update(self._coordinator.filtered_boiler_temperature)
self.pid.update(max_error, self.heating_curve.value, self._coordinator.filtered_boiler_temperature)
# Update our PID controllers if we have valid values
if self._coordinator.filtered_boiler_temperature is not None:
self._areas.pids.update(self._coordinator.filtered_boiler_temperature)

if self.heating_curve.value is not None:
self.pid.update(max_error, self.heating_curve.value, self._coordinator.filtered_boiler_temperature)

elif max_error != self.pid.last_error:
_LOGGER.info(f"Updating error value to {max_error} (Reset: True)")

Expand Down Expand Up @@ -894,7 +904,8 @@ async def async_control_heating_loop(self, _time=None) -> None:
await self._async_control_relative_modulation()

# Control the integral (if exceeded the time limit)
self.pid.update_integral(self.max_error, self.heating_curve.value)
if self.heating_curve.value is not None:
self.pid.update_integral(self.max_error, self.heating_curve.value)

# Control our areas
await self._areas.async_control_heating_loops()
Expand Down Expand Up @@ -969,6 +980,10 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:

# Set the hvac mode for those climate devices
for entity_id in climates:
state = self.hass.states.get(entity_id)
if state is None or hvac_mode not in state.attributes.get("hvac_modes"):
return

data = {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode}
await self.hass.services.async_call(CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, data, blocking=True)

Expand Down
22 changes: 16 additions & 6 deletions custom_components/sat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .coordinator import SatDataUpdateCoordinator
from .overshoot_protection import OvershootProtection
from .util import calculate_default_maximum_setpoint, snake_case
from .validators import valid_serial_device

DEFAULT_NAME = "Living Room"

Expand Down Expand Up @@ -112,9 +113,9 @@ async def async_step_mqtt(self, discovery_info: MqttServiceInfo):

async def async_step_mosquitto(self, _user_input: dict[str, Any] | None = None):
"""Entry step to select the MQTT mode and branch to specific setup."""
self.errors = {}

if _user_input is not None:
self.errors = {}
self.data.update(_user_input)

if self.data[CONF_MODE] == MODE_MQTT_OPENTHERM:
Expand Down Expand Up @@ -178,18 +179,27 @@ async def async_step_esphome(self, _user_input: dict[str, Any] | None = None):
)

async def async_step_serial(self, _user_input: dict[str, Any] | None = None):
self.errors = {}

if _user_input is not None:
self.errors = {}
self.data.update(_user_input)
self.data[CONF_MODE] = MODE_SERIAL

if not valid_serial_device(self.data[CONF_DEVICE]):
self.errors["base"] = "invalid_device"
return await self.async_step_serial()

gateway = OpenThermGateway()
if not await gateway.connect(port=self.data[CONF_DEVICE], skip_init=True, timeout=5):
await gateway.disconnect()

try:
connected = await asyncio.wait_for(gateway.connection.connect(port=self.data[CONF_DEVICE]), timeout=5)
except asyncio.TimeoutError:
connected = False

if not connected:
self.errors["base"] = "connection"
return await self.async_step_serial()

await gateway.disconnect()
return await self.async_step_sensors()

return self.async_show_form(
Expand Down Expand Up @@ -367,7 +377,7 @@ async def async_step_calibrate(self, _user_input: dict[str, Any] | None = None):
async def start_calibration():
try:
coordinator = await self.async_create_coordinator()
await coordinator.async_added_to_hass()
await coordinator.async_setup()

overshoot_protection = OvershootProtection(coordinator, self.data.get(CONF_HEATING_SYSTEM))
self.overshoot_protection_value = await overshoot_protection.calculate()
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 @@ -140,7 +140,7 @@ def return_temperature(self) -> float | None:
return None

@property
def filtered_boiler_temperature(self) -> float:
def filtered_boiler_temperature(self) -> float | None:
# Not able to use if we do not have at least two values
if len(self.boiler_temperatures) < 2:
return self.boiler_temperature
Expand Down
6 changes: 3 additions & 3 deletions custom_components/sat/heating_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ def base_offset(self) -> float:
return 20 if self._heating_system == HEATING_SYSTEM_UNDERFLOOR else 27.2

@property
def optimal_coefficient(self):
def optimal_coefficient(self) -> float | None:
return self._optimal_coefficient

@property
def coefficient_derivative(self):
def coefficient_derivative(self) -> float | None:
return self._coefficient_derivative

@property
def value(self):
def value(self) -> float | None:
return self._last_heating_curve_value
2 changes: 1 addition & 1 deletion custom_components/sat/mqtt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
STORAGE_VERSION = 1


class SatMqttCoordinator(ABC, SatDataUpdateCoordinator):
class SatMqttCoordinator(SatDataUpdateCoordinator, ABC):
"""Base class to manage fetching data using MQTT."""

def __init__(self, hass: HomeAssistant, device_id: str, data: Mapping[str, Any], options: Mapping[str, Any] | None = None) -> None:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/sat/serial/__init__.py
Original file line number Diff line number Diff line change
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=self._port, timeout=5)
await self._api.connect(port=int(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
1 change: 1 addition & 0 deletions custom_components/sat/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"reconfigure_successful": "Gateway has been re-configured."
},
"error": {
"invalid_device": "This is an invalid device.",
"connection": "Unable to connect to the gateway.",
"mqtt_component": "The MQTT component is unavailable.",
"unable_to_calibrate": "The calibration process has encountered an issue and could not be completed successfully. Please ensure that your heating system is functioning properly and that all required sensors are connected and working correctly.\n\nIf you continue to experience issues with calibration, consider contacting us for further assistance. We apologize for any inconvenience caused."
Expand Down
12 changes: 12 additions & 0 deletions custom_components/sat/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from urllib.parse import urlparse


def valid_serial_device(value: str):
if value.startswith("socket://"):
parsed_url = urlparse(value)
if parsed_url.hostname and parsed_url.port:
return True
elif value.startswith("/dev/"):
return True

return False

0 comments on commit dee213e

Please sign in to comment.