From 22a9246eb5390d2651cf89990e2bbaaa5657bd35 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 29 Aug 2026 01:24:30 +0200 Subject: [PATCH] Fix the Peblar charge limit maximum (#180564) --- homeassistant/components/peblar/__init__.py | 1 + homeassistant/components/peblar/number.py | 10 +++- tests/components/peblar/conftest.py | 30 +++++++++--- tests/components/peblar/test_number.py | 53 +++++++++++++++++++++ 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/peblar/__init__.py b/homeassistant/components/peblar/__init__.py index db4ebe80824b..bc911dccd30b 100644 --- a/homeassistant/components/peblar/__init__.py +++ b/homeassistant/components/peblar/__init__.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_create_clientsession +from .const import DOMAIN from .coordinator import ( PeblarConfigEntry, PeblarDataUpdateCoordinator, diff --git a/homeassistant/components/peblar/number.py b/homeassistant/components/peblar/number.py index 4bc28c345f95..3379ea5d0ab7 100644 --- a/homeassistant/components/peblar/number.py +++ b/homeassistant/components/peblar/number.py @@ -70,8 +70,16 @@ class PeblarChargeCurrentLimitNumberEntity( coordinator=coordinator, description=NumberEntityDescription(key="charge_current_limit"), ) + # Not the user's own charge limit: that is the value being set here, + # so using it as the ceiling would ratchet the slider down and never + # let it back up. The charger accepts up to its hardware rating, and + # reduces anything above the installation limit configured during + # commissioning, so the lower of the two is what can actually be set. configuration = entry.runtime_data.user_configuration_coordinator.data - self._attr_native_max_value = configuration.user_defined_charge_limit_current + self._attr_native_max_value = min( + entry.runtime_data.system_information.hardware_max_current, + configuration.current_control_fixed_charge_current_limit, + ) @override async def async_added_to_hass(self) -> None: diff --git a/tests/components/peblar/conftest.py b/tests/components/peblar/conftest.py index 93380afae09a..e8fe5fad7f44 100644 --- a/tests/components/peblar/conftest.py +++ b/tests/components/peblar/conftest.py @@ -2,6 +2,7 @@ from collections.abc import Generator from contextlib import nullcontext +import json from unittest.mock import MagicMock, patch from peblar import ( @@ -43,8 +44,25 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture -def mock_peblar() -> Generator[MagicMock]: - """Return a mocked Peblar client.""" +def mock_peblar(request: pytest.FixtureRequest) -> Generator[MagicMock]: + """Return a mocked Peblar client. + + Parametrize indirectly with a dict to override single fixture fields, + so a test that cares about one flag or one limit does not need a full + copy of the fixture. Keys are looked up in both the system information + and the user configuration. + """ + overrides = getattr(request, "param", {}) + system_information = json.loads(load_fixture("system_information.json", DOMAIN)) + user_configuration = json.loads(load_fixture("user_configuration.json", DOMAIN)) + for key, value in overrides.items(): + if key in system_information: + system_information[key] = value + elif key in user_configuration: + user_configuration[key] = value + else: + msg = f"Unknown fixture field: {key}" + raise ValueError(msg) with ( patch("homeassistant.components.peblar.Peblar", autospec=True) as peblar_mock, patch("homeassistant.components.peblar.config_flow.Peblar", new=peblar_mock), @@ -56,11 +74,11 @@ def mock_peblar() -> Generator[MagicMock]: peblar.current_versions.return_value = PeblarVersions.from_json( load_fixture("current_versions.json", DOMAIN) ) - peblar.user_configuration.return_value = PeblarUserConfiguration.from_json( - load_fixture("user_configuration.json", DOMAIN) + peblar.user_configuration.return_value = PeblarUserConfiguration.from_dict( + user_configuration ) - peblar.system_information.return_value = PeblarSystemInformation.from_json( - load_fixture("system_information.json", DOMAIN) + peblar.system_information.return_value = PeblarSystemInformation.from_dict( + system_information ) api = peblar.rest_api.return_value diff --git a/tests/components/peblar/test_number.py b/tests/components/peblar/test_number.py index b8b82af12002..af4697274cf1 100644 --- a/tests/components/peblar/test_number.py +++ b/tests/components/peblar/test_number.py @@ -7,6 +7,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( + ATTR_MAX, ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -265,3 +266,55 @@ async def test_restore_state( # Check if state is restored and value is set correctly assert (state := hass.states.get("number.peblar_ev_charger_charge_limit")) assert state.state == expected_state + + +@pytest.mark.parametrize( + ("mock_peblar", "expected_max"), + [ + ({"UserDefinedChargeLimitCurrent": 10}, 16), + ({"CurrentCtrlFixedChargeCurrentLimit": 10}, 10), + ({"HwMaxCurrent": 10}, 10), + ], + ids=["user limit is not a ceiling", "installation limit", "hardware rating"], + indirect=["mock_peblar"], +) +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") +async def test_charge_limit_maximum( + hass: HomeAssistant, + expected_max: int, +) -> None: + """Test the ceiling on the charge limit. + + The charger accepts up to its hardware rating and reduces anything + above the installation limit. The user's own charge limit is the value + being set here, so it must not narrow the range it is chosen from. + """ + state = hass.states.get("number.peblar_ev_charger_charge_limit") + assert state + assert state.attributes[ATTR_MAX] == expected_max + + +@pytest.mark.parametrize( + "mock_peblar", + [{"UserDefinedChargeLimitCurrent": 10}], + indirect=True, +) +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") +async def test_charge_limit_can_be_raised_again( + hass: HomeAssistant, + mock_peblar: MagicMock, +) -> None: + """A charger left on a low limit can still be turned back up.""" + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.peblar_ev_charger_charge_limit", ATTR_VALUE: 16}, + blocking=True, + ) + + mocked_method.assert_any_call(charge_current_limit=16000)