From b5d7eba4f680ed3fdb4ab74fe3ac7689138ca9c2 Mon Sep 17 00:00:00 2001 From: Hessel Date: Wed, 4 Sep 2024 14:00:38 +0200 Subject: [PATCH] Add new number component for setting the wallbox ICP current (#125209) * Add new number component for setting the wallbox ICP current * feat: Add number component for wallbox ICP current control --- homeassistant/components/wallbox/const.py | 4 + .../components/wallbox/coordinator.py | 29 +++++ homeassistant/components/wallbox/number.py | 11 ++ homeassistant/components/wallbox/sensor.py | 8 ++ homeassistant/components/wallbox/strings.json | 6 + tests/components/wallbox/__init__.py | 8 ++ tests/components/wallbox/const.py | 1 + tests/components/wallbox/test_number.py | 105 +++++++++++++++++- 8 files changed, 171 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 69633cbda22..c38b8967776 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -22,11 +22,15 @@ CHARGER_CURRENCY_KEY = "currency" CHARGER_DATA_KEY = "config_data" CHARGER_DEPOT_PRICE_KEY = "depot_price" CHARGER_ENERGY_PRICE_KEY = "energy_price" +CHARGER_FEATURES_KEY = "features" CHARGER_SERIAL_NUMBER_KEY = "serial_number" CHARGER_PART_NUMBER_KEY = "part_number" +CHARGER_PLAN_KEY = "plan" +CHARGER_POWER_BOOST_KEY = "POWER_BOOST" CHARGER_SOFTWARE_KEY = "software" CHARGER_MAX_AVAILABLE_POWER_KEY = "max_available_power" CHARGER_MAX_CHARGING_CURRENT_KEY = "max_charging_current" +CHARGER_MAX_ICP_CURRENT_KEY = "icp_max_current" CHARGER_PAUSE_RESUME_KEY = "paused" CHARGER_LOCKED_UNLOCKED_KEY = "locked" CHARGER_NAME_KEY = "name" diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index e24ccd28440..f3679551bc4 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -19,8 +19,12 @@ from .const import ( CHARGER_CURRENCY_KEY, CHARGER_DATA_KEY, CHARGER_ENERGY_PRICE_KEY, + CHARGER_FEATURES_KEY, CHARGER_LOCKED_UNLOCKED_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_MAX_ICP_CURRENT_KEY, + CHARGER_PLAN_KEY, + CHARGER_POWER_BOOST_KEY, CHARGER_STATUS_DESCRIPTION_KEY, CHARGER_STATUS_ID_KEY, CODE_KEY, @@ -130,6 +134,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][ CHARGER_ENERGY_PRICE_KEY ] + # Only show max_icp_current if power_boost is available in the wallbox unit: + if ( + data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0 + and CHARGER_POWER_BOOST_KEY + in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY] + ): + data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][ + CHARGER_MAX_ICP_CURRENT_KEY + ] + data[CHARGER_CURRENCY_KEY] = ( f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" ) @@ -160,6 +174,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) await self.async_request_refresh() + @_require_authentication + def _set_icp_current(self, icp_current: float) -> None: + """Set maximum icp current for Wallbox.""" + try: + self._wallbox.setIcpMaxCurrent(self._station, icp_current) + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 403: + raise InvalidAuth from wallbox_connection_error + raise + + async def async_set_icp_current(self, icp_current: float) -> None: + """Set maximum icp current for Wallbox.""" + await self.hass.async_add_executor_job(self._set_icp_current, icp_current) + await self.async_request_refresh() + @_require_authentication def _set_energy_cost(self, energy_cost: float) -> None: """Set energy cost for Wallbox.""" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 8ae4c473299..24cdd16f99d 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -21,6 +21,7 @@ from .const import ( CHARGER_ENERGY_PRICE_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_MAX_ICP_CURRENT_KEY, CHARGER_PART_NUMBER_KEY, CHARGER_SERIAL_NUMBER_KEY, DOMAIN, @@ -67,6 +68,16 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { set_value_fn=lambda coordinator: coordinator.async_set_energy_cost, native_step=0.01, ), + CHARGER_MAX_ICP_CURRENT_KEY: WallboxNumberEntityDescription( + key=CHARGER_MAX_ICP_CURRENT_KEY, + translation_key="maximum_icp_current", + max_value_fn=lambda coordinator: cast( + float, coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY] + ), + min_value_fn=lambda _: 6, + set_value_fn=lambda coordinator: coordinator.async_set_icp_current, + native_step=1, + ), } diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index eadbc04dca2..18d8afb5612 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -38,6 +38,7 @@ from .const import ( CHARGER_ENERGY_PRICE_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_MAX_ICP_CURRENT_KEY, CHARGER_SERIAL_NUMBER_KEY, CHARGER_STATE_OF_CHARGE_KEY, CHARGER_STATUS_DESCRIPTION_KEY, @@ -145,6 +146,13 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), + CHARGER_MAX_ICP_CURRENT_KEY: WallboxSensorEntityDescription( + key=CHARGER_MAX_ICP_CURRENT_KEY, + translation_key=CHARGER_MAX_ICP_CURRENT_KEY, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index dd96cebf605..f4378b328d8 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -38,6 +38,9 @@ }, "energy_price": { "name": "Energy price" + }, + "maximum_icp_current": { + "name": "Maximum ICP current" } }, "sensor": { @@ -79,6 +82,9 @@ }, "max_charging_current": { "name": "Max charging current" + }, + "icp_max_current": { + "name": "Max ICP current" } }, "switch": { diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index f21e895b3a7..f4258ea0d49 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -14,11 +14,15 @@ from homeassistant.components.wallbox.const import ( CHARGER_CURRENT_VERSION_KEY, CHARGER_DATA_KEY, CHARGER_ENERGY_PRICE_KEY, + CHARGER_FEATURES_KEY, CHARGER_LOCKED_UNLOCKED_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_MAX_ICP_CURRENT_KEY, CHARGER_NAME_KEY, CHARGER_PART_NUMBER_KEY, + CHARGER_PLAN_KEY, + CHARGER_POWER_BOOST_KEY, CHARGER_SERIAL_NUMBER_KEY, CHARGER_SOFTWARE_KEY, CHARGER_STATUS_ID_KEY, @@ -45,6 +49,8 @@ test_response = { CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, + CHARGER_MAX_ICP_CURRENT_KEY: 20, + CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, }, } @@ -64,6 +70,8 @@ test_response_bidir = { CHARGER_PART_NUMBER_KEY: "QSP1-0-2-4-9-002-E", CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, + CHARGER_MAX_ICP_CURRENT_KEY: 20, + CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, }, } diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index 452b3af0af8..a86ae9fc3b9 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -9,6 +9,7 @@ STATUS = "status" MOCK_NUMBER_ENTITY_ID = "number.wallbox_wallboxname_maximum_charging_current" MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID = "number.wallbox_wallboxname_energy_price" +MOCK_NUMBER_ENTITY_ICP_CURRENT_ID = "number.wallbox_wallboxname_maximum_icp_current" MOCK_LOCK_ENTITY_ID = "lock.wallbox_wallboxname_lock" MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.wallbox_wallboxname_charging_speed" MOCK_SENSOR_CHARGING_POWER_ID = "sensor.wallbox_wallboxname_charging_power" diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 5d782224ce5..0a8b1aa1207 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -6,9 +6,12 @@ import pytest import requests_mock from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.wallbox import InvalidAuth from homeassistant.components.wallbox.const import ( CHARGER_ENERGY_PRICE_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_MAX_ICP_CURRENT_KEY, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -20,7 +23,11 @@ from . import ( setup_integration_bidir, setup_integration_platform_not_ready, ) -from .const import MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, MOCK_NUMBER_ENTITY_ID +from .const import ( + MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, + MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + MOCK_NUMBER_ENTITY_ID, +) from tests.common import MockConfigEntry @@ -212,3 +219,99 @@ async def test_wallbox_number_class_platform_not_ready( assert state is None await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_number_class_icp_energy( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=200, + ) + + mock_request.post( + "https://api.wall-box.com/chargers/config/12345", + json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, + status_code=200, + ) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_number_class_icp_energy_auth_error( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=200, + ) + mock_request.post( + "https://api.wall-box.com/chargers/config/12345", + json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, + status_code=403, + ) + + with pytest.raises(InvalidAuth): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_number_class_icp_energy_connection_error( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox sensor class.""" + + await setup_integration(hass, entry) + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=200, + ) + mock_request.post( + "https://api.wall-box.com/chargers/config/12345", + json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, + status_code=404, + ) + + with pytest.raises(ConnectionError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) + await hass.config_entries.async_unload(entry.entry_id)