Ecobee aux cutover threshold (#129474)

* removing extra blank space

* Adding EcobeeAuxCutoverThreshold

First pass.

* minor reorg and changes; testing local check-in

* Adding entity, setting device class and name

* Bumping max value slightly to hopefully accomodate celsius, setting numberMode=box

* fixing the entity name for aux cutover threshold

* Combined async_add_entities

* Using a list comprehension

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fixing stuff with listcomprehension

* exchanging call to list.append() to extend with list comprehension

* Updating the class name and the entity name to match the device UI.
Removing abbreviations from entity names

* Fixing tests to match new entity names

* respecting 88 column limit

* Formatting

* Adding test coverage for update/set compressorMinTemp values

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/130287/head
Nicholas Romyn 2024-11-10 08:13:01 -05:00 committed by GitHub
parent 433321136d
commit a1a08f7755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 18 deletions

View File

@ -6,9 +6,14 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import logging
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfTime
from homeassistant.const import UnitOfTemperature, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -54,21 +59,30 @@ async def async_setup_entry(
) -> None:
"""Set up the ecobee thermostat number entity."""
data: EcobeeData = hass.data[DOMAIN]
_LOGGER.debug("Adding min time ventilators numbers (if present)")
async_add_entities(
assert data is not None
entities: list[NumberEntity] = [
EcobeeVentilatorMinTime(data, index, numbers)
for index, thermostat in enumerate(data.ecobee.thermostats)
if thermostat["settings"]["ventilatorType"] != "none"
for numbers in VENTILATOR_NUMBERS
]
_LOGGER.debug("Adding compressor min temp number (if present)")
entities.extend(
(
EcobeeVentilatorMinTime(data, index, numbers)
EcobeeCompressorMinTemp(data, index)
for index, thermostat in enumerate(data.ecobee.thermostats)
if thermostat["settings"]["ventilatorType"] != "none"
for numbers in VENTILATOR_NUMBERS
),
True,
if thermostat["settings"]["hasHeatPump"]
)
)
async_add_entities(entities, True)
class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
"""A number class, representing min time for an ecobee thermostat with ventilator attached."""
"""A number class, representing min time for an ecobee thermostat with ventilator attached."""
entity_description: EcobeeNumberEntityDescription
@ -105,3 +119,53 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
"""Set new ventilator Min On Time value."""
self.entity_description.set_fn(self.data, self.thermostat_index, int(value))
self.update_without_throttle = True
class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
"""Minimum outdoor temperature at which the compressor will operate.
This applies more to air source heat pumps than geothermal. This serves as a safety
feature (compressors have a minimum operating temperature) as well as
providing the ability to choose fuel in a dual-fuel system (i.e. choose between
electrical heat pump and fossil auxiliary heat depending on Time of Use, Solar,
etc.).
Note that python-ecobee-api refers to this as Aux Cutover Threshold, but Ecobee
uses Compressor Protection Min Temp.
"""
_attr_device_class = NumberDeviceClass.TEMPERATURE
_attr_has_entity_name = True
_attr_icon = "mdi:thermometer-off"
_attr_mode = NumberMode.BOX
_attr_native_min_value = -25
_attr_native_max_value = 66
_attr_native_step = 5
_attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT
_attr_translation_key = "compressor_protection_min_temp"
def __init__(
self,
data: EcobeeData,
thermostat_index: int,
) -> None:
"""Initialize ecobee compressor min temperature."""
super().__init__(data, thermostat_index)
self._attr_unique_id = f"{self.base_unique_id}_compressor_protection_min_temp"
self.update_without_throttle = False
async def async_update(self) -> None:
"""Get the latest state from the thermostat."""
if self.update_without_throttle:
await self.data.update(no_throttle=True)
self.update_without_throttle = False
else:
await self.data.update()
self._attr_native_value = (
(self.thermostat["settings"]["compressorProtectionMinTemp"]) / 10
)
def set_native_value(self, value: float) -> None:
"""Set new compressor minimum temperature."""
self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value)
self.update_without_throttle = True

View File

@ -33,15 +33,18 @@
},
"number": {
"ventilator_min_type_home": {
"name": "Ventilator min time home"
"name": "Ventilator minimum time home"
},
"ventilator_min_type_away": {
"name": "Ventilator min time away"
"name": "Ventilator minimum time away"
},
"compressor_protection_min_temp": {
"name": "Compressor minimum temperature"
}
},
"switch": {
"aux_heat_only": {
"name": "Aux heat only"
"name": "Auxiliary heat only"
}
}
},

View File

@ -160,6 +160,7 @@
"hasHumidifier": true,
"humidifierMode": "manual",
"hasHeatPump": true,
"compressorProtectionMinTemp": 100,
"humidity": "30"
},
"equipmentStatus": "fan",

View File

@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant
from .common import setup_platform
VENTILATOR_MIN_HOME_ID = "number.ecobee_ventilator_min_time_home"
VENTILATOR_MIN_AWAY_ID = "number.ecobee_ventilator_min_time_away"
VENTILATOR_MIN_HOME_ID = "number.ecobee_ventilator_minimum_time_home"
VENTILATOR_MIN_AWAY_ID = "number.ecobee_ventilator_minimum_time_away"
THERMOSTAT_ID = 0
@ -26,7 +26,9 @@ async def test_ventilator_min_on_home_attributes(hass: HomeAssistant) -> None:
assert state.attributes.get("min") == 0
assert state.attributes.get("max") == 60
assert state.attributes.get("step") == 5
assert state.attributes.get("friendly_name") == "ecobee Ventilator min time home"
assert (
state.attributes.get("friendly_name") == "ecobee Ventilator minimum time home"
)
assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES
@ -39,7 +41,9 @@ async def test_ventilator_min_on_away_attributes(hass: HomeAssistant) -> None:
assert state.attributes.get("min") == 0
assert state.attributes.get("max") == 60
assert state.attributes.get("step") == 5
assert state.attributes.get("friendly_name") == "ecobee Ventilator min time away"
assert (
state.attributes.get("friendly_name") == "ecobee Ventilator minimum time away"
)
assert state.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES
@ -77,3 +81,42 @@ async def test_set_min_time_away(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
mock_set_min_away_time.assert_called_once_with(THERMOSTAT_ID, target_value)
COMPRESSOR_MIN_TEMP_ID = "number.ecobee2_compressor_minimum_temperature"
async def test_compressor_protection_min_temp_attributes(hass: HomeAssistant) -> None:
"""Test the compressor min temp value is correct.
Ecobee runs in Fahrenheit; the test rig runs in Celsius. Conversions are necessary.
"""
await setup_platform(hass, NUMBER_DOMAIN)
state = hass.states.get(COMPRESSOR_MIN_TEMP_ID)
assert state.state == "-12.2"
assert (
state.attributes.get("friendly_name")
== "ecobee2 Compressor minimum temperature"
)
async def test_set_compressor_protection_min_temp(hass: HomeAssistant) -> None:
"""Test the number can set minimum compressor operating temp.
Ecobee runs in Fahrenheit; the test rig runs in Celsius. Conversions are necessary
"""
target_value = 0
with patch(
"homeassistant.components.ecobee.Ecobee.set_aux_cutover_threshold"
) as mock_set_compressor_min_temp:
await setup_platform(hass, NUMBER_DOMAIN)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: COMPRESSOR_MIN_TEMP_ID, ATTR_VALUE: target_value},
blocking=True,
)
await hass.async_block_till_done()
mock_set_compressor_min_temp.assert_called_once_with(1, 32)

View File

@ -118,7 +118,7 @@ async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None:
mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False)
DEVICE_ID = "switch.ecobee2_aux_heat_only"
DEVICE_ID = "switch.ecobee2_auxiliary_heat_only"
async def test_aux_heat_only_turn_on(hass: HomeAssistant) -> None: