From 157222126ea25819fc34370c00e993ca77b18073 Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Tue, 22 Nov 2022 13:46:57 +0200 Subject: [PATCH] Add Vallox temperature control entities (#75858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastian Lövdahl Co-authored-by: Andre Richter Co-authored-by: Martin Hjelmare --- homeassistant/components/vallox/__init__.py | 1 + homeassistant/components/vallox/number.py | 127 ++++++++++++++++++++ tests/components/vallox/test_number.py | 80 ++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 homeassistant/components/vallox/number.py create mode 100644 tests/components/vallox/test_number.py diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 1a1788aeeedda6..f393342dfd5427 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -63,6 +63,7 @@ Platform.SENSOR, Platform.FAN, Platform.BINARY_SENSOR, + Platform.NUMBER, Platform.SWITCH, ] diff --git a/homeassistant/components/vallox/number.py b/homeassistant/components/vallox/number.py new file mode 100644 index 00000000000000..5be91fe66e670e --- /dev/null +++ b/homeassistant/components/vallox/number.py @@ -0,0 +1,127 @@ +"""Support for Vallox ventilation unit numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from vallox_websocket_api import Vallox + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ValloxDataUpdateCoordinator, ValloxEntity +from .const import DOMAIN + + +class ValloxNumberEntity(ValloxEntity, NumberEntity): + """Representation of a Vallox number entity.""" + + entity_description: ValloxNumberEntityDescription + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, + name: str, + coordinator: ValloxDataUpdateCoordinator, + description: ValloxNumberEntityDescription, + client: Vallox, + ) -> None: + """Initialize the Vallox number entity.""" + super().__init__(name, coordinator) + + self.entity_description = description + + self._attr_unique_id = f"{self._device_uuid}-{description.key}" + self._client = client + + @property + def native_value(self) -> float | None: + """Return the value reported by the sensor.""" + if ( + value := self.coordinator.data.get_metric( + self.entity_description.metric_key + ) + ) is None: + return None + + return float(value) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self._client.set_values( + {self.entity_description.metric_key: float(value)} + ) + await self.coordinator.async_request_refresh() + + +@dataclass +class ValloxMetricMixin: + """Holds Vallox metric key.""" + + metric_key: str + + +@dataclass +class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): + """Describes Vallox number entity.""" + + +NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( + ValloxNumberEntityDescription( + key="supply_air_target_home", + name="Supply air temperature (Home)", + metric_key="A_CYC_HOME_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), + ValloxNumberEntityDescription( + key="supply_air_target_away", + name="Supply air temperature (Away)", + metric_key="A_CYC_AWAY_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), + ValloxNumberEntityDescription( + key="supply_air_target_boost", + name="Supply air temperature (Boost)", + metric_key="A_CYC_BOOST_AIR_TEMP_TARGET", + device_class=NumberDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + native_min_value=5.0, + native_max_value=25.0, + native_step=1.0, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + ValloxNumberEntity( + data["name"], data["coordinator"], description, data["client"] + ) + for description in NUMBER_ENTITIES + ] + ) diff --git a/tests/components/vallox/test_number.py b/tests/components/vallox/test_number.py new file mode 100644 index 00000000000000..3d05cafaef1c9a --- /dev/null +++ b/tests/components/vallox/test_number.py @@ -0,0 +1,80 @@ +"""Tests for Vallox number platform.""" +import pytest + +from homeassistant.components.number.const import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import patch_metrics, patch_metrics_set + +from tests.common import MockConfigEntry + +TEST_TEMPERATURE_ENTITIES_DATA = [ + ( + "number.vallox_supply_air_temperature_home", + "A_CYC_HOME_AIR_TEMP_TARGET", + 19.0, + ), + ( + "number.vallox_supply_air_temperature_away", + "A_CYC_AWAY_AIR_TEMP_TARGET", + 18.0, + ), + ( + "number.vallox_supply_air_temperature_boost", + "A_CYC_BOOST_AIR_TEMP_TARGET", + 17.0, + ), +] + + +@pytest.mark.parametrize("entity_id, metric_key, value", TEST_TEMPERATURE_ENTITIES_DATA) +async def test_temperature_number_entities( + entity_id: str, + metric_key: str, + value: float, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test temperature entities.""" + # Arrange + metrics = {metric_key: value} + + # Act + with patch_metrics(metrics=metrics): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Assert + sensor = hass.states.get(entity_id) + assert sensor.state == str(value) + assert sensor.attributes["unit_of_measurement"] == "°C" + + +@pytest.mark.parametrize("entity_id, metric_key, value", TEST_TEMPERATURE_ENTITIES_DATA) +async def test_temperature_number_entity_set( + entity_id: str, + metric_key: str, + value: float, + mock_entry: MockConfigEntry, + hass: HomeAssistant, +) -> None: + """Test temperature set.""" + # Act + with patch_metrics(metrics={}), patch_metrics_set() as metrics_set: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }, + ) + await hass.async_block_till_done() + metrics_set.assert_called_once_with({metric_key: value})