Wallbox Integration, Reduce API impact by limiting the amount of API calls made (#147618)

This commit is contained in:
Hessel 2025-06-30 20:15:48 +02:00 committed by GitHub
parent 511b739bf6
commit 90cbe272a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 88 deletions

View File

@ -22,6 +22,8 @@ CHARGER_CURRENT_MODE_KEY = "current_mode"
CHARGER_CURRENT_VERSION_KEY = "currentVersion"
CHARGER_CURRENCY_KEY = "currency"
CHARGER_DATA_KEY = "config_data"
CHARGER_DATA_POST_L1_KEY = "data"
CHARGER_DATA_POST_L2_KEY = "chargerData"
CHARGER_DEPOT_PRICE_KEY = "depot_price"
CHARGER_ENERGY_PRICE_KEY = "energy_price"
CHARGER_FEATURES_KEY = "features"
@ -32,7 +34,9 @@ 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_CHARGING_CURRENT_POST_KEY = "maxChargingCurrent"
CHARGER_MAX_ICP_CURRENT_KEY = "icp_max_current"
CHARGER_MAX_ICP_CURRENT_POST_KEY = "maxAvailableCurrent"
CHARGER_PAUSE_RESUME_KEY = "paused"
CHARGER_LOCKED_UNLOCKED_KEY = "locked"
CHARGER_NAME_KEY = "name"

View File

@ -14,11 +14,13 @@ from wallbox import Wallbox
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CHARGER_CURRENCY_KEY,
CHARGER_DATA_KEY,
CHARGER_DATA_POST_L1_KEY,
CHARGER_DATA_POST_L2_KEY,
CHARGER_ECO_SMART_KEY,
CHARGER_ECO_SMART_MODE_KEY,
CHARGER_ECO_SMART_STATUS_KEY,
@ -26,6 +28,7 @@ from .const import (
CHARGER_FEATURES_KEY,
CHARGER_LOCKED_UNLOCKED_KEY,
CHARGER_MAX_CHARGING_CURRENT_KEY,
CHARGER_MAX_CHARGING_CURRENT_POST_KEY,
CHARGER_MAX_ICP_CURRENT_KEY,
CHARGER_PLAN_KEY,
CHARGER_POWER_BOOST_KEY,
@ -192,10 +195,10 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
@ -204,10 +207,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return await self.hass.async_add_executor_job(self._get_data)
@_require_authentication
def _set_charging_current(self, charging_current: float) -> None:
def _set_charging_current(
self, charging_current: float
) -> dict[str, dict[str, dict[str, Any]]]:
"""Set maximum charging current for Wallbox."""
try:
self._wallbox.setMaxChargingCurrent(self._station, charging_current)
result = self._wallbox.setMaxChargingCurrent(
self._station, charging_current
)
data = self.data
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = result[CHARGER_DATA_POST_L1_KEY][
CHARGER_DATA_POST_L2_KEY
][CHARGER_MAX_CHARGING_CURRENT_POST_KEY]
return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
@ -221,16 +233,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def async_set_charging_current(self, charging_current: float) -> None:
"""Set maximum charging current for Wallbox."""
await self.hass.async_add_executor_job(
data = await self.hass.async_add_executor_job(
self._set_charging_current, charging_current
)
await self.async_request_refresh()
self.async_set_updated_data(data)
@_require_authentication
def _set_icp_current(self, icp_current: float) -> None:
def _set_icp_current(self, icp_current: float) -> dict[str, Any]:
"""Set maximum icp current for Wallbox."""
try:
self._wallbox.setIcpMaxCurrent(self._station, icp_current)
result = self._wallbox.setIcpMaxCurrent(self._station, icp_current)
data = self.data
data[CHARGER_MAX_ICP_CURRENT_KEY] = result[CHARGER_MAX_ICP_CURRENT_KEY]
return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
@ -244,14 +259,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
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()
data = await self.hass.async_add_executor_job(
self._set_icp_current, icp_current
)
self.async_set_updated_data(data)
@_require_authentication
def _set_energy_cost(self, energy_cost: float) -> None:
def _set_energy_cost(self, energy_cost: float) -> dict[str, Any]:
"""Set energy cost for Wallbox."""
try:
self._wallbox.setEnergyCost(self._station, energy_cost)
result = self._wallbox.setEnergyCost(self._station, energy_cost)
data = self.data
data[CHARGER_ENERGY_PRICE_KEY] = result[CHARGER_ENERGY_PRICE_KEY]
return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
@ -263,17 +283,24 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def async_set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox."""
await self.hass.async_add_executor_job(self._set_energy_cost, energy_cost)
await self.async_request_refresh()
data = await self.hass.async_add_executor_job(
self._set_energy_cost, energy_cost
)
self.async_set_updated_data(data)
@_require_authentication
def _set_lock_unlock(self, lock: bool) -> None:
def _set_lock_unlock(self, lock: bool) -> dict[str, dict[str, dict[str, Any]]]:
"""Set wallbox to locked or unlocked."""
try:
if lock:
self._wallbox.lockCharger(self._station)
result = self._wallbox.lockCharger(self._station)
else:
self._wallbox.unlockCharger(self._station)
result = self._wallbox.unlockCharger(self._station)
data = self.data
data[CHARGER_LOCKED_UNLOCKED_KEY] = result[CHARGER_DATA_POST_L1_KEY][
CHARGER_DATA_POST_L2_KEY
][CHARGER_LOCKED_UNLOCKED_KEY]
return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
@ -287,8 +314,8 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def async_set_lock_unlock(self, lock: bool) -> None:
"""Set wallbox to locked or unlocked."""
await self.hass.async_add_executor_job(self._set_lock_unlock, lock)
await self.async_request_refresh()
data = await self.hass.async_add_executor_job(self._set_lock_unlock, lock)
self.async_set_updated_data(data)
@_require_authentication
def _pause_charger(self, pause: bool) -> None:

View File

@ -7,7 +7,6 @@ from typing import Any
from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
@ -16,7 +15,7 @@ from .const import (
CHARGER_SERIAL_NUMBER_KEY,
DOMAIN,
)
from .coordinator import InvalidAuth, WallboxCoordinator
from .coordinator import WallboxCoordinator
from .entity import WallboxEntity
LOCK_TYPES: dict[str, LockEntityDescription] = {
@ -34,16 +33,6 @@ async def async_setup_entry(
) -> None:
"""Create wallbox lock entities in HASS."""
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
# Check if the user is authorized to lock, if so, add lock component
try:
await coordinator.async_set_lock_unlock(
coordinator.data[CHARGER_LOCKED_UNLOCKED_KEY]
)
except InvalidAuth:
return
except HomeAssistantError as exc:
raise PlatformNotReady from exc
async_add_entities(
WallboxLock(coordinator, description)
for ent in coordinator.data

View File

@ -12,7 +12,6 @@ from typing import cast
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
@ -26,7 +25,7 @@ from .const import (
CHARGER_SERIAL_NUMBER_KEY,
DOMAIN,
)
from .coordinator import InvalidAuth, WallboxCoordinator
from .coordinator import WallboxCoordinator
from .entity import WallboxEntity
@ -86,16 +85,6 @@ async def async_setup_entry(
) -> None:
"""Create wallbox number entities in HASS."""
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
# Check if the user has sufficient rights to change values, if so, add number component:
try:
await coordinator.async_set_charging_current(
coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY]
)
except InvalidAuth:
return
except HomeAssistantError as exc:
raise PlatformNotReady from exc
async_add_entities(
WallboxNumber(coordinator, entry, description)
for ent in coordinator.data

View File

@ -12,10 +12,10 @@ from homeassistant.exceptions import HomeAssistantError
from . import (
authorisation_response,
http_403_error,
http_404_error,
http_429_error,
setup_integration,
setup_integration_platform_not_ready,
setup_integration_read_only,
)
from .const import MOCK_LOCK_ENTITY_ID
@ -38,11 +38,15 @@ async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) -
),
patch(
"homeassistant.components.wallbox.Wallbox.lockCharger",
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
new=Mock(
return_value={"data": {"chargerData": {CHARGER_LOCKED_UNLOCKED_KEY: 1}}}
),
),
patch(
"homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
new=Mock(
return_value={"data": {"chargerData": {CHARGER_LOCKED_UNLOCKED_KEY: 0}}}
),
),
):
await hass.services.async_call(
@ -129,6 +133,52 @@ async def test_wallbox_lock_class_connection_error(
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"lock",
SERVICE_LOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.lockCharger",
new=Mock(side_effect=http_403_error),
),
patch(
"homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(side_effect=http_403_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"lock",
SERVICE_UNLOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.lockCharger",
new=Mock(side_effect=http_404_error),
),
patch(
"homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(side_effect=http_404_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"lock",
@ -138,27 +188,3 @@ async def test_wallbox_lock_class_connection_error(
},
blocking=True,
)
async def test_wallbox_lock_class_authentication_error(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox lock not loaded on authentication error."""
await setup_integration_read_only(hass, entry)
state = hass.states.get(MOCK_LOCK_ENTITY_ID)
assert state is None
async def test_wallbox_lock_class_platform_not_ready(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox lock not loaded on authentication error."""
await setup_integration_platform_not_ready(hass, entry)
state = hass.states.get(MOCK_LOCK_ENTITY_ID)
assert state is None

View File

@ -23,7 +23,6 @@ from . import (
http_429_error,
setup_integration,
setup_integration_bidir,
setup_integration_platform_not_ready,
)
from .const import (
MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
@ -56,7 +55,9 @@ async def test_wallbox_number_class(
),
patch(
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
new=Mock(return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}),
new=Mock(
return_value={"data": {"chargerData": {"maxChargingCurrent": 20}}}
),
),
):
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
@ -259,18 +260,6 @@ async def test_wallbox_number_class_energy_price_auth_error(
)
async def test_wallbox_number_class_platform_not_ready(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox lock not loaded on authentication error."""
await setup_integration_platform_not_ready(hass, entry)
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
assert state is None
async def test_wallbox_number_class_icp_energy(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
@ -285,7 +274,7 @@ async def test_wallbox_number_class_icp_energy(
),
patch(
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
new=Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}),
new=Mock(return_value={"icp_max_current": 20}),
),
):
await hass.services.async_call(
@ -328,6 +317,35 @@ async def test_wallbox_number_class_icp_energy_auth_error(
)
async def test_wallbox_number_class_energy_auth_error(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox sensor class."""
await setup_integration(hass, entry)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
new=Mock(side_effect=http_403_error),
),
pytest.raises(InvalidAuth),
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
ATTR_VALUE: 10,
},
blocking=True,
)
async def test_wallbox_number_class_icp_energy_connection_error(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:

View File

@ -50,6 +50,13 @@ async def test_wallbox_select_solar_charging_class(
) -> None:
"""Test wallbox select class."""
if mode == EcoSmartMode.OFF:
response = test_response
elif mode == EcoSmartMode.ECO_MODE:
response = test_response_eco_mode
elif mode == EcoSmartMode.FULL_SOLAR:
response = test_response_full_solar
with (
patch(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
@ -59,6 +66,10 @@ async def test_wallbox_select_solar_charging_class(
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(return_value=response),
),
):
await setup_integration_select(hass, entry, response)
@ -110,6 +121,10 @@ async def test_wallbox_select_class_error(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
new=Mock(side_effect=error),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(return_value=test_response_eco_mode),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
@ -144,6 +159,10 @@ async def test_wallbox_select_too_many_requests_error(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
new=Mock(side_effect=http_429_error),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(return_value=test_response_eco_mode),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(

View File

@ -10,7 +10,13 @@ from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import authorisation_response, http_404_error, http_429_error, setup_integration
from . import (
authorisation_response,
http_404_error,
http_429_error,
setup_integration,
test_response,
)
from .const import MOCK_SWITCH_ENTITY_ID
from tests.common import MockConfigEntry
@ -40,6 +46,10 @@ async def test_wallbox_switch_class(
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(return_value=test_response),
),
):
await hass.services.async_call(
"switch",