From 7d2fa5bf60de0c88df3c3476eb4479e5dc273b8f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 22 Oct 2023 22:39:40 +0200 Subject: [PATCH] Correct range for nibe_heatpump numbers (#102553) --- .coveragerc | 1 - .../components/nibe_heatpump/number.py | 2 + tests/components/nibe_heatpump/__init__.py | 17 +++ tests/components/nibe_heatpump/conftest.py | 13 +- .../nibe_heatpump/snapshots/test_number.ambr | 135 ++++++++++++++++++ tests/components/nibe_heatpump/test_button.py | 14 +- tests/components/nibe_heatpump/test_number.py | 109 ++++++++++++++ 7 files changed, 277 insertions(+), 14 deletions(-) create mode 100644 tests/components/nibe_heatpump/snapshots/test_number.ambr create mode 100644 tests/components/nibe_heatpump/test_number.py diff --git a/.coveragerc b/.coveragerc index b994e61a122..9634ca2edb8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -827,7 +827,6 @@ omit = homeassistant/components/nibe_heatpump/__init__.py homeassistant/components/nibe_heatpump/climate.py homeassistant/components/nibe_heatpump/binary_sensor.py - homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py homeassistant/components/nibe_heatpump/switch.py diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 1b3bc928985..8231cc65450 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -50,6 +50,8 @@ class Number(CoilEntity, NumberEntity): self._attr_native_min_value, self._attr_native_max_value, ) = _get_numeric_limits(coil.size) + self._attr_native_min_value /= coil.factor + self._attr_native_max_value /= coil.factor else: self._attr_native_min_value = float(coil.min) self._attr_native_max_value = float(coil.max) diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 5446e289656..d2852ec42f5 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -2,12 +2,24 @@ from typing import Any +from nibe.heatpump import Model + from homeassistant.components.nibe_heatpump import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +MOCK_ENTRY_DATA = { + "model": None, + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, + "word_swap": True, + "connection_type": "nibegw", +} + async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: """Add entry and get the coordinator.""" @@ -17,3 +29,8 @@ async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED + + +async def async_add_model(hass: HomeAssistant, model: Model): + """Add entry of specific model.""" + await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index 2a4e2f80ff5..d7343eac69c 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -62,4 +62,15 @@ async def fixture_coils(mock_connection): mock_connection.read_coil = read_coil mock_connection.read_coils = read_coils - return coils + + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.nibe_heatpump import HeatPump + + get_coils_original = HeatPump.get_coils + + def get_coils(x): + coils_data = get_coils_original(x) + return [coil for coil in coils_data if coil.address in coils] + + with patch.object(HeatPump, "get_coils", new=get_coils): + yield coils diff --git a/tests/components/nibe_heatpump/snapshots/test_number.ambr b/tests/components/nibe_heatpump/snapshots/test_number.ambr new file mode 100644 index 00000000000..d174c0cc059 --- /dev/null +++ b/tests/components/nibe_heatpump/snapshots/test_number.ambr @@ -0,0 +1,135 @@ +# serializer version: 1 +# name: test_update[Model.F1155-47011-number.heat_offset_s1_47011--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1155 Heat Offset S1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heat_offset_s1_47011', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.F1155-47011-number.heat_offset_s1_47011-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F1155 Heat Offset S1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heat_offset_s1_47011', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.F1155-47062-number.heat_offset_s1_47011-None] + None +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.F750-47062-number.hw_charge_offset_47062-None] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'F750 HW charge offset', + 'max': 12.7, + 'min': -12.8, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': '°C', + }), + 'context': , + 'entity_id': 'number.hw_charge_offset_47062', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031--10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': '-10.0', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031-10] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_update[Model.S320-40031-number.heating_offset_climate_system_1_40031-None] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'S320 Heating offset climate system 1', + 'max': 10.0, + 'min': -10.0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.heating_offset_climate_system_1_40031', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py index e4f90a59f67..755827fa128 100644 --- a/tests/components/nibe_heatpump/test_button.py +++ b/tests/components/nibe_heatpump/test_button.py @@ -17,20 +17,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import async_add_entry +from . import async_add_model from tests.common import async_fire_time_changed -MOCK_ENTRY_DATA = { - "model": None, - "ip_address": "127.0.0.1", - "listening_port": 9999, - "remote_read_port": 10000, - "remote_write_port": 10001, - "word_swap": True, - "connection_type": "nibegw", -} - @pytest.fixture(autouse=True) async def fixture_single_platform(): @@ -62,7 +52,7 @@ async def test_reset_button( coils[unit.alarm_reset] = 0 coils[unit.alarm] = 0 - await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) + await async_add_model(hass, model) state = hass.states.get(entity_id) assert state diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py new file mode 100644 index 00000000000..5c4d7f4341b --- /dev/null +++ b/tests/components/nibe_heatpump/test_number.py @@ -0,0 +1,109 @@ +"""Test the Nibe Heat Pump config flow.""" +from typing import Any +from unittest.mock import AsyncMock, patch + +from nibe.coil import CoilData +from nibe.heatpump import Model +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant + +from . import async_add_model + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch("homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.NUMBER]): + yield + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "value"), + [ + # Tests for S series coils with min/max + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", 10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", -10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", None), + # Tests for F series coils with min/max + (Model.F1155, 47011, "number.heat_offset_s1_47011", 10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", -10), + (Model.F1155, 47062, "number.heat_offset_s1_47011", None), + # Tests for F series coils without min/max + (Model.F750, 47062, "number.hw_charge_offset_47062", 10), + (Model.F750, 47062, "number.hw_charge_offset_47062", -10), + (Model.F750, 47062, "number.hw_charge_offset_47062", None), + ], +) +async def test_update( + hass: HomeAssistant, + model: Model, + entity_id: str, + address: int, + value: Any, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, + snapshot: SnapshotAssertion, +) -> None: + """Test setting of value.""" + coils[address] = value + + await async_add_model(hass, model) + + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state == snapshot + + +@pytest.mark.parametrize( + ("model", "address", "entity_id", "value"), + [ + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", 10), + (Model.S320, 40031, "number.heating_offset_climate_system_1_40031", -10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", 10), + (Model.F1155, 47011, "number.heat_offset_s1_47011", -10), + (Model.F750, 47062, "number.hw_charge_offset_47062", 10), + ], +) +async def test_set_value( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + address: int, + value: Any, + coils: dict[int, Any], + entity_registry_enabled_by_default: None, +) -> None: + """Test setting of value.""" + coils[address] = 0 + + await async_add_model(hass, model) + + await hass.async_block_till_done() + assert hass.states.get(entity_id) + + # Write value + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + await hass.async_block_till_done() + + # Verify written + args = mock_connection.write_coil.call_args + assert args + coil = args.args[0] + assert isinstance(coil, CoilData) + assert coil.coil.address == address + assert coil.value == value