Files
ha_config/custom_components/better_thermostat/utils/controlling.py
T
2025-11-20 20:09:50 +01:00

389 lines
14 KiB
Python

import asyncio
import logging
from homeassistant.components.climate.const import HVACMode
from custom_components.better_thermostat.model_fixes.model_quirks import (
override_set_hvac_mode,
override_set_temperature,
)
from custom_components.better_thermostat.adapters.delegate import (
set_offset,
get_current_offset,
set_temperature,
set_hvac_mode,
)
from custom_components.better_thermostat.events.trv import (
convert_outbound_states,
update_hvac_action,
)
from custom_components.better_thermostat.utils.helpers import convert_to_float
from custom_components.better_thermostat.utils.const import CalibrationMode
_LOGGER = logging.getLogger(__name__)
class TaskManager:
def __init__(self):
self.tasks = set()
def create_task(self, coro):
task = asyncio.create_task(coro)
self.tasks.add(task)
task.add_done_callback(self.tasks.discard)
return task
async def control_queue(self):
"""The accutal control loop.
Parameters
----------
self :
instance of better_thermostat
Returns
-------
None
"""
if not hasattr(self, "task_manager"):
self.task_manager = TaskManager()
while True:
if self.ignore_states or self.startup_running:
await asyncio.sleep(1)
continue
else:
controls_to_process = await self.control_queue_task.get()
if controls_to_process is not None:
self.ignore_states = True
result = True
for trv in self.real_trvs.keys():
try:
_temp = await control_trv(self, trv)
if _temp is False:
result = False
except Exception:
_LOGGER.exception(
"better_thermostat %s: ERROR controlling: %s",
self.device_name,
trv,
)
result = False
# Retry task if some TRVs failed. Discard the task if the queue is full
# to avoid blocking and therefore deadlocking this function.
if result is False:
try:
self.control_queue_task.put_nowait(self)
except asyncio.QueueFull:
_LOGGER.debug(
"better_thermostat %s: control queue is full, discarding task"
)
self.control_queue_task.task_done()
self.ignore_states = False
async def control_trv(self, heater_entity_id=None):
"""This is the main controller for the real TRV
Parameters
----------
self :
instance of better_thermostat
Returns
-------
None
"""
if not hasattr(self, "task_manager"):
self.task_manager = TaskManager()
async with self._temp_lock:
self.real_trvs[heater_entity_id]["ignore_trv_states"] = True
await update_hvac_action(self)
await self.calculate_heating_power()
_trv = self.hass.states.get(heater_entity_id)
_current_set_temperature = convert_to_float(
str(_trv.attributes.get("temperature", None)),
self.device_name,
"controlling()",
)
_remapped_states = convert_outbound_states(
self, heater_entity_id, self.bt_hvac_mode
)
if not isinstance(_remapped_states, dict):
_LOGGER.debug(
f"better_thermostat {self.device_name}: ERROR {heater_entity_id} {_remapped_states}"
)
await asyncio.sleep(10)
self.ignore_states = False
self.real_trvs[heater_entity_id]["ignore_trv_states"] = False
return False
_temperature = _remapped_states.get("temperature", None)
_calibration = _remapped_states.get("local_temperature_calibration", None)
_calibration_mode = self.real_trvs[heater_entity_id]["advanced"].get(
"calibration_mode", CalibrationMode.DEFAULT
)
_new_hvac_mode = handle_window_open(self, _remapped_states)
# New cooler section
if self.cooler_entity_id is not None:
if (
self.cur_temp >= self.bt_target_cooltemp - self.tolerance
and _new_hvac_mode is not HVACMode.OFF
and self.cur_temp > self.bt_target_temp
):
await self.hass.services.async_call(
"climate",
"set_temperature",
{
"entity_id": self.cooler_entity_id,
"temperature": self.bt_target_cooltemp,
},
blocking=True,
context=self.context,
)
await self.hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.COOL},
blocking=True,
context=self.context,
)
elif (
self.cur_temp <= (self.bt_target_cooltemp - self.tolerance)
and _new_hvac_mode is not HVACMode.OFF
):
await self.hass.services.async_call(
"climate",
"set_temperature",
{
"entity_id": self.cooler_entity_id,
"temperature": self.bt_target_cooltemp,
},
blocking=True,
context=self.context,
)
await self.hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.OFF},
blocking=True,
context=self.context,
)
else:
await self.hass.services.async_call(
"climate",
"set_temperature",
{
"entity_id": self.cooler_entity_id,
"temperature": self.bt_target_cooltemp,
},
blocking=True,
context=self.context,
)
await self.hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": self.cooler_entity_id, "hvac_mode": HVACMode.OFF},
blocking=True,
context=self.context,
)
# if we don't need ot heat, we force HVACMode to be off
if self.call_for_heat is False:
_new_hvac_mode = HVACMode.OFF
# Manage TRVs with no HVACMode.OFF
_no_off_system_mode = (
HVACMode.OFF not in self.real_trvs[heater_entity_id]["hvac_modes"]
) or (
self.real_trvs[heater_entity_id]["advanced"].get(
"no_off_system_mode", False
)
is True
)
if _no_off_system_mode is True and _new_hvac_mode == HVACMode.OFF:
_min_temp = self.real_trvs[heater_entity_id]["min_temp"]
_LOGGER.debug(
f"better_thermostat {self.device_name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off"
)
_temperature = _min_temp
# send new HVAC mode to TRV, if it changed
if (
_new_hvac_mode is not None
and _new_hvac_mode != _trv.state
and (
(_no_off_system_mode is True and _new_hvac_mode != HVACMode.OFF)
or (_no_off_system_mode is False)
)
):
_LOGGER.debug(
f"better_thermostat {self.device_name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}"
)
self.real_trvs[heater_entity_id]["last_hvac_mode"] = _new_hvac_mode
_tvr_has_quirk = await override_set_hvac_mode(
self, heater_entity_id, _new_hvac_mode
)
if _tvr_has_quirk is False:
await set_hvac_mode(self, heater_entity_id, _new_hvac_mode)
if self.real_trvs[heater_entity_id]["system_mode_received"] is True:
self.real_trvs[heater_entity_id]["system_mode_received"] = False
self.task_manager.create_task(check_system_mode(self, heater_entity_id))
# set new calibration offset
if (
_calibration is not None
and _new_hvac_mode != HVACMode.OFF
and _calibration_mode != CalibrationMode.NO_CALIBRATION
):
_current_calibration_s = await get_current_offset(self, heater_entity_id)
if _current_calibration_s is None:
_LOGGER.error(
"better_thermostat %s: calibration fatal error %s",
self.device_name,
heater_entity_id,
)
# this should not be before, set_hvac_mode (because if it fails, the new hvac mode will never be sent)
self.ignore_states = False
self.real_trvs[heater_entity_id]["ignore_trv_states"] = False
return True
_current_calibration = convert_to_float(
str(_current_calibration_s), self.device_name, "controlling()"
)
_calibration = float(str(_calibration))
_old_calibration = self.real_trvs[heater_entity_id].get(
"last_calibration", _current_calibration
)
if self.real_trvs[heater_entity_id][
"calibration_received"
] is True and float(_old_calibration) != float(_calibration):
_LOGGER.debug(
f"better_thermostat {self.device_name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {_old_calibration} to: {_calibration}"
)
await set_offset(self, heater_entity_id, _calibration)
self.real_trvs[heater_entity_id]["calibration_received"] = False
# set new target temperature
if _temperature is not None and (
_new_hvac_mode != HVACMode.OFF or _no_off_system_mode
):
if _temperature != _current_set_temperature:
old = self.real_trvs[heater_entity_id].get("last_temperature", "?")
_LOGGER.debug(
f"better_thermostat {self.device_name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}"
)
self.real_trvs[heater_entity_id]["last_temperature"] = _temperature
_tvr_has_quirk = await override_set_temperature(
self, heater_entity_id, _temperature
)
if _tvr_has_quirk is False:
await set_temperature(self, heater_entity_id, _temperature)
if self.real_trvs[heater_entity_id]["target_temp_received"] is True:
self.real_trvs[heater_entity_id]["target_temp_received"] = False
self.task_manager.create_task(
check_target_temperature(self, heater_entity_id)
)
await asyncio.sleep(3)
self.real_trvs[heater_entity_id]["ignore_trv_states"] = False
return True
def handle_window_open(self, _remapped_states):
"""handle window open"""
_converted_hvac_mode = _remapped_states.get("system_mode", None)
_hvac_mode_send = _converted_hvac_mode
if self.window_open is True and self.last_window_state is False:
# if the window is open or the sensor is not available, we're done
self.last_main_hvac_mode = _hvac_mode_send
_hvac_mode_send = HVACMode.OFF
self.last_window_state = True
_LOGGER.debug(
f"better_thermostat {self.device_name}: control_trv: window is open or status of window is unknown, setting window open"
)
elif self.window_open is False and self.last_window_state is True:
_hvac_mode_send = self.last_main_hvac_mode
self.last_window_state = False
_LOGGER.debug(
f"better_thermostat {self.device_name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}"
)
# Force off on window open
if self.window_open is True:
_hvac_mode_send = HVACMode.OFF
return _hvac_mode_send
async def check_system_mode(self, heater_entity_id=None):
"""check system mode"""
_timeout = 0
_real_trv = self.real_trvs[heater_entity_id]
while _real_trv["hvac_mode"] != _real_trv["last_hvac_mode"]:
if _timeout > 360:
_LOGGER.debug(
f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the system mode change"
)
_timeout = 0
break
await asyncio.sleep(1)
_timeout += 1
await asyncio.sleep(2)
_real_trv["system_mode_received"] = True
return True
async def check_target_temperature(self, heater_entity_id=None):
"""Check if target temperature is reached."""
_timeout = 0
_real_trv = self.real_trvs[heater_entity_id]
while True:
_current_set_temperature = convert_to_float(
str(
self.hass.states.get(heater_entity_id).attributes.get(
"temperature", None
)
),
self.device_name,
"check_target_temperature()",
)
if _timeout == 0:
_LOGGER.debug(
f"better_thermostat {self.device_name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}"
)
if (
_current_set_temperature is None
or _real_trv["last_temperature"] == _current_set_temperature
):
_timeout = 0
break
if _timeout > 360:
_LOGGER.debug(
f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the target temperature change"
)
_timeout = 0
break
await asyncio.sleep(1)
_timeout += 1
await asyncio.sleep(2)
_real_trv["target_temp_received"] = True
return True