Add Switch platform to Wallbox (#70584)

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
pull/70835/head
hesselonline 2022-04-26 23:27:43 +02:00 committed by GitHub
parent 9303e35a7d
commit c973e5d0d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 307 additions and 32 deletions

View File

@ -32,44 +32,45 @@ from .const import (
CHARGER_STATUS_ID_KEY,
CONF_STATION,
DOMAIN,
ChargerStatus,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.LOCK]
PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.LOCK, Platform.SWITCH]
UPDATE_INTERVAL = 30
# Translation of StatusId based on Wallbox portal code:
# https://my.wallbox.com/src/utilities/charger/chargerStatuses.js
CHARGER_STATUS: dict[int, str] = {
0: "Disconnected",
14: "Error",
15: "Error",
161: "Ready",
162: "Ready",
163: "Disconnected",
164: "Waiting",
165: "Locked",
166: "Updating",
177: "Scheduled",
178: "Paused",
179: "Scheduled",
180: "Waiting for car demand",
181: "Waiting for car demand",
182: "Paused",
183: "Waiting in queue by Power Sharing",
184: "Waiting in queue by Power Sharing",
185: "Waiting in queue by Power Boost",
186: "Waiting in queue by Power Boost",
187: "Waiting MID failed",
188: "Waiting MID safety margin exceeded",
189: "Waiting in queue by Eco-Smart",
193: "Charging",
194: "Charging",
195: "Charging",
196: "Discharging",
209: "Locked",
210: "Locked",
CHARGER_STATUS: dict[int, ChargerStatus] = {
0: ChargerStatus.DISCONNECTED,
14: ChargerStatus.ERROR,
15: ChargerStatus.ERROR,
161: ChargerStatus.READY,
162: ChargerStatus.READY,
163: ChargerStatus.DISCONNECTED,
164: ChargerStatus.WAITING,
165: ChargerStatus.LOCKED,
166: ChargerStatus.UPDATING,
177: ChargerStatus.SCHEDULED,
178: ChargerStatus.PAUSED,
179: ChargerStatus.SCHEDULED,
180: ChargerStatus.WAITING_FOR_CAR,
181: ChargerStatus.WAITING_FOR_CAR,
182: ChargerStatus.PAUSED,
183: ChargerStatus.WAITING_IN_QUEUE_POWER_SHARING,
184: ChargerStatus.WAITING_IN_QUEUE_POWER_SHARING,
185: ChargerStatus.WAITING_IN_QUEUE_POWER_BOOST,
186: ChargerStatus.WAITING_IN_QUEUE_POWER_BOOST,
187: ChargerStatus.WAITING_MID_FAILED,
188: ChargerStatus.WAITING_MID_SAFETY,
189: ChargerStatus.WAITING_IN_QUEUE_ECO_SMART,
193: ChargerStatus.CHARGING,
194: ChargerStatus.CHARGING,
195: ChargerStatus.CHARGING,
196: ChargerStatus.DISCHARGING,
209: ChargerStatus.LOCKED,
210: ChargerStatus.LOCKED,
}
@ -122,7 +123,7 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
CHARGER_LOCKED_UNLOCKED_KEY
]
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], "Unknown"
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
)
return data
@ -169,6 +170,24 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
await self.hass.async_add_executor_job(self._set_lock_unlock, lock)
await self.async_request_refresh()
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
async def async_pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume."""
await self.hass.async_add_executor_job(self._pause_charger, pause)
await self.async_request_refresh()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Wallbox from a config entry."""

View File

@ -1,4 +1,5 @@
"""Constants for the Wallbox integration."""
from homeassistant.backports.enum import StrEnum
DOMAIN = "wallbox"
@ -18,9 +19,32 @@ CHARGER_PART_NUMBER_KEY = "part_number"
CHARGER_SOFTWARE_KEY = "software"
CHARGER_MAX_AVAILABLE_POWER_KEY = "max_available_power"
CHARGER_MAX_CHARGING_CURRENT_KEY = "max_charging_current"
CHARGER_PAUSE_RESUME_KEY = "paused"
CHARGER_LOCKED_UNLOCKED_KEY = "locked"
CHARGER_NAME_KEY = "name"
CHARGER_STATE_OF_CHARGE_KEY = "state_of_charge"
CHARGER_STATUS_ID_KEY = "status_id"
CHARGER_STATUS_DESCRIPTION_KEY = "status_description"
CHARGER_CONNECTIONS = "connections"
class ChargerStatus(StrEnum):
"""Charger Status Description."""
CHARGING = "Charging"
DISCHARGING = "Discharging"
PAUSED = "Paused"
SCHEDULED = "Scheduled"
WAITING_FOR_CAR = "Waiting for car demand"
WAITING = "Waiting"
DISCONNECTED = "Disconnected"
ERROR = "Error"
READY = "Ready"
LOCKED = "Locked"
UPDATING = "Updating"
WAITING_IN_QUEUE_POWER_SHARING = "Waiting in queue by Power Sharing"
WAITING_IN_QUEUE_POWER_BOOST = "Waiting in queue by Power Boost"
WAITING_MID_FAILED = "Waiting MID failed"
WAITING_MID_SAFETY = "Waiting MID safety margin exceeded"
WAITING_IN_QUEUE_ECO_SMART = "Waiting in queue by Eco-Smart"
UNKNOWN = "Unknown"

View File

@ -0,0 +1,78 @@
"""Home Assistant component for accessing the Wallbox Portal API. The switch component creates a switch entity."""
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import WallboxCoordinator, WallboxEntity
from .const import (
CHARGER_DATA_KEY,
CHARGER_PAUSE_RESUME_KEY,
CHARGER_SERIAL_NUMBER_KEY,
CHARGER_STATUS_DESCRIPTION_KEY,
DOMAIN,
ChargerStatus,
)
SWITCH_TYPES: dict[str, SwitchEntityDescription] = {
CHARGER_PAUSE_RESUME_KEY: SwitchEntityDescription(
key=CHARGER_PAUSE_RESUME_KEY,
name="Pause/Resume",
),
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Create wallbox sensor entities in HASS."""
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[WallboxSwitch(coordinator, entry, SWITCH_TYPES[CHARGER_PAUSE_RESUME_KEY])]
)
class WallboxSwitch(WallboxEntity, SwitchEntity):
"""Representation of the Wallbox portal."""
def __init__(
self,
coordinator: WallboxCoordinator,
entry: ConfigEntry,
description: SwitchEntityDescription,
) -> None:
"""Initialize a Wallbox switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_name = f"{entry.title} {description.name}"
self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}"
@property
def available(self) -> bool:
"""Return the availability of the switch."""
return self.coordinator.data[CHARGER_STATUS_DESCRIPTION_KEY] in {
ChargerStatus.CHARGING,
ChargerStatus.PAUSED,
ChargerStatus.SCHEDULED,
}
@property
def is_on(self) -> bool:
"""Return the status of pause/resume."""
return self.coordinator.data[CHARGER_STATUS_DESCRIPTION_KEY] in {
ChargerStatus.CHARGING,
ChargerStatus.WAITING_FOR_CAR,
ChargerStatus.WAITING,
}
async def async_turn_off(self, **kwargs: Any) -> None:
"""Pause charger."""
await self.coordinator.async_pause_charger(True)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Resume charger."""
await self.coordinator.async_pause_charger(False)

View File

@ -34,7 +34,7 @@ test_response = json.loads(
json.dumps(
{
CHARGER_CHARGING_POWER_KEY: 0,
CHARGER_STATUS_ID_KEY: 161,
CHARGER_STATUS_ID_KEY: 193,
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.2,
CHARGER_CHARGING_SPEED_KEY: 0,
CHARGER_ADDED_RANGE_KEY: 150,

View File

@ -10,3 +10,4 @@ MOCK_LOCK_ENTITY_ID = "lock.mock_title_locked_unlocked"
MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.mock_title_charging_speed"
MOCK_SENSOR_CHARGING_POWER_ID = "sensor.mock_title_charging_power"
MOCK_SENSOR_MAX_AVAILABLE_POWER = "sensor.mock_title_max_available_power"
MOCK_SWITCH_ENTITY_ID = "switch.mock_title_pause_resume"

View File

@ -0,0 +1,153 @@
"""Test Wallbox Lock component."""
import json
import pytest
import requests_mock
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.components.wallbox import InvalidAuth
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from tests.components.wallbox import entry, setup_integration
from tests.components.wallbox.const import (
ERROR,
JWT,
MOCK_SWITCH_ENTITY_ID,
STATUS,
TTL,
USER_ID,
)
authorisation_response = json.loads(
json.dumps(
{
JWT: "fakekeyhere",
USER_ID: 12345,
TTL: 145656758,
ERROR: "false",
STATUS: 200,
}
)
)
async def test_wallbox_switch_class(hass: HomeAssistant) -> None:
"""Test wallbox switch class."""
await setup_integration(hass)
state = hass.states.get(MOCK_SWITCH_ENTITY_ID)
assert state
assert state.state == "on"
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://api.wall-box.com/auth/token/user",
json=authorisation_response,
status_code=200,
)
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json=json.loads(json.dumps({CHARGER_STATUS_ID_KEY: 193})),
status_code=200,
)
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
await hass.config_entries.async_unload(entry.entry_id)
async def test_wallbox_switch_class_connection_error(hass: HomeAssistant) -> None:
"""Test wallbox switch class connection error."""
await setup_integration(hass)
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://api.wall-box.com/auth/token/user",
json=authorisation_response,
status_code=200,
)
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json=json.loads(json.dumps({CHARGER_STATUS_ID_KEY: 193})),
status_code=404,
)
with pytest.raises(ConnectionError):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
with pytest.raises(ConnectionError):
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
await hass.config_entries.async_unload(entry.entry_id)
async def test_wallbox_switch_class_authentication_error(hass: HomeAssistant) -> None:
"""Test wallbox switch class connection error."""
await setup_integration(hass)
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://api.wall-box.com/auth/token/user",
json=authorisation_response,
status_code=200,
)
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json=json.loads(json.dumps({CHARGER_STATUS_ID_KEY: 193})),
status_code=403,
)
with pytest.raises(InvalidAuth):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
with pytest.raises(InvalidAuth):
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
await hass.config_entries.async_unload(entry.entry_id)