Skip to content

Commit

Permalink
Merge pull request #1059 from KartoffelToby/feature/cooling
Browse files Browse the repository at this point in the history
Add the support for cooling devices too (AC)
  • Loading branch information
KartoffelToby committed Oct 31, 2023
2 parents 71beaa6 + 1797332 commit f5eabfd
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 35 deletions.
16 changes: 16 additions & 0 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@ climate:
heater: input_boolean.heater2
target_sensor: input_number.internal_sensor2

- platform: generic_thermostat
name: Dummy_real_AC
heater: input_boolean.cooler
target_sensor: input_number.internal_sensor3
ac_mode: true
cold_tolerance: 0.3

input_boolean:
heater:
name: Heater
initial: on
heater2:
name: Heater2
initial: on
cooler:
name: Cooler
initial: on
window_open:
name: Window open
initial: off
Expand All @@ -50,6 +60,12 @@ input_number:
max: 35
step: 0.1
initial: 20
internal_sensor3:
name: Internal Sensor2
min: 5
max: 35
step: 0.1
initial: 20
external_sensor:
name: External Sensor
initial: 18.2
Expand Down
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8"
},
"python.formatting.provider": "none"
}
148 changes: 133 additions & 15 deletions custom_components/better_thermostat/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from random import randint
from statistics import mean

from custom_components.better_thermostat.events.cooler import trigger_cooler_change

from .utils.watcher import check_all_entities

from .utils.weather import check_ambient_air_temperature, check_weather
Expand All @@ -20,17 +22,23 @@

from .utils.model_quirks import load_model_quirks

from .utils.helpers import convert_to_float, find_battery_entity
from .utils.helpers import convert_to_float, find_battery_entity, get_hvac_bt_mode
from homeassistant.helpers import entity_platform
from homeassistant.core import callback, CoreState, Context, ServiceCall
import json
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate import (
ClimateEntity,
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
)
from homeassistant.components.climate.const import (
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_TARGET_TEMP_STEP,
HVACMode,
HVACAction,
ClimateEntityFeature,
)
from homeassistant.const import (
CONF_NAME,
Expand Down Expand Up @@ -58,6 +66,7 @@
ATTR_STATE_WINDOW_OPEN,
ATTR_STATE_SAVED_TEMPERATURE,
ATTR_STATE_HEATING_POWER,
CONF_COOLER,
CONF_HEATER,
CONF_HUMIDITY,
CONF_MODEL,
Expand Down Expand Up @@ -135,6 +144,7 @@ async def async_service_handler(self, data: ServiceCall):
entry.data.get(CONF_OFF_TEMPERATURE, None),
entry.data.get(CONF_TOLERANCE, 0.0),
entry.data.get(CONF_MODEL, None),
entry.data.get(CONF_COOLER, None),
hass.config.units.temperature_unit,
entry.entry_id,
device_class="better_thermostat",
Expand Down Expand Up @@ -203,6 +213,7 @@ def __init__(
off_temperature,
tolerance,
model,
cooler_entity_id,
unit,
unique_id,
device_class,
Expand All @@ -221,6 +232,7 @@ def __init__(
self.all_trvs = heater_entity_id
self.sensor_entity_id = sensor_entity_id
self.humidity_entity_id = humidity_sensor_entity_id
self.cooler_entity_id = cooler_entity_id
self.window_id = window_id or None
self.window_delay = window_delay or 0
self.window_delay_after = window_delay_after or 0
Expand All @@ -233,6 +245,7 @@ def __init__(
self._device_class = device_class
self._state_class = state_class
self._hvac_list = [HVACMode.HEAT, HVACMode.OFF]
self.map_on_hvac_mode = HVACMode.HEAT
self.next_valve_maintenance = datetime.now() + timedelta(
hours=randint(1, 24 * 5)
)
Expand All @@ -243,6 +256,7 @@ def __init__(
self.bt_min_temp = 0
self.bt_max_temp = 30
self.bt_target_temp = 5
self.bt_target_cooltemp = None
self._support_flags = SUPPORT_FLAGS
self.bt_hvac_mode = None
self.closed_window_triggered = False
Expand Down Expand Up @@ -295,6 +309,11 @@ async def async_added_to_hass(self):
"You updated from version before 1.0.0-Beta36 of the Better Thermostat integration, you need to remove the BT devices (integration) and add it again."
)

if self.cooler_entity_id is not None:
self._hvac_list.remove(HVACMode.HEAT)
self._hvac_list.append(HVACMode.HEAT_COOL)
self.map_on_hvac_mode = HVACMode.HEAT_COOL

self.entity_ids = [
entity for trv in self.all_trvs if (entity := trv["trv"]) is not None
]
Expand Down Expand Up @@ -426,6 +445,16 @@ async def _trigger_window_change(self, event):

self.hass.async_create_task(trigger_window_change(self, event))

async def _tigger_cooler_change(self, event):
_check = await check_all_entities(self)
if _check is False:
return
self.async_set_context(event.context)
if (event.data.get("new_state")) is None:
return

self.hass.async_create_task(trigger_cooler_change(self, event))

async def startup(self):
"""Run when entity about to be added.
Expand All @@ -439,6 +468,7 @@ async def startup(self):
self.name,
self.version,
)

sensor_state = self.hass.states.get(self.sensor_entity_id)
if sensor_state is not None:
if sensor_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
Expand Down Expand Up @@ -487,6 +517,20 @@ async def startup(self):
await asyncio.sleep(10)
continue

if self.cooler_entity_id is not None:
if self.hass.states.get(self.cooler_entity_id).state in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
None,
):
_LOGGER.info(
"better_thermostat %s: waiting for cooler entity with id '%s' to become fully available...",
self.name,
self.cooler_entity_id,
)
await asyncio.sleep(10)
continue

if self.humidity_entity_id is not None:
if self.hass.states.get(self.humidity_entity_id).state in (
STATE_UNAVAILABLE,
Expand Down Expand Up @@ -553,6 +597,18 @@ async def startup(self):
self.name,
"startup()",
)

if self.cooler_entity_id is not None:
self.bt_target_cooltemp = convert_to_float(
str(
self.hass.states.get(self.cooler_entity_id).attributes.get(
"temperature"
)
),
self.name,
"startup()",
)

if self.window_id is not None:
self.all_entities.append(self.window_id)
window = self.hass.states.get(self.window_id)
Expand Down Expand Up @@ -702,7 +758,11 @@ async def startup(self):
self.cur_humidity = 0

self.last_window_state = self.window_open
if self.bt_hvac_mode not in (HVACMode.OFF, HVACMode.HEAT):
if self.bt_hvac_mode not in (
HVACMode.OFF,
HVACMode.HEAT_COOL,
HVACMode.HEAT,
):
self.bt_hvac_mode = HVACMode.HEAT

self.async_write_ha_state()
Expand Down Expand Up @@ -830,6 +890,12 @@ async def startup(self):
self.hass, [self.window_id], self._trigger_window_change
)
)
if self.cooler_entity_id is not None:
self.async_on_remove(
async_track_state_change_event(
self.hass, [self.cooler_entity_id], self._tigger_cooler_change
)
)
_LOGGER.info("better_thermostat %s: startup completed.", self.name)
self.async_write_ha_state()
await self.async_update_ha_state(force_refresh=True)
Expand Down Expand Up @@ -1024,7 +1090,7 @@ def hvac_mode(self):
string
HVAC mode only from homeassistant.components.climate.const is valid
"""
return self.bt_hvac_mode
return get_hvac_bt_mode(self, self.bt_hvac_mode)

@property
def hvac_action(self):
Expand All @@ -1041,7 +1107,7 @@ def hvac_action(self):
return self.attr_hvac_action

@property
def target_temperature(self):
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.
Returns
Expand All @@ -1059,6 +1125,18 @@ def target_temperature(self):
return self.bt_max_temp
return self.bt_target_temp

@property
def target_temperature_low(self) -> float | None:
if self.cooler_entity_id is None:
return None
return self.bt_target_temp

@property
def target_temperature_high(self) -> float | None:
if self.cooler_entity_id is None:
return None
return self.bt_target_cooltemp

@property
def hvac_modes(self):
"""List of available operation modes.
Expand All @@ -1077,8 +1155,8 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
-------
None
"""
if hvac_mode in (HVACMode.HEAT, HVACMode.OFF):
self.bt_hvac_mode = hvac_mode
if hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL, HVACMode.OFF):
self.bt_hvac_mode = get_hvac_bt_mode(self, hvac_mode)
else:
_LOGGER.error(
"better_thermostat %s: Unsupported hvac_mode %s", self.name, hvac_mode
Expand All @@ -1098,17 +1176,55 @@ async def async_set_temperature(self, **kwargs) -> None:
-------
None
"""
_new_setpoint = convert_to_float(
str(kwargs.get(ATTR_TEMPERATURE, None)),
self.name,
"controlling.settarget_temperature()",
)
if _new_setpoint is None:
_new_setpoint = None
_new_setpointlow = None
_new_setpointhigh = None

if ATTR_HVAC_MODE in kwargs:
hvac_mode = str(kwargs.get(ATTR_HVAC_MODE, None))
if hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL, HVACMode.OFF):
self.bt_hvac_mode = hvac_mode
else:
_LOGGER.error(
"better_thermostat %s: Unsupported hvac_mode %s",
self.name,
hvac_mode,
)
if ATTR_TEMPERATURE in kwargs:
_new_setpoint = convert_to_float(
str(kwargs.get(ATTR_TEMPERATURE, None)),
self.name,
"controlling.settarget_temperature()",
)
if ATTR_TARGET_TEMP_LOW in kwargs:
_new_setpointlow = convert_to_float(
str(kwargs.get(ATTR_TARGET_TEMP_LOW, None)),
self.name,
"controlling.settarget_temperature_low()",
)
if ATTR_TARGET_TEMP_HIGH in kwargs:
_new_setpointhigh = convert_to_float(
str(kwargs.get(ATTR_TARGET_TEMP_HIGH, None)),
self.name,
"controlling.settarget_temperature_high()",
)

if _new_setpoint is None and _new_setpointlow is None:
_LOGGER.debug(
f"better_thermostat {self.name}: received a new setpoint from HA, but temperature attribute was not set, ignoring"
)
return
self.bt_target_temp = _new_setpoint
self.bt_target_temp = _new_setpoint or _new_setpointlow
if _new_setpointhigh is not None:
self.bt_target_cooltemp = _new_setpointhigh

_LOGGER.debug(
"better_thermostat %s: HA set target temperature to %s & %s",
self.name,
self.bt_target_temp,
self.bt_target_cooltemp,
)

self.async_write_ha_state()
await self.control_queue_task.put(self)

Expand Down Expand Up @@ -1166,4 +1282,6 @@ def supported_features(self):
array
Supported features.
"""
return self._support_flags
if self.cooler_entity_id is not None:
return ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
return ClimateEntityFeature.TARGET_TEMPERATURE
Loading

0 comments on commit f5eabfd

Please sign in to comment.