diff --git a/CODEOWNERS b/CODEOWNERS index e7f20d27e18..3d7a53749cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -544,6 +544,7 @@ homeassistant/components/vlc_telnet/* @rodripf @dmcc homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/wake_on_lan/* @ntilley905 +homeassistant/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py new file mode 100644 index 00000000000..97b2ea12f35 --- /dev/null +++ b/homeassistant/components/wallbox/__init__.py @@ -0,0 +1,147 @@ +"""The Wallbox integration.""" +import asyncio +from datetime import timedelta +import logging + +import requests +from wallbox import Wallbox + +from homeassistant import exceptions +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, CONF_ROUND, CONF_SENSOR_TYPES, CONF_STATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +UPDATE_INTERVAL = 30 + + +class WallboxHub: + """Wallbox Hub class.""" + + def __init__(self, station, username, password, hass): + """Initialize.""" + self._station = station + self._username = username + self._password = password + self._wallbox = Wallbox(self._username, self._password) + self._hass = hass + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="wallbox", + update_method=self.async_get_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + def _authenticate(self): + """Authenticate using Wallbox API.""" + try: + self._wallbox.authenticate() + return True + 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 + + def _get_data(self): + """Get new sensor data for Wallbox component.""" + try: + self._authenticate() + data = self._wallbox.getChargerStatus(self._station) + + filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} + + for key, value in filtered_data.items(): + sensor_round = CONF_SENSOR_TYPES[key][CONF_ROUND] + if sensor_round: + try: + filtered_data[key] = round(value, sensor_round) + except TypeError: + _LOGGER.debug("Cannot format %s", key) + + return filtered_data + except requests.exceptions.HTTPError as wallbox_connection_error: + raise ConnectionError from wallbox_connection_error + + async def async_coordinator_first_refresh(self): + """Refresh coordinator for the first time.""" + await self._coordinator.async_config_entry_first_refresh() + + async def async_authenticate(self) -> bool: + """Authenticate using Wallbox API.""" + return await self._hass.async_add_executor_job(self._authenticate) + + async def async_get_data(self) -> bool: + """Get new sensor data for Wallbox component.""" + data = await self._hass.async_add_executor_job(self._get_data) + return data + + @property + def coordinator(self): + """Return the coordinator.""" + return self._coordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Wallbox from a config entry.""" + wallbox = WallboxHub( + entry.data[CONF_STATION], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + hass, + ) + + await wallbox.async_authenticate() + + await wallbox.async_coordinator_first_refresh() + + hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}}) + hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = wallbox + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN]["connections"].pop(entry.entry_id) + + return unload_ok + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot connect to Wallbox API. %s", msg) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot authenticate with Wallbox API. %s", msg) diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py new file mode 100644 index 00000000000..69b01d96c40 --- /dev/null +++ b/homeassistant/components/wallbox/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Wallbox integration.""" +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from . import CannotConnect, InvalidAuth, WallboxHub +from .const import CONF_STATION, DOMAIN + +COMPONENT_DOMAIN = DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = WallboxHub(data["station"], data["username"], data["password"], hass) + + await hub.async_get_data() + + # Return info that you want to store in the config entry. + return {"title": "Wallbox Portal"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): + """Handle a config flow for Wallbox.""" + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py new file mode 100644 index 00000000000..41996107ce0 --- /dev/null +++ b/homeassistant/components/wallbox/const.py @@ -0,0 +1,99 @@ +"""Constants for the Wallbox integration.""" +from homeassistant.const import ( + CONF_ICON, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + STATE_UNAVAILABLE, +) + +DOMAIN = "wallbox" + +CONF_STATION = "station" + +CONF_CONNECTIONS = "connections" +CONF_ROUND = "round" + +CONF_SENSOR_TYPES = { + "charging_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Charging Power", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + STATE_UNAVAILABLE: False, + }, + "max_available_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Max Available Power", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + STATE_UNAVAILABLE: False, + }, + "charging_speed": { + CONF_ICON: "mdi:speedometer", + CONF_NAME: "Charging Speed", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "added_range": { + CONF_ICON: "mdi:map-marker-distance", + CONF_NAME: "Added Range", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + STATE_UNAVAILABLE: False, + }, + "added_energy": { + CONF_ICON: "mdi:battery-positive", + CONF_NAME: "Added Energy", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + STATE_UNAVAILABLE: False, + }, + "charging_time": { + CONF_ICON: "mdi:timer", + CONF_NAME: "Charging Time", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "cost": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Cost", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "state_of_charge": { + CONF_ICON: "mdi:battery-charging-80", + CONF_NAME: "State of Charge", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, + STATE_UNAVAILABLE: False, + }, + "current_mode": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Current Mode", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "depot_price": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Depot Price", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "status_description": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Status Description", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, +} diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json new file mode 100644 index 00000000000..aeadf541345 --- /dev/null +++ b/homeassistant/components/wallbox/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "wallbox", + "name": "Wallbox", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wallbox", + "requirements": ["wallbox==0.4.4"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@hesselonline"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py new file mode 100644 index 00000000000..6d3ef952cbe --- /dev/null +++ b/homeassistant/components/wallbox/sensor.py @@ -0,0 +1,61 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + CONF_CONNECTIONS, + CONF_ICON, + CONF_NAME, + CONF_SENSOR_TYPES, + CONF_UNIT_OF_MEASUREMENT, + DOMAIN, +) + +CONF_STATION = "station" +UPDATE_INTERVAL = 30 + + +async def async_setup_entry(hass, config, async_add_entities): + """Create wallbox sensor entities in HASS.""" + wallbox = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] + + coordinator = wallbox.coordinator + + async_add_entities( + WallboxSensor(coordinator, idx, ent, config) + for idx, ent in enumerate(coordinator.data) + ) + + +class WallboxSensor(CoordinatorEntity, Entity): + """Representation of the Wallbox portal.""" + + def __init__(self, coordinator, idx, ent, config): + """Initialize a Wallbox sensor.""" + super().__init__(coordinator) + self._properties = CONF_SENSOR_TYPES[ent] + self._name = f"{config.title} {self._properties[CONF_NAME]}" + self._icon = self._properties[CONF_ICON] + self._unit = self._properties[CONF_UNIT_OF_MEASUREMENT] + self._ent = ent + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._ent] + + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json new file mode 100644 index 00000000000..63fc5d89e85 --- /dev/null +++ b/homeassistant/components/wallbox/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Wallbox", + "config": { + "step": { + "user": { + "data": { + "station": "Station Serial Number", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json new file mode 100644 index 00000000000..a63fe801490 --- /dev/null +++ b/homeassistant/components/wallbox/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "station": "Station S/N", + "password": "Password", + "username": "Username" + } + } + } + }, + "title": "MyWallbox" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3a28a24315b..d927b244ede 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -275,6 +275,7 @@ FLOWS = [ "vilfo", "vizio", "volumio", + "wallbox", "waze_travel_time", "wemo", "wiffi", diff --git a/requirements_all.txt b/requirements_all.txt index 156b75bb8a2..63286d2e081 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,6 +2332,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac6bcea193f..91618f263fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1256,6 +1256,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.folder_watcher watchdog==2.1.2 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py new file mode 100644 index 00000000000..35bf3cee242 --- /dev/null +++ b/tests/components/wallbox/__init__.py @@ -0,0 +1 @@ +"""Tests for the Wallbox integration.""" diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py new file mode 100644 index 00000000000..074f67abe2c --- /dev/null +++ b/tests/components/wallbox/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Wallbox config flow.""" +from unittest.mock import patch + +from voluptuous.schema_builder import raises + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.wallbox import CannotConnect, InvalidAuth, config_flow +from homeassistant.components.wallbox.const import DOMAIN +from homeassistant.core import HomeAssistant + + +async def test_show_set_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + flow = config_flow.ConfigFlow() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_validate_input(hass): + """Test we can validate input.""" + data = { + "station": "12345", + "username": "test-username", + "password": "test-password", + } + + def alternate_authenticate_method(): + return None + + def alternate_get_charger_status_method(station): + data = '{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}' + return data + + with patch( + "wallbox.Wallbox.authenticate", + side_effect=alternate_authenticate_method, + ), patch( + "wallbox.Wallbox.getChargerStatus", + side_effect=alternate_get_charger_status_method, + ): + + result = await config_flow.validate_input(hass, data) + + assert result == {"title": "Wallbox Portal"} + + +async def test_configflow_class(): + """Test configFlow class.""" + configflow = config_flow.ConfigFlow() + assert configflow + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=TypeError, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=CannotConnect, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + ), raises(Exception): + assert await configflow.async_step_user(True) + + +def test_cannot_connect_class(): + """Test cannot Connect class.""" + cannot_connect = CannotConnect + assert cannot_connect + + +def test_invalid_auth_class(): + """Test invalid auth class.""" + invalid_auth = InvalidAuth + assert invalid_auth diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py new file mode 100644 index 00000000000..892e77dc7f6 --- /dev/null +++ b/tests/components/wallbox/test_init.py @@ -0,0 +1,165 @@ +"""Test Wallbox Init Component.""" +import json + +import pytest +import requests_mock +from voluptuous.schema_builder import raises + +from homeassistant.components import wallbox +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + + +async def test_wallbox_setup_entry(hass: HomeAssistantType): + """Test Wallbox Setup.""" + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=200, + ) + assert await wallbox.async_setup_entry(hass, entry) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":404}', + status_code=404, + ) + assert await wallbox.async_setup_entry(hass, entry) is False + + +async def test_wallbox_unload_entry(hass: HomeAssistantType): + """Test Wallbox Unload.""" + hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} + + assert await wallbox.async_unload_entry(hass, entry) + + hass.data[DOMAIN] = {"fail_entry": entry} + + with pytest.raises(KeyError): + await wallbox.async_unload_entry(hass, entry) + + +async def test_get_data(hass: HomeAssistantType): + """Test hub class, get_data.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_get_data_rounding_error(hass: HomeAssistantType): + """Test hub class, get_data with rounding error.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response_rounding_error, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_authentication_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + m.get( + "https://api.wall-box.com/chargers/status/test", + json=test_response, + status_code=403, + ) + assert await hub.async_get_data() + + +async def test_get_data_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + text="data", + status_code=404, + ) + assert await hub.async_get_data() diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py new file mode 100644 index 00000000000..5c0c3511a30 --- /dev/null +++ b/tests/components/wallbox/test_sensor.py @@ -0,0 +1,81 @@ +"""Test Wallbox Switch component.""" + +import json +from unittest.mock import MagicMock + +from homeassistant.components.wallbox import sensor +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + +CONF_STATION = ("12345",) +CONF_USERNAME = ("test-username",) +CONF_PASSWORD = "test-password" + +# wallbox = WallboxHub(CONF_STATION, CONF_USERNAME, CONF_PASSWORD, hass) + + +async def test_wallbox_sensor_class(): + """Test wallbox sensor class.""" + + coordinator = MagicMock(return_value="connected") + idx = 1 + ent = "charging_power" + + wallboxSensor = sensor.WallboxSensor(coordinator, idx, ent, entry) + + assert wallboxSensor.icon == "mdi:ev-station" + assert wallboxSensor.unit_of_measurement == "kW" + assert wallboxSensor.name == "Mock Title Charging Power" + assert wallboxSensor.state + + +# async def test_wallbox_updater(hass: HomeAssistantType): +# """Test wallbox updater.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass) + + +# async def test_wallbox_updater_rounding_error(hass: HomeAssistantType): +# """Test wallbox updater rounding error.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response_rounding_error, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass)