From 3639481027e6684dbb3c6dede2f708540b3ee999 Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:05:12 -0700 Subject: [PATCH] Add sensors to venstar integration (#58851) --- .coveragerc | 1 + homeassistant/components/venstar/__init__.py | 16 +-- homeassistant/components/venstar/climate.py | 2 + homeassistant/components/venstar/sensor.py | 135 ++++++++++++++++++ .../venstar/fixtures/colortouch_alerts.json | 1 + .../venstar/fixtures/t2k_alerts.json | 1 + tests/components/venstar/test_climate.py | 8 +- tests/components/venstar/test_init.py | 2 + tests/components/venstar/util.py | 4 + 9 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/venstar/sensor.py create mode 100644 tests/components/venstar/fixtures/colortouch_alerts.json create mode 100644 tests/components/venstar/fixtures/t2k_alerts.json diff --git a/.coveragerc b/.coveragerc index 9591a8705d1..bb7e429fa12 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1178,6 +1178,7 @@ omit = homeassistant/components/venstar/__init__.py homeassistant/components/venstar/binary_sensor.py homeassistant/components/venstar/climate.py + homeassistant/components/venstar/sensor.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py homeassistant/components/verisure/binary_sensor.py diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 72e8b3e32d9..2ed6080f84f 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -PLATFORMS = ["binary_sensor", "climate"] +PLATFORMS = ["binary_sensor", "climate", "sensor"] async def async_setup_entry(hass, config): @@ -88,7 +88,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(3) + await asyncio.sleep(1) try: await self.hass.async_add_executor_job(self.client.update_sensors) @@ -98,7 +98,7 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): ) from ex # older venstars sometimes cannot handle rapid sequential connections - await asyncio.sleep(3) + await asyncio.sleep(1) try: await self.hass.async_add_executor_job(self.client.update_alerts) @@ -129,16 +129,6 @@ class VenstarEntity(CoordinatorEntity): """Handle updated data from the coordinator.""" self.async_write_ha_state() - @property - def name(self): - """Return the name of the thermostat.""" - return self._client.name - - @property - def unique_id(self): - """Set unique_id for this entity.""" - return f"{self._config.entry_id}" - @property def device_info(self): """Return the device information for this entity.""" diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 5d28d41db53..cb4e8ff1527 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -120,6 +120,8 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): HVAC_MODE_COOL: self._client.MODE_COOL, HVAC_MODE_AUTO: self._client.MODE_AUTO, } + self._attr_unique_id = config.entry_id + self._attr_name = self._client.name @property def supported_features(self): diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py new file mode 100644 index 00000000000..dc13269f7df --- /dev/null +++ b/homeassistant/components/venstar/sensor.py @@ -0,0 +1,135 @@ +"""Representation of Venstar sensors.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import Entity + +from . import VenstarDataUpdateCoordinator, VenstarEntity +from .const import DOMAIN + + +@dataclass +class VenstarSensorTypeMixin: + """Mixin for sensor required keys.""" + + cls: type[VenstarSensor] + stype: str + + +@dataclass +class VenstarSensorEntityDescription(SensorEntityDescription, VenstarSensorTypeMixin): + """Base description of a Sensor entity.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up Vensar device binary_sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities: list[Entity] = [] + + sensors = coordinator.client.get_sensor_list() + if not sensors: + return + + entities = [] + + for sensor_name in sensors: + entities.extend( + [ + description.cls(coordinator, config_entry, description, sensor_name) + for description in SENSOR_ENTITIES + if coordinator.client.get_sensor(sensor_name, description.stype) + is not None + ] + ) + + async_add_entities(entities) + + +class VenstarSensor(VenstarEntity, SensorEntity): + """Base class for a Venstar sensor.""" + + entity_description: VenstarSensorEntityDescription + + def __init__( + self, + coordinator: VenstarDataUpdateCoordinator, + config: ConfigEntry, + entity_description: VenstarSensorEntityDescription, + sensor_name: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, config) + self.entity_description = entity_description + self.sensor_name = sensor_name + self._config = config + + @property + def unique_id(self): + """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" + + @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" + ) + + @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) + + +SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( + VenstarSensorEntityDescription( + key="humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + cls=VenstarHumiditySensor, + stype="hum", + ), + VenstarSensorEntityDescription( + key="temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + cls=VenstarTemperatureSensor, + stype="temp", + ), +) diff --git a/tests/components/venstar/fixtures/colortouch_alerts.json b/tests/components/venstar/fixtures/colortouch_alerts.json new file mode 100644 index 00000000000..54a29b9eb3a --- /dev/null +++ b/tests/components/venstar/fixtures/colortouch_alerts.json @@ -0,0 +1 @@ +{"alerts":[{"name":"Air Filter","active":true},{"name":"UV Lamp","active":false},{"name":"Service","active":false}]} diff --git a/tests/components/venstar/fixtures/t2k_alerts.json b/tests/components/venstar/fixtures/t2k_alerts.json new file mode 100644 index 00000000000..54a29b9eb3a --- /dev/null +++ b/tests/components/venstar/fixtures/t2k_alerts.json @@ -0,0 +1 @@ +{"alerts":[{"name":"Air Filter","active":true},{"name":"UV Lamp","active":false},{"name":"Service","active":false}]} diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index 9461032060b..babd946073b 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -1,5 +1,7 @@ """The climate tests for the venstar integration.""" +from unittest.mock import patch + from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, @@ -18,7 +20,8 @@ EXPECTED_BASE_SUPPORTED_FEATURES = ( async def test_colortouch(hass): """Test interfacing with a venstar colortouch with attached humidifier.""" - await async_init_integration(hass) + with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + await async_init_integration(hass) state = hass.states.get("climate.colortouch") assert state.state == "heat" @@ -53,7 +56,8 @@ async def test_colortouch(hass): async def test_t2000(hass): """Test interfacing with a venstar T2000 presently turned off.""" - await async_init_integration(hass) + with patch("homeassistant.components.onewire.sensor.asyncio.sleep"): + await async_init_integration(hass) state = hass.states.get("climate.t2000") assert state.state == "off" diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 08dae732466..b245f4eef6d 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -36,6 +36,8 @@ async def test_setup_entry(hass: HomeAssistant): ), patch( "homeassistant.components.venstar.VenstarColorTouch.update_alerts", new=VenstarColorTouchMock.update_alerts, + ), patch( + "homeassistant.components.onewire.sensor.asyncio.sleep" ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py index b86f8475798..aa53a9c0a8d 100644 --- a/tests/components/venstar/util.py +++ b/tests/components/venstar/util.py @@ -33,6 +33,10 @@ def mock_venstar_devices(f): f"http://venstar-{model}.localdomain/query/sensors", text=load_fixture(f"venstar/{model}_sensors.json"), ) + m.get( + f"http://venstar-{model}.localdomain/query/alerts", + text=load_fixture(f"venstar/{model}_alerts.json"), + ) return await f(hass) return wrapper