Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wallbox Add Authentication Decorator #102520

Merged
merged 8 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 60 additions & 56 deletions homeassistant/components/wallbox/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"""DataUpdateCoordinator for the wallbox integration."""
from __future__ import annotations

from collections.abc import Callable
from datetime import timedelta
from http import HTTPStatus
import logging
from typing import Any
from typing import Any, Concatenate, ParamSpec, TypeVar

import requests
from wallbox import Wallbox

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import (
CHARGER_CURRENCY_KEY,
Expand Down Expand Up @@ -62,6 +63,29 @@
210: ChargerStatus.LOCKED_CAR_CONNECTED,
}

_WallboxCoordinatorT = TypeVar("_WallboxCoordinatorT", bound="WallboxCoordinator")
_P = ParamSpec("_P")


def _require_authentication(
func: Callable[Concatenate[_WallboxCoordinatorT, _P], Any]
) -> Callable[Concatenate[_WallboxCoordinatorT, _P], Any]:
"""Authenticate with decorator using Wallbox API."""

def require_authentication(
self: _WallboxCoordinatorT, *args: _P.args, **kwargs: _P.kwargs
) -> Any:
"""Authenticate using Wallbox API."""
try:
self.authenticate()
return func(self, *args, **kwargs)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
raise ConfigEntryAuthFailed from wallbox_connection_error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we start a reauth flow whenever we fail authentication? If this is raised as part of a service call currently no reauth flow is started. It's only _async_update_data that will automatically handle ConfigEntryAuthFailed and start a reauth flow.

raise ConnectionError from wallbox_connection_error

return require_authentication


class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Wallbox Coordinator class."""
Expand All @@ -78,15 +102,9 @@ def __init__(self, station: str, wallbox: Wallbox, hass: HomeAssistant) -> None:
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)

def _authenticate(self) -> None:
def authenticate(self) -> None:
"""Authenticate using Wallbox API."""
try:
self._wallbox.authenticate()

except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
raise ConfigEntryAuthFailed from wallbox_connection_error
raise ConnectionError from wallbox_connection_error
self._wallbox.authenticate()

def _validate(self) -> None:
"""Authenticate using Wallbox API."""
Expand All @@ -101,47 +119,41 @@ async def async_validate_input(self) -> None:
"""Get new sensor data for Wallbox component."""
await self.hass.async_add_executor_job(self._validate)

@_require_authentication
def _get_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component."""
try:
self._authenticate()
edenhaus marked this conversation as resolved.
Show resolved Hide resolved
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
CHARGER_MAX_CHARGING_CURRENT_KEY
]
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
CHARGER_LOCKED_UNLOCKED_KEY
]
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
CHARGER_ENERGY_PRICE_KEY
]
data[
CHARGER_CURRENCY_KEY
] = f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"

data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
)
return data
except (
ConnectionError,
requests.exceptions.HTTPError,
) as wallbox_connection_error:
raise UpdateFailed from wallbox_connection_error
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
CHARGER_MAX_CHARGING_CURRENT_KEY
]
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
CHARGER_LOCKED_UNLOCKED_KEY
]
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
CHARGER_ENERGY_PRICE_KEY
]
data[
CHARGER_CURRENCY_KEY
] = f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"

data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
)
return data

async def _async_update_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component."""
return await self.hass.async_add_executor_job(self._get_data)

@_require_authentication
edenhaus marked this conversation as resolved.
Show resolved Hide resolved
def _set_charging_current(self, charging_current: float) -> None:
"""Set maximum charging current for Wallbox."""
try:
self._authenticate()
self._wallbox.setMaxChargingCurrent(self._station, charging_current)
except requests.exceptions.HTTPError as wallbox_connection_error:
edenhaus marked this conversation as resolved.
Show resolved Hide resolved
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
raise ConnectionError from wallbox_connection_error
raise wallbox_connection_error

async def async_set_charging_current(self, charging_current: float) -> None:
"""Set maximum charging current for Wallbox."""
Expand All @@ -150,51 +162,43 @@ async def async_set_charging_current(self, charging_current: float) -> None:
)
await self.async_request_refresh()

@_require_authentication
def _set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox."""
try:
self._authenticate()
self._wallbox.setEnergyCost(self._station, energy_cost)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
raise ConnectionError from wallbox_connection_error

self._wallbox.setEnergyCost(self._station, energy_cost)

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()

@_require_authentication
def _set_lock_unlock(self, lock: bool) -> None:
"""Set wallbox to locked or unlocked."""
try:
self._authenticate()
if lock:
self._wallbox.lockCharger(self._station)
else:
self._wallbox.unlockCharger(self._station)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
raise ConnectionError from wallbox_connection_error
raise wallbox_connection_error

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()

@_require_authentication
def _pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume."""
try:
self._authenticate()
if pause:
self._wallbox.pauseChargingSession(self._station)
else:
self._wallbox.resumeChargingSession(self._station)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error
raise ConnectionError from wallbox_connection_error

if pause:
self._wallbox.pauseChargingSession(self._station)
else:
self._wallbox.resumeChargingSession(self._station)

async def async_pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume."""
Expand Down
4 changes: 2 additions & 2 deletions tests/components/wallbox/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
CHARGER_ENERGY_PRICE_KEY,
CHARGER_MAX_CHARGING_CURRENT_KEY,
)
from homeassistant.components.wallbox.coordinator import InvalidAuth
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed

from . import (
authorisation_response,
Expand Down Expand Up @@ -186,7 +186,7 @@ async def test_wallbox_number_class_energy_price_auth_error(
status_code=403,
)

with pytest.raises(InvalidAuth):
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
Expand Down
6 changes: 3 additions & 3 deletions tests/components/wallbox/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
from homeassistant.components.wallbox.coordinator import InvalidAuth
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed

from . import authorisation_response, setup_integration
from .const import MOCK_SWITCH_ENTITY_ID
Expand Down Expand Up @@ -120,7 +120,7 @@ async def test_wallbox_switch_class_authentication_error(
status_code=403,
)

with pytest.raises(InvalidAuth):
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
Expand All @@ -129,7 +129,7 @@ async def test_wallbox_switch_class_authentication_error(
},
blocking=True,
)
with pytest.raises(InvalidAuth):
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
Expand Down