mirror of
https://github.com/home-assistant/core.git
synced 2025-04-27 10:47:51 +00:00
Add energy price number entities to Wallbox (#101840)
This commit is contained in:
parent
e5b5858915
commit
bd0df2f18f
@ -150,6 +150,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
await self.async_request_refresh()
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
def _set_lock_unlock(self, lock: bool) -> None:
|
||||
"""Set wallbox to locked or unlocked."""
|
||||
try:
|
||||
|
@ -4,6 +4,7 @@ The number component allows control of charging current.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import (
|
||||
BIDIRECTIONAL_MODEL_PREFIXES,
|
||||
CHARGER_DATA_KEY,
|
||||
CHARGER_ENERGY_PRICE_KEY,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
CHARGER_PART_NUMBER_KEY,
|
||||
@ -26,8 +28,29 @@ from .coordinator import InvalidAuth, WallboxCoordinator
|
||||
from .entity import WallboxEntity
|
||||
|
||||
|
||||
def min_charging_current_value(coordinator: WallboxCoordinator) -> float:
|
||||
"""Return the minimum available value for charging current."""
|
||||
if (
|
||||
coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:2]
|
||||
in BIDIRECTIONAL_MODEL_PREFIXES
|
||||
):
|
||||
return cast(float, (coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY] * -1))
|
||||
return 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class WallboxNumberEntityDescription(NumberEntityDescription):
|
||||
class WallboxNumberEntityDescriptionMixin:
|
||||
"""Load entities from different handlers."""
|
||||
|
||||
max_value_fn: Callable[[WallboxCoordinator], float]
|
||||
min_value_fn: Callable[[WallboxCoordinator], float]
|
||||
set_value_fn: Callable[[WallboxCoordinator], Callable[[float], Awaitable[None]]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WallboxNumberEntityDescription(
|
||||
NumberEntityDescription, WallboxNumberEntityDescriptionMixin
|
||||
):
|
||||
"""Describes Wallbox number entity."""
|
||||
|
||||
|
||||
@ -35,6 +58,20 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription(
|
||||
key=CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
translation_key="maximum_charging_current",
|
||||
max_value_fn=lambda coordinator: cast(
|
||||
float, coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]
|
||||
),
|
||||
min_value_fn=min_charging_current_value,
|
||||
set_value_fn=lambda coordinator: coordinator.async_set_charging_current,
|
||||
native_step=1,
|
||||
),
|
||||
CHARGER_ENERGY_PRICE_KEY: WallboxNumberEntityDescription(
|
||||
key=CHARGER_ENERGY_PRICE_KEY,
|
||||
translation_key="energy_price",
|
||||
max_value_fn=lambda _: 5,
|
||||
min_value_fn=lambda _: -5,
|
||||
set_value_fn=lambda coordinator: coordinator.async_set_energy_cost,
|
||||
native_step=0.01,
|
||||
),
|
||||
}
|
||||
|
||||
@ -44,7 +81,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Create wallbox number entities in HASS."""
|
||||
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
# Check if the user is authorized to change current, if so, add number component:
|
||||
# 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]
|
||||
@ -79,28 +116,22 @@ class WallboxNumber(WallboxEntity, NumberEntity):
|
||||
self.entity_description = description
|
||||
self._coordinator = coordinator
|
||||
self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}"
|
||||
self._is_bidirectional = (
|
||||
coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:2]
|
||||
in BIDIRECTIONAL_MODEL_PREFIXES
|
||||
)
|
||||
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum available current."""
|
||||
return cast(float, self._coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY])
|
||||
"""Return the maximum available value."""
|
||||
return self.entity_description.max_value_fn(self.coordinator)
|
||||
|
||||
@property
|
||||
def native_min_value(self) -> float:
|
||||
"""Return the minimum available current based on charger type - some chargers can discharge."""
|
||||
return (self.max_value * -1) if self._is_bidirectional else 6
|
||||
"""Return the minimum available value."""
|
||||
return self.entity_description.min_value_fn(self.coordinator)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the value of the entity."""
|
||||
return cast(
|
||||
float | None, self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY]
|
||||
)
|
||||
return cast(float | None, self._coordinator.data[self.entity_description.key])
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the value of the entity."""
|
||||
await self._coordinator.async_set_charging_current(value)
|
||||
await self.entity_description.set_value_fn(self.coordinator)(value)
|
||||
|
@ -35,6 +35,9 @@
|
||||
"number": {
|
||||
"maximum_charging_current": {
|
||||
"name": "Maximum charging current"
|
||||
},
|
||||
"energy_price": {
|
||||
"name": "Energy price"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
@ -34,7 +34,7 @@ test_response = json.loads(
|
||||
{
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.2,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
@ -52,6 +52,26 @@ test_response = json.loads(
|
||||
)
|
||||
)
|
||||
|
||||
test_response_bidir = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
authorisation_response = json.loads(
|
||||
json.dumps(
|
||||
{
|
||||
@ -109,6 +129,29 @@ async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_bidir(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response_bidir,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json=json.loads(json.dumps({CHARGER_MAX_CHARGING_CURRENT_KEY: 20})),
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
|
@ -6,6 +6,7 @@ ERROR = "error"
|
||||
STATUS = "status"
|
||||
|
||||
MOCK_NUMBER_ENTITY_ID = "number.wallbox_wallboxname_maximum_charging_current"
|
||||
MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID = "number.wallbox_wallboxname_energy_price"
|
||||
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"
|
||||
|
@ -5,16 +5,21 @@ import pytest
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE
|
||||
from homeassistant.components.wallbox.const import CHARGER_MAX_CHARGING_CURRENT_KEY
|
||||
from homeassistant.components.wallbox.const import (
|
||||
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 . import (
|
||||
authorisation_response,
|
||||
setup_integration,
|
||||
setup_integration_bidir,
|
||||
setup_integration_platform_not_ready,
|
||||
)
|
||||
from .const import MOCK_NUMBER_ENTITY_ID
|
||||
from .const import MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID, MOCK_NUMBER_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -37,6 +42,9 @@ async def test_wallbox_number_class(
|
||||
json=json.loads(json.dumps({CHARGER_MAX_CHARGING_CURRENT_KEY: 20})),
|
||||
status_code=200,
|
||||
)
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == 0
|
||||
assert state.attributes["max"] == 25
|
||||
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
@ -50,6 +58,51 @@ async def test_wallbox_number_class(
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_bidir(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration_bidir(hass, entry)
|
||||
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == -25
|
||||
assert state.attributes["max"] == 25
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_energy_class(
|
||||
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=json.loads(json.dumps({CHARGER_ENERGY_PRICE_KEY: 1.1})),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
@ -82,6 +135,70 @@ async def test_wallbox_number_class_connection_error(
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_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=json.loads(json.dumps({CHARGER_ENERGY_PRICE_KEY: 1.1})),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
with pytest.raises(ConnectionError):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_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=json.loads(json.dumps({CHARGER_ENERGY_PRICE_KEY: 1.1})),
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidAuth):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_platform_not_ready(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
|
Loading…
x
Reference in New Issue
Block a user