Add Venstar runtimes and battery sensors (#60414)
* Venstar: Add runtimes and battery sensors * Address review - replace classes with lambda functions * Clean up patch usage, make temperature_unit it's own function * Remove double define of entities
This commit is contained in:
parent
bf1cacf4b2
commit
dd4ede09c8
8 changed files with 124 additions and 45 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -25,5 +25,6 @@ HOLD_MODE_OFF = "off"
|
|||
HOLD_MODE_TEMPERATURE = "temperature"
|
||||
|
||||
VENSTAR_TIMEOUT = 5
|
||||
VENSTAR_SLEEP = 1.0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -60,3 +60,7 @@ class VenstarColorTouchMock:
|
|||
def update_alerts(self):
|
||||
"""Mock update_alerts."""
|
||||
return True
|
||||
|
||||
def get_runtimes(self):
|
||||
"""Mock get runtimes."""
|
||||
return {}
|
||||
|
|
|
@ -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}]}
|
1
tests/components/venstar/fixtures/t2k_runtimes.json
Normal file
1
tests/components/venstar/fixtures/t2k_runtimes.json
Normal file
|
@ -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}]}
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue