diff --git a/custom_components/alfen_wallbox/__init__.py b/custom_components/alfen_wallbox/__init__.py index 2aa2689..0b0692a 100644 --- a/custom_components/alfen_wallbox/__init__.py +++ b/custom_components/alfen_wallbox/__init__.py @@ -15,7 +15,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from .const import CONF_TRANSACTION_DATA, DEFAULT_SCAN_INTERVAL, DEFAULT_TIMEOUT +from .const import ( + CONF_REFRESH_CATEGORIES, + DEFAULT_REFRESH_CATEGORIES, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TIMEOUT, +) from .coordinator import AlfenConfigEntry, AlfenCoordinator, options_update_listener PLATFORMS = [ @@ -42,7 +47,7 @@ async def async_migrate_entry( options = { CONF_SCAN_INTERVAL: scan_interval, CONF_TIMEOUT: DEFAULT_TIMEOUT, - CONF_TRANSACTION_DATA: False, + CONF_REFRESH_CATEGORIES: DEFAULT_REFRESH_CATEGORIES, } data = { CONF_HOST: config_entry.data.get(CONF_HOST), diff --git a/custom_components/alfen_wallbox/alfen.py b/custom_components/alfen_wallbox/alfen.py index 8130d60..6ebc60f 100644 --- a/custom_components/alfen_wallbox/alfen.py +++ b/custom_components/alfen_wallbox/alfen.py @@ -1,5 +1,6 @@ """Alfen Wallbox API.""" +import datetime import json import logging import ssl @@ -10,17 +11,8 @@ from .const import ( ALFEN_PRODUCT_MAP, CAT, - CAT_COMM, - CAT_DISPLAY, - CAT_GENERIC, - CAT_GENERIC2, - CAT_MBUS_TCP, - CAT_METER1, - CAT_METER2, - CAT_METER4, - CAT_OCPP, - CAT_STATES, - CAT_TEMP, + CAT_TRANSACTIONS, + CATEGORIES, CMD, DEFAULT_TIMEOUT, DISPLAY_NAME_VALUE, @@ -57,7 +49,7 @@ def __init__( name: str, username: str, password: str, - get_transactions: bool, + category_options: list, ) -> None: """Init.""" @@ -66,6 +58,7 @@ def __init__( self._status = None self._session = session self.username = username + self.category_options = category_options self.info = None self.id = None if self.username is None: @@ -80,7 +73,8 @@ def __init__( self.latest_tag = None self.transaction_offset = 0 self.transaction_counter = 0 - self.get_transactions = get_transactions + self.static_properties = [] + self.get_static_properties = True # disable_warnings() @@ -124,6 +118,7 @@ async def get_info(self) -> bool: if response.status == 200: resp = await response.json(content_type=None) self.info = AlfenDeviceInfo(resp) + return True _LOGGER.debug("Info API not available, use generic info") @@ -155,15 +150,29 @@ def device_info(self) -> dict: async def async_update(self) -> bool: """Update the device properties.""" - if self.keep_logout: return True - result = await self._get_all_properties_value() - if not result: - return False - - if self.get_transactions: + dynamic_properties = [] + self.properties = [] + if self.get_static_properties: + self.static_properties = [] + + for cat in CATEGORIES: + if cat == CAT_TRANSACTIONS: + continue + if cat in self.category_options: + dynamic_properties = ( + dynamic_properties + await self._get_all_properties_value(cat) + ) + elif self.get_static_properties: + self.static_properties = ( + self.static_properties + await self._get_all_properties_value(cat) + ) + self.properties = self.static_properties + dynamic_properties + self.get_static_properties = False + + if CAT_TRANSACTIONS in self.category_options: if self.transaction_counter == 0: await self._get_transaction() self.transaction_counter += 1 @@ -296,48 +305,38 @@ async def _get_value(self, api_param): prop[VALUE] = resp[VALUE] break - async def _get_all_properties_value(self) -> bool: + async def _get_all_properties_value(self, category: str) -> list: """Get all properties from the API.""" _LOGGER.debug("Get properties") + properties = [] - for cat in ( - CAT_GENERIC, - CAT_GENERIC2, - CAT_METER1, - CAT_STATES, - CAT_TEMP, - CAT_OCPP, - CAT_METER4, - CAT_MBUS_TCP, - CAT_COMM, - CAT_DISPLAY, - CAT_METER2, - ): - nextRequest = True - offset = 0 - attempt = 0 - while nextRequest: - attempt += 1 - cmd = f"{PROP}?{CAT}={cat}&{OFFSET}={offset}" - response = await self._get(url=self.__get_url(cmd)) - _LOGGER.debug("Status Response %s: %s", cmd, str(response)) - - if response is not None: - attempt = 0 - properties += response[PROPERTIES] - nextRequest = response[TOTAL] > (offset + len(response[PROPERTIES])) - offset += len(response[PROPERTIES]) - elif attempt >= 3: - # This only possible in case of series of timeouts or unknown exceptions in self._get() - # It's better to break completely, otherwise we can provide partial data in self.properties. - _LOGGER.debug("Returning earlier after %s attempts", str(attempt)) - self.properties = [] - return False + tx_start = datetime.datetime.now() + nextRequest = True + offset = 0 + attempt = 0 + + while nextRequest: + attempt += 1 + cmd = f"{PROP}?{CAT}={category}&{OFFSET}={offset}" + response = await self._get(url=self.__get_url(cmd)) + _LOGGER.debug("Status Response %s: %s", cmd, str(response)) + + if response is not None: + attempt = 0 + properties += response[PROPERTIES] + nextRequest = response[TOTAL] > (offset + len(response[PROPERTIES])) + offset += len(response[PROPERTIES]) + elif attempt >= 3: + # This only possible in case of series of timeouts or unknown exceptions in self._get() + # It's better to break completely, otherwise we can provide partial data in self.properties. + _LOGGER.debug("Returning earlier after %s attempts", str(attempt)) + self.properties = [] + return False _LOGGER.debug("Properties %s", str(properties)) - self.properties = properties - - return True + runtime = datetime.datetime.now() - tx_start + _LOGGER.info("Called %s in %.2f seconds", category, runtime.total_seconds()) + return properties async def reboot_wallbox(self): """Reboot the wallbox.""" @@ -516,7 +515,7 @@ async def set_value(self, api_param, value): async def get_value(self, api_param): """Get a value from the API.""" - await self._get_value(api_param) + return await self._get_value(api_param) async def set_current_limit(self, limit) -> None: """Set the current limit.""" diff --git a/custom_components/alfen_wallbox/binary_sensor.py b/custom_components/alfen_wallbox/binary_sensor.py index be1113c..7005606 100644 --- a/custom_components/alfen_wallbox/binary_sensor.py +++ b/custom_components/alfen_wallbox/binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CAT, ID, LICENSE_HIGH_POWER, LICENSE_LOAD_BALANCING_ACTIVE, @@ -196,3 +197,11 @@ def is_on(self) -> bool: return False return self._attr_is_on + + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None diff --git a/custom_components/alfen_wallbox/button.py b/custom_components/alfen_wallbox/button.py index ceeddb2..3a1a3ce 100644 --- a/custom_components/alfen_wallbox/button.py +++ b/custom_components/alfen_wallbox/button.py @@ -97,6 +97,7 @@ def __init__( async def async_press(self) -> None: """Press the button.""" if self.entity_description.url_action == FORCE_UPDATE: + self.coordinator.device.get_static_properties = True await self.coordinator.device.async_update() return diff --git a/custom_components/alfen_wallbox/config_flow.py b/custom_components/alfen_wallbox/config_flow.py index 56cdd22..753af5b 100644 --- a/custom_components/alfen_wallbox/config_flow.py +++ b/custom_components/alfen_wallbox/config_flow.py @@ -1,6 +1,5 @@ """Config flow for the Alfen Wallbox platform.""" -import logging from typing import Any import voluptuous as vol @@ -22,15 +21,19 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from .const import CONF_TRANSACTION_DATA, DEFAULT_SCAN_INTERVAL, DEFAULT_TIMEOUT, DOMAIN - -_LOGGER = logging.getLogger(__name__) - +from .const import ( + CATEGORIES, + CONF_REFRESH_CATEGORIES, + DEFAULT_REFRESH_CATEGORIES, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TIMEOUT, + DOMAIN, +) DEFAULT_OPTIONS = { CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, CONF_TIMEOUT: DEFAULT_TIMEOUT, - CONF_TRANSACTION_DATA: False, + CONF_REFRESH_CATEGORIES: DEFAULT_REFRESH_CATEGORIES, } @@ -61,12 +64,12 @@ async def async_step_init( ), ): vol.All(vol.Coerce(int), vol.Range(min=1, max=30)), vol.Required( - CONF_TRANSACTION_DATA, + CONF_REFRESH_CATEGORIES, default=self.config_entry.options.get( - CONF_TRANSACTION_DATA, True + CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES ), - ): cv.boolean, - } + ): cv.multi_select(CATEGORIES), + }, ), ) @@ -77,7 +80,6 @@ class AlfenFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 2 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> AlfenOptionsFlowHandler: diff --git a/custom_components/alfen_wallbox/const.py b/custom_components/alfen_wallbox/const.py index f877ab8..9a62abe 100644 --- a/custom_components/alfen_wallbox/const.py +++ b/custom_components/alfen_wallbox/const.py @@ -27,23 +27,60 @@ DISPLAY_NAME_VALUE = "ha" +CAT_COMM = "comm" +CAT_DISPLAY = "display" CAT_GENERIC = "generic" CAT_GENERIC2 = "generic2" +CAT_MBUS_TCP = "MbusTCP" CAT_METER1 = "meter1" +CAT_METER2 = "meter2" CAT_METER4 = "meter4" +CAT_OCPP = "ocpp" CAT_STATES = "states" CAT_TEMP = "temp" -CAT_OCPP = "ocpp" -CAT_MBUS_TCP = "MbusTCP" -CAT_COMM = "comm" -CAT_DISPLAY = "display" # CAT_LEDS = "leds" # CAT_ACCELERO = "accelero" -CAT_METER2 = "meter2" +CAT_TRANSACTIONS = "transactions" COMMAND_REBOOT = "reboot" -CONF_TRANSACTION_DATA = "get_transaction_data" +CONF_REFRESH_CATEGORIES = "refresh_categories" + +DEFAULT_REFRESH_CATEGORIES = ( + CAT_DISPLAY, + CAT_METER1, + CAT_METER2, + CAT_METER4, + CAT_STATES, + CAT_TEMP, +) + +CATEGORIES = ( + CAT_COMM, + CAT_DISPLAY, + CAT_GENERIC, + CAT_GENERIC2, + CAT_MBUS_TCP, + CAT_METER1, + CAT_METER2, + CAT_METER4, + CAT_OCPP, + CAT_STATES, + CAT_TEMP, + CAT_TRANSACTIONS, +) + +# CONF_GENERIC = "get_generic" +# CONF_GENERIC2 = "get_generic2" +# CONF_METER1 = "get_meter1" +# CONF_METER2 = "get_meter2" +# CONF_METER4 = "get_meter4" +# CONF_STATES = "states" +# CONF_TEMP = "temp" +# CONF_OCPP = "ocpp" +# CONF_MBUSTCP = "MbusTCP" +# CONF_COMM = "comm" +# CONF_TRANSACTION_DATA = "display" DEFAULT_SCAN_INTERVAL = 5 DEFAULT_TIMEOUT = 20 diff --git a/custom_components/alfen_wallbox/coordinator.py b/custom_components/alfen_wallbox/coordinator.py index b5f67ed..5bccb13 100644 --- a/custom_components/alfen_wallbox/coordinator.py +++ b/custom_components/alfen_wallbox/coordinator.py @@ -1,31 +1,23 @@ """Class representing a Alfen Wallbox update coordinator.""" -import logging - -import async_timeout import asyncio - +import logging from datetime import timedelta +import async_timeout from aiohttp import ClientConnectionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TIMEOUT, - CONF_USERNAME, -) +from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PASSWORD, + CONF_SCAN_INTERVAL, CONF_TIMEOUT, + CONF_USERNAME) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator, + UpdateFailed) from .alfen import AlfenDevice -from .const import CONF_TRANSACTION_DATA, DEFAULT_SCAN_INTERVAL, DEFAULT_TIMEOUT, DOMAIN +from .const import (CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES, + DEFAULT_SCAN_INTERVAL, DEFAULT_TIMEOUT, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -61,7 +53,7 @@ async def _async_setup(self): self.entry.data[CONF_NAME], self.entry.data[CONF_USERNAME], self.entry.data[CONF_PASSWORD], - self.entry.options[CONF_TRANSACTION_DATA], + self.entry.options.get(CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES), ) if not await self.async_connect(): raise UpdateFailed("Error communicating with API") @@ -103,11 +95,12 @@ async def async_connect(self) -> bool: async def options_update_listener(self, entry: AlfenConfigEntry): """Handle options update.""" - self.coordinator = entry.runtime_data - self.coordinator.device.get_transactions = entry.options.get( - CONF_TRANSACTION_DATA, False + coordinator = entry.runtime_data + coordinator.device.get_static_properties = True + coordinator.device.category_options = entry.options.get( + CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES ) - self.coordinator.update_interval = timedelta( + coordinator.update_interval = timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) diff --git a/custom_components/alfen_wallbox/diagnostics.py b/custom_components/alfen_wallbox/diagnostics.py new file mode 100644 index 0000000..7f44f2b --- /dev/null +++ b/custom_components/alfen_wallbox/diagnostics.py @@ -0,0 +1,27 @@ +"""Diagnostics support for Alfen.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant + +from .coordinator import AlfenConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AlfenConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + device = entry.runtime_data.device + return { + "id": device.id, + "name": device.name, + "info": vars(device.info), + "keep_logout": device.keep_logout, + "max_allowed_phases": device.max_allowed_phases, + "number_socket": device.number_socket, + "licenses": device.licenses, + "category_options": device.category_options, + "properties": device.properties, + } diff --git a/custom_components/alfen_wallbox/number.py b/custom_components/alfen_wallbox/number.py index 2c6f3ea..8d8272d 100644 --- a/custom_components/alfen_wallbox/number.py +++ b/custom_components/alfen_wallbox/number.py @@ -5,19 +5,33 @@ from typing import Final import voluptuous as vol -from homeassistant.components.number import (NumberDeviceClass, NumberEntity, - NumberEntityDescription, - NumberMode) -from homeassistant.const import (CURRENCY_EURO, PERCENTAGE, - UnitOfElectricCurrent, UnitOfPower, - UnitOfTime) +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import ( + CURRENCY_EURO, + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfPower, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import (ID, LICENSE_HIGH_POWER, SERVICE_SET_COMFORT_POWER, - SERVICE_SET_CURRENT_LIMIT, SERVICE_SET_GREEN_SHARE, VALUE) +from .const import ( + CAT, + ID, + LICENSE_HIGH_POWER, + SERVICE_SET_COMFORT_POWER, + SERVICE_SET_CURRENT_LIMIT, + SERVICE_SET_GREEN_SHARE, + VALUE, +) from .coordinator import AlfenConfigEntry from .entity import AlfenEntity @@ -562,6 +576,14 @@ async def async_set_native_value(self, value: float) -> None: ) self._set_current_option() + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None + def _get_current_option(self) -> str | None: """Return the current option.""" for prop in self.coordinator.device.properties: diff --git a/custom_components/alfen_wallbox/select.py b/custom_components/alfen_wallbox/select.py index 2154c6b..ff7d94b 100644 --- a/custom_components/alfen_wallbox/select.py +++ b/custom_components/alfen_wallbox/select.py @@ -10,6 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CAT, ID, SERVICE_DISABLE_RFID_AUTHORIZATION_MODE, SERVICE_ENABLE_RFID_AUTHORIZATION_MODE, @@ -359,6 +360,14 @@ def current_option(self) -> str | None: value = self._get_current_option() return self.values_dict.get(value) + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None + def _get_current_option(self) -> str | None: """Return the current option.""" for prop in self.coordinator.device.properties: diff --git a/custom_components/alfen_wallbox/sensor.py b/custom_components/alfen_wallbox/sensor.py index 94ec76d..724e34b 100644 --- a/custom_components/alfen_wallbox/sensor.py +++ b/custom_components/alfen_wallbox/sensor.py @@ -1,6 +1,7 @@ """Support for Alfen Eve Single Proline Wallbox.""" import datetime + from dataclasses import dataclass from typing import Final @@ -27,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import ID, SERVICE_REBOOT_WALLBOX, VALUE +from .const import CAT, ID, SERVICE_REBOOT_WALLBOX, VALUE from .coordinator import AlfenConfigEntry from .entity import AlfenEntity @@ -1566,6 +1567,14 @@ def state(self): return "Unknown" + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None + async def async_reboot_wallbox(self): """Reboot the wallbox.""" await self.coordinator.device.reboot_wallbox() @@ -1963,6 +1972,14 @@ def state(self) -> StateType: return prop[VALUE] return None + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None + @property def unit_of_measurement(self) -> str: """Return the unit of measurement.""" diff --git a/custom_components/alfen_wallbox/strings.json b/custom_components/alfen_wallbox/strings.json index 29cc4f6..02d3fbc 100644 --- a/custom_components/alfen_wallbox/strings.json +++ b/custom_components/alfen_wallbox/strings.json @@ -24,7 +24,7 @@ "data": { "scan_interval": "Scan interval", "timeout": "Timeout", - "get_transaction_data": "Get transaction data." + "refresh_categories": "Select the categories to update periodically" } } } diff --git a/custom_components/alfen_wallbox/switch.py b/custom_components/alfen_wallbox/switch.py index 65027b0..96c86db 100644 --- a/custom_components/alfen_wallbox/switch.py +++ b/custom_components/alfen_wallbox/switch.py @@ -9,6 +9,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CAT, ID, SERVICE_DISABLE_PHASE_SWITCHING, SERVICE_ENABLE_PHASE_SWITCHING, @@ -135,6 +136,14 @@ def is_on(self) -> bool: return False + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" # Do the turning on. diff --git a/custom_components/alfen_wallbox/text.py b/custom_components/alfen_wallbox/text.py index 31f1e21..75c147c 100644 --- a/custom_components/alfen_wallbox/text.py +++ b/custom_components/alfen_wallbox/text.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ID +from .const import CAT, ID from .coordinator import AlfenConfigEntry from .entity import AlfenEntity @@ -112,3 +112,11 @@ async def async_set_value(self, value: str) -> None: self.entity_description.api_param, value ) self.async_write_ha_state() + + @property + def extra_state_attributes(self): + """Return the default attributes of the element.""" + for prop in self.coordinator.device.properties: + if prop[ID] == self.entity_description.api_param: + return {"category": prop[CAT]} + return None diff --git a/custom_components/alfen_wallbox/translations/en.json b/custom_components/alfen_wallbox/translations/en.json index cc575eb..181f49e 100644 --- a/custom_components/alfen_wallbox/translations/en.json +++ b/custom_components/alfen_wallbox/translations/en.json @@ -22,9 +22,9 @@ "step": { "init": { "data": { - "scan_interval": "Scan interval", + "scan_interval": "Vernieuw interval", "timeout": "Timeout", - "get_transaction_data": "Get transaction data." + "refresh_categories": "Select the categories to update periodically" } } } diff --git a/custom_components/alfen_wallbox/translations/nl.json b/custom_components/alfen_wallbox/translations/nl.json index 5a7b726..184fb06 100644 --- a/custom_components/alfen_wallbox/translations/nl.json +++ b/custom_components/alfen_wallbox/translations/nl.json @@ -25,7 +25,7 @@ "data": { "scan_interval": "Vernieuw interval", "timeout": "Timeout", - "get_transaction_data": "Haal transactiegegevens op" + "refresh_categories": "Selecteer de categoriƫn om periodiek te updaten" } } }