diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index be082d0ad85..9bd42c33203 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1,4 +1,6 @@ """The venstar component.""" +from __future__ import annotations + import asyncio from datetime import timedelta @@ -18,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import update_coordinator from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT +from .const import _LOGGER, DOMAIN, VENSTAR_SLEEP, VENSTAR_TIMEOUT PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] @@ -78,6 +80,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): update_interval=timedelta(seconds=60), ) self.client = venstar_connection + self.runtimes: list[dict[str, int]] = [] async def _async_update_data(self) -> None: """Update the state.""" @@ -89,7 +92,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(1) + await asyncio.sleep(VENSTAR_SLEEP) try: await self.hass.async_add_executor_job(self.client.update_sensors) @@ -99,7 +102,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(1) + await asyncio.sleep(VENSTAR_SLEEP) try: await self.hass.async_add_executor_job(self.client.update_alerts) @@ -107,6 +110,19 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): raise update_coordinator.UpdateFailed( f"Exception during Venstar alert update: {ex}" ) from ex + + # older venstars sometimes cannot handle rapid sequential connections + await asyncio.sleep(VENSTAR_SLEEP) + + try: + self.runtimes = await self.hass.async_add_executor_job( + self.client.get_runtimes + ) + except (OSError, RequestException) as ex: + raise update_coordinator.UpdateFailed( + f"Exception during Venstar runtime update: {ex}" + ) from ex + return None diff --git a/homeassistant/components/venstar/const.py b/homeassistant/components/venstar/const.py index 999e08384dd..ec672afd714 100644 --- a/homeassistant/components/venstar/const.py +++ b/homeassistant/components/venstar/const.py @@ -25,5 +25,6 @@ HOLD_MODE_OFF = "off" HOLD_MODE_TEMPERATURE = "temperature" VENSTAR_TIMEOUT = 5 +VENSTAR_SLEEP = 1.0 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index dc13269f7df..d7b806927ae 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -1,9 +1,12 @@ """Representation of Venstar sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -11,19 +14,51 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_MINUTES from homeassistant.helpers.entity import Entity from . import VenstarDataUpdateCoordinator, VenstarEntity from .const import DOMAIN +RUNTIME_HEAT1 = "heat1" +RUNTIME_HEAT2 = "heat2" +RUNTIME_COOL1 = "cool1" +RUNTIME_COOL2 = "cool2" +RUNTIME_AUX1 = "aux1" +RUNTIME_AUX2 = "aux2" +RUNTIME_FC = "fc" +RUNTIME_OV = "ov" + +RUNTIME_DEVICES = [ + RUNTIME_HEAT1, + RUNTIME_HEAT2, + RUNTIME_COOL1, + RUNTIME_COOL2, + RUNTIME_AUX1, + RUNTIME_AUX2, + RUNTIME_FC, + RUNTIME_OV, +] + +RUNTIME_ATTRIBUTES = { + RUNTIME_HEAT1: "Heating Stage 1", + RUNTIME_HEAT2: "Heating Stage 2", + RUNTIME_COOL1: "Cooling Stage 1", + RUNTIME_COOL2: "Cooling Stage 2", + RUNTIME_AUX1: "Aux Stage 1", + RUNTIME_AUX2: "Aux Stage 2", + RUNTIME_FC: "Free Cooling", + RUNTIME_OV: "Override", +} + @dataclass class VenstarSensorTypeMixin: """Mixin for sensor required keys.""" - cls: type[VenstarSensor] - stype: str + value_fn: Callable[[Any, Any], Any] + name_fn: Callable[[Any, Any], str] + uom_fn: Callable[[Any], str] @dataclass @@ -40,21 +75,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: if not sensors: return - entities = [] - for sensor_name in sensors: entities.extend( [ - description.cls(coordinator, config_entry, description, sensor_name) + VenstarSensor(coordinator, config_entry, description, sensor_name) for description in SENSOR_ENTITIES - if coordinator.client.get_sensor(sensor_name, description.stype) + if coordinator.client.get_sensor(sensor_name, description.key) is not None ] ) + runtimes = coordinator.runtimes[-1] + for sensor_name in runtimes: + if sensor_name in RUNTIME_DEVICES: + entities.append( + VenstarSensor(coordinator, config_entry, RUNTIME_ENTITY, sensor_name) + ) + async_add_entities(entities) +def temperature_unit(coordinator: VenstarDataUpdateCoordinator) -> str: + """Return the correct unit for temperature.""" + unit = TEMP_CELSIUS + if coordinator.client.tempunits == coordinator.client.TEMPUNITS_F: + unit = TEMP_FAHRENHEIT + return unit + + class VenstarSensor(VenstarEntity, SensorEntity): """Base class for a Venstar sensor.""" @@ -78,58 +126,59 @@ class VenstarSensor(VenstarEntity, SensorEntity): """Return the unique id.""" return f"{self._config.entry_id}_{self.sensor_name.replace(' ', '_')}_{self.entity_description.key}" - -class VenstarHumiditySensor(VenstarSensor): - """Represent a Venstar humidity sensor.""" - @property def name(self): """Return the name of the device.""" - return f"{self._client.name} {self.sensor_name} Humidity" + return self.entity_description.name_fn(self.coordinator, self.sensor_name) @property def native_value(self) -> int: """Return state of the sensor.""" - return self._client.get_sensor(self.sensor_name, "hum") - - -class VenstarTemperatureSensor(VenstarSensor): - """Represent a Venstar temperature sensor.""" - - @property - def name(self): - """Return the name of the device.""" - return ( - f"{self._client.name} {self.sensor_name.replace(' Temp', '')} Temperature" - ) + return self.entity_description.value_fn(self.coordinator, self.sensor_name) @property def native_unit_of_measurement(self) -> str: """Return unit of measurement the value is expressed in.""" - if self._client.tempunits == self._client.TEMPUNITS_F: - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def native_value(self) -> float: - """Return state of the sensor.""" - return round(float(self._client.get_sensor(self.sensor_name, "temp")), 1) + return self.entity_description.uom_fn(self.coordinator) SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( VenstarSensorEntityDescription( - key="humidity", + key="hum", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - cls=VenstarHumiditySensor, - stype="hum", + uom_fn=lambda coordinator: PERCENTAGE, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "hum" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Humidity", ), VenstarSensorEntityDescription( - key="temperature", + key="temp", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - cls=VenstarTemperatureSensor, - stype="temp", + uom_fn=temperature_unit, + value_fn=lambda coordinator, sensor_name: round( + float(coordinator.client.get_sensor(sensor_name, "temp")), 1 + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name.replace(' Temp', '')} Temperature", + ), + VenstarSensorEntityDescription( + key="battery", + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + uom_fn=lambda coordinator: PERCENTAGE, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "battery" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Battery", ), ) + +RUNTIME_ENTITY = VenstarSensorEntityDescription( + key="runtime", + state_class=STATE_CLASS_MEASUREMENT, + uom_fn=lambda coordinator: TIME_MINUTES, + value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", +) diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index 326aeeeb0e2..c7b4815c5bb 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -60,3 +60,7 @@ class VenstarColorTouchMock: def update_alerts(self): """Mock update_alerts.""" return True + + def get_runtimes(self): + """Mock get runtimes.""" + return {} diff --git a/tests/components/venstar/fixtures/colortouch_runtimes.json b/tests/components/venstar/fixtures/colortouch_runtimes.json new file mode 100644 index 00000000000..2ec323755c2 --- /dev/null +++ b/tests/components/venstar/fixtures/colortouch_runtimes.json @@ -0,0 +1 @@ +{"runtimes":[{"ts":1637452800,"heat1":0,"heat2":0,"cool1":156,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637539200,"heat1":0,"heat2":0,"cool1":216,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637625600,"heat1":0,"heat2":0,"cool1":234,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637712000,"heat1":0,"heat2":0,"cool1":225,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637798400,"heat1":0,"heat2":0,"cool1":153,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637884800,"heat1":0,"heat2":0,"cool1":94,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637921499,"heat1":0,"heat2":0,"cool1":12,"cool2":0,"aux1":0,"aux2":0,"fc":0}]} \ No newline at end of file diff --git a/tests/components/venstar/fixtures/t2k_runtimes.json b/tests/components/venstar/fixtures/t2k_runtimes.json new file mode 100644 index 00000000000..bea2697a387 --- /dev/null +++ b/tests/components/venstar/fixtures/t2k_runtimes.json @@ -0,0 +1 @@ +{"runtimes":[{"ts":1637452800,"heat1":0,"heat2":0,"cool1":156,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637539200,"heat1":0,"heat2":0,"cool1":216,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637625600,"heat1":0,"heat2":0,"cool1":234,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637712000,"heat1":0,"heat2":0,"cool1":225,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637798400,"heat1":0,"heat2":0,"cool1":153,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637884800,"heat1":0,"heat2":0,"cool1":94,"cool2":0,"aux1":0,"aux2":0,"fc":0},{"ts":1637921489,"heat1":0,"heat2":0,"cool1":12,"cool2":0,"aux1":0,"aux2":0,"fc":0}]} \ No newline at end of file diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index babd946073b..fe1e6141c98 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -20,7 +20,7 @@ EXPECTED_BASE_SUPPORTED_FEATURES = ( async def test_colortouch(hass): """Test interfacing with a venstar colortouch with attached humidifier.""" - with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + with patch("homeassistant.components.venstar.VENSTAR_SLEEP", new=0): await async_init_integration(hass) state = hass.states.get("climate.colortouch") @@ -56,7 +56,7 @@ async def test_colortouch(hass): async def test_t2000(hass): """Test interfacing with a venstar T2000 presently turned off.""" - with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + with patch("homeassistant.components.venstar.VENSTAR_SLEEP", new=0): await async_init_integration(hass) state = hass.states.get("climate.t2000") diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index b245f4eef6d..696f20ed105 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -37,7 +37,11 @@ async def test_setup_entry(hass: HomeAssistant): "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, ), patch( - "homeassistant.components.onewire.sensor.asyncio.sleep" + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, + ), patch( + "homeassistant.components.venstar.VENSTAR_SLEEP", + new=0, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -72,6 +76,9 @@ async def test_setup_entry_exception(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done()