├── .gitignore ├── LICENSE ├── README.md ├── custom_components └── alfen_wallbox │ ├── __init__.py │ ├── alfen.py │ ├── binary_sensor.py │ ├── button.py │ ├── config_flow.py │ ├── const.py │ ├── coordinator.py │ ├── diagnostics.py │ ├── entity.py │ ├── manifest.json │ ├── number.py │ ├── select.py │ ├── sensor.py │ ├── services.yaml │ ├── strings.json │ ├── switch.py │ ├── text.py │ └── translations │ ├── en.json │ └── nl.json ├── doc ├── alfen_props.md └── screenshots │ ├── Screen Shot 2022-06-01 at 13.34.44.png │ ├── attribute category.png │ ├── categories.png │ ├── configure.png │ ├── wallbox-1.png │ ├── wallbox-2.png │ └── wallbox-3.png ├── hacs.json └── info.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jimmy Everling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Alfen Wallbox - HomeAssistant Integration 3 | 4 | This is a custom component to allow control of Alfen Wallboxes in [HomeAssistant](https://home-assistant.io). 5 | 6 | The component is a fork of the [Garo Wallbox custom integration](https://github.com/sockless-coding/garo_wallbox) and [egnerfl custom integration](https://github.com/egnerfl/alfen_wallbox) 7 | 8 | > After reverse engineering the API myself I found out that there is already a Python libary wrapping the Alfen API. 9 | > https://gitlab.com/LordGaav/alfen-eve/-/tree/develop/alfeneve 10 | > 11 | > https://github.com/leeyuentuen/alfen_wallbox/wiki/API-paramID 12 | 13 | ## Installation 14 | 15 | ### Install using HACS (recommended) 16 | If you do not have HACS installed yet visit https://hacs.xyz for installation instructions. 17 | 18 | To add the this repository to HACS in your Home Assistant instance, use this My button: 19 | 20 | [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?repository=alfen_wallbox&owner=leeyuentuen&category=Integration) 21 | 22 | After installation, please reboot and add Alfen Wallbox device to your Home Assistant instance, use this My button: 23 | 24 | [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=alfen_wallbox) 25 | 26 |
27 | Manual configuration steps 28 | 29 | > - In HACS, go to the Integrations section and add the custom repository via the 3 dot menu on the top right. Enter ```https://github.com/>> leeyuentuen/alfen_wallbox``` in the Repository field, choose the ```Integration``` category, then click add. 30 | Hit the big + at the bottom right and search for **Alfen Wallbox**. Click it, then click the download button. 31 | > - Clone or copy this repository and copy the folder 'custom_components/alfen_wallbox' into '/custom_components/alfen_wallbox' 32 | > - Once installed the Alfen Wallbox integration can be configured via the Home Assistant integration interface 33 | where you can enter the IP address of the device. 34 |
35 | 36 | ### Home Assistant Energy Dashboard 37 | The wallbox can be added to the Home Assistant Energy Dashboard using the `_meter_reading` sensor. 38 | 39 | ## Settings 40 | The wallbox can be configured using the Integrations settings menu: 41 | 42 | drawing 43 | 44 | Categories can be configured to refresh at each specified update interval. Categories that are not selected will only load when the integration starts. The exception to this rule is the `transactions` category, which will load only if explicitly selected. 45 | 46 | To locate a category, start by selecting all categories. Allow the integration to load, then find the desired entity. The category will be displayed in the entity's attributes. 47 | 48 | drawing 49 | 50 | Reducing the number of selected categories will enhance the integration's update speed. 51 | 52 | ## Simultaneous Use of the App and Integration 53 | The Alfen charger allows only one active login session at a time. This means the Alfen MyEve or Eve Connect app cannot be used concurrently with the Home Assistant integration. 54 | 55 | To manage this, the integration includes two buttons: HTTPS API Login and HTTPS API Logout. 56 | 57 | - To switch to the Alfen app: Click the Logout button in the Home Assistant integration, then use your preferred Alfen app. 58 | - To return to the integration: Click the Login button to reconnect the Home Assistant integration. 59 | 60 | The HTTPS API Login Status binary sensor shows the current state of the login session. 61 | 62 | ## Services 63 | Example of running in Services: 64 | Note; The name of the configured charging point is "wallbox" in these examples. 65 | 66 | ### - Changing Green Share % 67 | ``` 68 | service: alfen_wallbox.set_green_share 69 | data: 70 | entity_id: number.wallbox_solar_green_share 71 | value: 80 72 | ``` 73 | 74 | ### - Changing Comfort Charging Power in Watt 75 | ``` 76 | service: alfen_wallbox.set_comfort_power 77 | data: 78 | entity_id: number.wallbox_solar_comfort_level 79 | value: 1400 80 | ``` 81 | 82 | ### - Enable phase switching 83 | ``` 84 | service: alfen_wallbox.enable_phase_switching 85 | data: 86 | entity_id: switch.wallbox_enable_phase_switching 87 | ``` 88 | 89 | 90 | ### - Disable phase switching 91 | ``` 92 | service: alfen_wallbox.disable_phase_switching 93 | data: 94 | entity_id: switch.wallbox_enable_phase_switching 95 | ``` 96 | 97 | ### - Enable RFID Authorization Mode 98 | ``` 99 | service: alfen_wallbox.enable_rfid_authorization_mode 100 | data: 101 | entity_id: select.wallbox_authorization_mode 102 | ``` 103 | 104 | ### - Disable RFID Authorization Mode 105 | ``` 106 | service: alfen_wallbox.disable_rfid_authorization_mode 107 | data: 108 | entity_id: select.wallbox_authorization_mode 109 | ``` 110 | 111 | ### - Reboot wallbox 112 | ``` 113 | service: alfen_wallbox.reboot_wallbox 114 | data: 115 | entity_id: alfen_wallbox.garage 116 | ``` 117 | 118 | ## Screenshots 119 | 120 | 121 | ![Wallbox 2]() 122 | 123 | ![Wallbox 3]() 124 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/__init__.py: -------------------------------------------------------------------------------- 1 | """Alfen Wallbox integration.""" 2 | 3 | import logging 4 | from typing import Any 5 | 6 | from homeassistant.const import ( 7 | CONF_HOST, 8 | CONF_NAME, 9 | CONF_PASSWORD, 10 | CONF_SCAN_INTERVAL, 11 | CONF_TIMEOUT, 12 | CONF_USERNAME, 13 | Platform, 14 | ) 15 | from homeassistant.core import HomeAssistant, callback 16 | from homeassistant.helpers import entity_registry as er 17 | 18 | from .const import ( 19 | CONF_REFRESH_CATEGORIES, 20 | DEFAULT_REFRESH_CATEGORIES, 21 | DEFAULT_SCAN_INTERVAL, 22 | DEFAULT_TIMEOUT, 23 | ) 24 | from .coordinator import AlfenConfigEntry, AlfenCoordinator, options_update_listener 25 | 26 | PLATFORMS = [ 27 | Platform.BINARY_SENSOR, 28 | Platform.BUTTON, 29 | Platform.NUMBER, 30 | Platform.SELECT, 31 | Platform.SENSOR, 32 | Platform.SWITCH, 33 | Platform.TEXT, 34 | ] 35 | 36 | _LOGGER = logging.getLogger(__name__) 37 | 38 | 39 | async def async_migrate_entry( 40 | hass: HomeAssistant, config_entry: AlfenConfigEntry 41 | ) -> bool: 42 | """Migrate old entry.""" 43 | _LOGGER.debug("Migrating from version %s", config_entry.version) 44 | 45 | if config_entry.version == 1: 46 | scan_interval = config_entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) 47 | options = { 48 | CONF_SCAN_INTERVAL: scan_interval, 49 | CONF_TIMEOUT: DEFAULT_TIMEOUT, 50 | CONF_REFRESH_CATEGORIES: DEFAULT_REFRESH_CATEGORIES, 51 | } 52 | data = { 53 | CONF_HOST: config_entry.data.get(CONF_HOST), 54 | CONF_NAME: config_entry.data.get(CONF_NAME), 55 | CONF_USERNAME: config_entry.data.get(CONF_USERNAME), 56 | CONF_PASSWORD: config_entry.data.get(CONF_PASSWORD), 57 | } 58 | 59 | hass.config_entries.async_update_entry( 60 | config_entry, 61 | version=2, 62 | data=data, 63 | options=options, 64 | ) 65 | 66 | _LOGGER.debug("Migration to version %s successful", config_entry.version) 67 | 68 | return True 69 | 70 | 71 | async def async_setup_entry( 72 | hass: HomeAssistant, config_entry: AlfenConfigEntry 73 | ) -> bool: 74 | """Set up Alfen from a config entry.""" 75 | await er.async_migrate_entries( 76 | hass, config_entry.entry_id, async_migrate_entity_entry 77 | ) 78 | 79 | coordinator = AlfenCoordinator(hass, config_entry) 80 | await coordinator.async_config_entry_first_refresh() 81 | 82 | config_entry.runtime_data = coordinator 83 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 84 | 85 | config_entry.async_on_unload( 86 | config_entry.add_update_listener(options_update_listener) 87 | ) 88 | return True 89 | 90 | 91 | async def async_unload_entry( 92 | hass: HomeAssistant, config_entry: AlfenConfigEntry 93 | ) -> bool: 94 | """Unload a config entry.""" 95 | _LOGGER.debug("async_unload_entry: %s", config_entry) 96 | 97 | coordinator = config_entry.runtime_data 98 | await coordinator.device.logout() 99 | return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) 100 | 101 | 102 | @callback 103 | def async_migrate_entity_entry( 104 | entity_entry: er.RegistryEntry, 105 | ) -> dict[str, Any] | None: 106 | """Migrate a Alfen entity entry.""" 107 | 108 | # No migration needed 109 | return None 110 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/alfen.py: -------------------------------------------------------------------------------- 1 | """Alfen Wallbox API.""" 2 | 3 | import asyncio 4 | import datetime 5 | import json 6 | import logging 7 | from ssl import SSLContext 8 | 9 | from aiohttp import ClientResponse, ClientSession 10 | 11 | from .const import ( 12 | ALFEN_PRODUCT_MAP, 13 | CAT, 14 | CAT_LOGS, 15 | CAT_TRANSACTIONS, 16 | CATEGORIES, 17 | CMD, 18 | COMMAND_CLEAR_TRANSACTIONS, 19 | COMMAND_REBOOT, 20 | DEFAULT_TIMEOUT, 21 | DISPLAY_NAME_VALUE, 22 | DOMAIN, 23 | ID, 24 | INFO, 25 | LICENSES, 26 | LOGIN, 27 | LOGOUT, 28 | METHOD_GET, 29 | OFFSET, 30 | PARAM_COMMAND, 31 | PARAM_DISPLAY_NAME, 32 | PARAM_PASSWORD, 33 | PARAM_USERNAME, 34 | PROP, 35 | PROPERTIES, 36 | TOTAL, 37 | VALUE, 38 | ) 39 | 40 | POST_HEADER_JSON = {"Content-Type": "application/json"} 41 | 42 | _LOGGER = logging.getLogger(__name__) 43 | 44 | 45 | class AlfenDevice: 46 | """Alfen Device.""" 47 | 48 | def __init__( 49 | self, 50 | session: ClientSession, 51 | host: str, 52 | name: str, 53 | username: str, 54 | password: str, 55 | category_options: list, 56 | ssl: SSLContext, 57 | ) -> None: 58 | """Init.""" 59 | 60 | self.host = host 61 | self.name = name 62 | self._session = session 63 | self.username = username 64 | self.category_options = category_options 65 | self.info = None 66 | self.id = None 67 | if self.username is None: 68 | self.username = "admin" 69 | self.password = password 70 | self.properties = [] 71 | self._session.verify = False 72 | self.keep_logout = False 73 | self.max_allowed_phases = 1 74 | self.latest_tag = None 75 | self.transaction_offset = 0 76 | self.transaction_counter = 0 77 | self.ssl = ssl 78 | self.static_properties = [] 79 | self.get_static_properties = True 80 | self.logged_in = False 81 | self.last_updated = None 82 | self.latest_logs = [] 83 | # prevent multiple call to wallbox 84 | self.lock = False 85 | self.update_values = {} 86 | self.updating = False 87 | 88 | async def init(self) -> bool: 89 | """Initialize the Alfen API.""" 90 | result = await self.get_info() 91 | self.id = f"alfen_{self.name}" 92 | if self.name is None: 93 | self.name = f"{self.info.identity} ({self.host})" 94 | 95 | return result 96 | 97 | def get_number_of_sockets(self) -> int | None: 98 | """Get number of sockets from the properties.""" 99 | sockets = 1 100 | if "205E_0" in self.properties: 101 | sockets = self.properties["205E_0"][VALUE] 102 | return sockets 103 | 104 | def get_licenses(self) -> list | None: 105 | """Get licenses from the properties.""" 106 | licenses = [] 107 | if "21A2_0" in self.properties: 108 | prop = self.properties["21A2_0"] 109 | for key, value in LICENSES.items(): 110 | if int(prop[VALUE]) & int(value): 111 | licenses.append(key) 112 | return licenses 113 | 114 | async def get_info(self) -> bool: 115 | """Get info from the API.""" 116 | response = await self._session.get(url=self.__get_url(INFO), ssl=self.ssl) 117 | _LOGGER.debug("Response %s", str(response)) 118 | 119 | if response.status == 200: 120 | resp = await response.json(content_type=None) 121 | self.info = AlfenDeviceInfo(resp) 122 | 123 | return True 124 | 125 | _LOGGER.debug("Info API not available, use generic info") 126 | generic_info = { 127 | "Identity": self.host, 128 | "FWVersion": "?", 129 | "Model": "Generic Alfen Wallbox", 130 | "ObjectId": "?", 131 | "Type": "?", 132 | } 133 | self.info = AlfenDeviceInfo(generic_info) 134 | return False 135 | 136 | @property 137 | def device_info(self) -> dict: 138 | """Return a device description for device registry.""" 139 | return { 140 | "identifiers": {(DOMAIN, self.name)}, 141 | "manufacturer": "Alfen", 142 | "model": self.info.model, 143 | "name": self.name, 144 | "sw_version": self.info.firmware_version, 145 | } 146 | 147 | async def async_update(self) -> bool: 148 | """Update the device properties.""" 149 | if self.keep_logout: 150 | return True 151 | if self.updating: 152 | return True 153 | 154 | try: 155 | self.updating = True 156 | # we update first the self.update_values 157 | # copy the values to other dict 158 | # we need to copy the values to avoid the dict changed size error 159 | values = self.update_values.copy() 160 | for value in values.values(): 161 | response = await self._update_value(value["api_param"], value["value"]) 162 | 163 | if response: 164 | # we expect that the value is updated so we are just update the value in the properties 165 | if value["api_param"] in self.properties: 166 | prop = self.properties[value["api_param"]] 167 | _LOGGER.debug( 168 | "Set %s value %s", 169 | str(value["api_param"]), 170 | str(value["value"]), 171 | ) 172 | prop[VALUE] = value["value"] 173 | self.properties[value["api_param"]] = prop 174 | # remove the update from the list 175 | del self.update_values[value["api_param"]] 176 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 177 | _LOGGER.error("Unexpected error on update %s", str(e)) 178 | self.updating = False 179 | return False 180 | finally: 181 | self.updating = False 182 | 183 | self.last_updated = datetime.datetime.now() 184 | dynamic_properties = [] 185 | if self.get_static_properties: 186 | self.static_properties = [] 187 | 188 | for cat in CATEGORIES: 189 | if cat in (CAT_TRANSACTIONS, CAT_LOGS): 190 | continue 191 | if cat in self.category_options: 192 | dynamic_properties = ( 193 | dynamic_properties + await self._get_all_properties_value(cat) 194 | ) 195 | elif self.get_static_properties: 196 | self.static_properties = ( 197 | self.static_properties + await self._get_all_properties_value(cat) 198 | ) 199 | self.properties = {} 200 | # for each properties (statis and dynamic, use the ID as index) 201 | for prop in dynamic_properties: 202 | # check if the ID is already in the properties 203 | propId = prop[ID] 204 | self.properties[propId] = prop 205 | 206 | for prop in self.static_properties: 207 | # check if the ID is already in the properties 208 | propId = prop[ID] 209 | self.properties[propId] = prop 210 | 211 | self.get_static_properties = False 212 | 213 | if CAT_LOGS in self.category_options: 214 | await self._get_log() 215 | 216 | if CAT_TRANSACTIONS in self.category_options: 217 | if self.transaction_counter == 0: 218 | await self._get_transaction() 219 | self.transaction_counter += 1 220 | 221 | if self.transaction_counter > 60: 222 | self.transaction_counter = 0 223 | 224 | return True 225 | 226 | async def _post( 227 | self, cmd, payload=None, allowed_login=True 228 | ) -> ClientResponse | None: 229 | """Send a POST request to the API.""" 230 | if self.keep_logout: 231 | return None 232 | 233 | if self.lock: 234 | return None 235 | 236 | try: 237 | self.lock = True 238 | _LOGGER.debug("Send Post Request") 239 | async with self._session.post( 240 | url=self.__get_url(cmd), 241 | json=payload, 242 | headers=POST_HEADER_JSON, 243 | timeout=DEFAULT_TIMEOUT, 244 | ssl=self.ssl, 245 | ) as response: 246 | if response.status == 401 and allowed_login: 247 | self.lock = False 248 | self.logged_in = False 249 | _LOGGER.debug("POST with login") 250 | await self.login() 251 | return await self._post(cmd, payload, False) 252 | response.raise_for_status() 253 | self.lock = False 254 | return response 255 | except json.JSONDecodeError as e: 256 | # skip tailing comma error from alfen 257 | _LOGGER.debug("trailing comma is not allowed") 258 | if e.msg == "trailing comma is not allowed": 259 | return None 260 | 261 | _LOGGER.error("JSONDecodeError error on POST %s", str(e)) 262 | self.lock = False 263 | except TimeoutError: 264 | _LOGGER.warning("Timeout on POST") 265 | self.lock = False 266 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 267 | if not allowed_login: 268 | _LOGGER.error("Unexpected error on POST %s", str(e)) 269 | self.lock = False 270 | 271 | async def _get( 272 | self, url, allowed_login=True, json_decode=True 273 | ) -> ClientResponse | None: 274 | """Send a GET request to the API.""" 275 | if self.keep_logout: 276 | return None 277 | 278 | if self.lock: 279 | return None 280 | 281 | try: 282 | self.lock = True 283 | async with self._session.get( 284 | url, timeout=DEFAULT_TIMEOUT, ssl=self.ssl 285 | ) as response: 286 | if response.status == 401 and allowed_login: 287 | self.lock = False 288 | self.logged_in = False 289 | _LOGGER.debug("GET with login") 290 | await self.login() 291 | return await self._get( 292 | url=url, allowed_login=False, json_decode=False 293 | ) 294 | 295 | response.raise_for_status() 296 | if json_decode: 297 | _resp = await response.json(content_type=None) 298 | else: 299 | _resp = await response.text() 300 | self.lock = False 301 | return _resp 302 | except TimeoutError: 303 | _LOGGER.warning("Timeout on GET") 304 | self.lock = False 305 | return None 306 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 307 | if not allowed_login: 308 | _LOGGER.error("Unexpected error on GET %s", str(e)) 309 | self.lock = False 310 | return None 311 | 312 | async def login(self): 313 | """Login to the API.""" 314 | self.keep_logout = False 315 | 316 | try: 317 | response = await self._post( 318 | cmd=LOGIN, 319 | payload={ 320 | PARAM_USERNAME: self.username, 321 | PARAM_PASSWORD: self.password, 322 | PARAM_DISPLAY_NAME: DISPLAY_NAME_VALUE, 323 | }, 324 | ) 325 | self.logged_in = True 326 | self.last_updated = datetime.datetime.now() 327 | 328 | _LOGGER.debug("Login response %s", response) 329 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 330 | _LOGGER.error("Unexpected error on LOGIN %s", str(e)) 331 | return 332 | 333 | async def logout(self): 334 | """Logout from the API.""" 335 | self.keep_logout = True 336 | 337 | try: 338 | response = await self._post(cmd=LOGOUT, allowed_login=False) 339 | self.logged_in = False 340 | self.last_updated = datetime.datetime.now() 341 | 342 | _LOGGER.debug("Logout response %s", str(response)) 343 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 344 | _LOGGER.error("Unexpected error on LOGOUT %s", str(e)) 345 | return 346 | 347 | async def _update_value( 348 | self, api_param, value, allowed_login=True 349 | ) -> ClientResponse | None: 350 | """Update a value on the API.""" 351 | if self.keep_logout: 352 | return None 353 | 354 | if self.lock: 355 | return None 356 | 357 | try: 358 | self.lock = True 359 | async with self._session.post( 360 | url=self.__get_url(PROP), 361 | json={api_param: {ID: api_param, VALUE: str(value)}}, 362 | headers=POST_HEADER_JSON, 363 | timeout=DEFAULT_TIMEOUT, 364 | ssl=self.ssl, 365 | ) as response: 366 | if response.status == 401 and allowed_login: 367 | self.logged_in = False 368 | self.lock = False 369 | _LOGGER.debug("POST(Update) with login") 370 | await self.login() 371 | return await self._update_value(api_param, value, False) 372 | response.raise_for_status() 373 | self.lock = False 374 | return response 375 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 376 | if not allowed_login: 377 | _LOGGER.error("Unexpected error on UPDATE VALUE %s", str(e)) 378 | self.lock = False 379 | return None 380 | 381 | async def _get_value(self, api_param): 382 | """Get a value from the API.""" 383 | cmd = f"{PROP}?{ID}={api_param}" 384 | response = await self._get(url=self.__get_url(cmd)) 385 | # _LOGGER.debug("Status Response %s: %s", cmd, str(response)) 386 | 387 | if response is not None: 388 | if self.properties is None: 389 | self.properties = {} 390 | for resp in response[PROPERTIES]: 391 | if resp[ID] in self.properties: 392 | self.properties[resp[ID]] = resp 393 | 394 | async def _get_all_properties_value(self, category: str) -> list: 395 | """Get all properties from the API.""" 396 | _LOGGER.debug("Get properties") 397 | 398 | properties = [] 399 | tx_start = datetime.datetime.now() 400 | nextRequest = True 401 | offset = 0 402 | attempt = 0 403 | 404 | while nextRequest: 405 | attempt += 1 406 | cmd = f"{PROP}?{CAT}={category}&{OFFSET}={offset}" 407 | response = await self._get(url=self.__get_url(cmd)) 408 | # _LOGGER.debug("Status Response %s: %s", cmd, str(response)) 409 | 410 | if response is not None: 411 | attempt = 0 412 | # if response is a string, convert it to json 413 | if isinstance(response, str): 414 | response = json.loads(response) 415 | # merge the properties with response properties 416 | properties += response[PROPERTIES] 417 | nextRequest = response[TOTAL] > (offset + len(response[PROPERTIES])) 418 | offset += len(response[PROPERTIES]) 419 | elif attempt >= 3: 420 | # This only possible in case of series of timeouts or unknown exceptions in self._get() 421 | # It's better to break completely, otherwise we can provide partial data in self.properties. 422 | _LOGGER.debug("Returning earlier after %s attempts", str(attempt)) 423 | break 424 | else: 425 | await asyncio.sleep(5) 426 | 427 | # _LOGGER.debug("Properties %s", str(properties)) 428 | runtime = datetime.datetime.now() - tx_start 429 | _LOGGER.info("Called %s in %.2f seconds", category, runtime.total_seconds()) 430 | return properties 431 | 432 | async def reboot_wallbox(self): 433 | """Reboot the wallbox.""" 434 | response = await self._post(cmd=CMD, payload={PARAM_COMMAND: COMMAND_REBOOT}) 435 | _LOGGER.debug("Reboot response %s", str(response)) 436 | 437 | async def clear_transactions(self): 438 | """Clear the transactions.""" 439 | response = await self._post( 440 | cmd=CMD, payload={PARAM_COMMAND: COMMAND_CLEAR_TRANSACTIONS} 441 | ) 442 | _LOGGER.debug("Clear Transactions response %s", str(response)) 443 | 444 | async def send_command(self, command): 445 | """Run a command.""" 446 | response = await self._post(cmd=CMD, payload=command) 447 | _LOGGER.debug("Run Command response %s", str(response)) 448 | 449 | async def _fetch_log(self, log_offset) -> str | None: 450 | """Fetch the log.""" 451 | response = await self._get( 452 | url=self.__get_url("log?offset=" + str(log_offset)), 453 | json_decode=False, 454 | ) 455 | if response is None: 456 | return None 457 | lines = response.splitlines() 458 | 459 | # we need to get all the log between the self.lastest_log_id and the log_id before we update the self.latest_log_id 460 | for line in lines: 461 | if self.latest_logs is None: 462 | self.latest_logs = [] 463 | if line in self.latest_logs: 464 | continue 465 | self.latest_logs.append(line) 466 | # _LOGGER.debug(line) 467 | 468 | return True 469 | 470 | async def _get_log(self): 471 | """Get the log.""" 472 | log_offset = 0 473 | self.latest_logs = [] 474 | while await self._fetch_log(log_offset): 475 | log_offset += 1 476 | if log_offset > 5: 477 | break 478 | 479 | self.latest_logs.reverse() 480 | for log in self.latest_logs: 481 | # split on \n 482 | lines = log.splitlines() 483 | for linerec in lines: 484 | # _LOGGER.debug(line) 485 | # get the index of _ 486 | index = linerec.find("_") 487 | if index == -1 or index >= 20: 488 | continue 489 | line_id = linerec[:index] 490 | # substring on : so we get the date and time 491 | line = linerec[index + 1 :] 492 | index = line.split(":") 493 | # if we have less then 7 then we skip it 494 | if len(index) < 7: 495 | continue 496 | # get the date and time 497 | date = index[0] + ":" + index[1] + ":" + index[2] 498 | # type of log 499 | type = index[3] 500 | # filename 501 | filename = index[4] 502 | # line number 503 | line = index[5] 504 | # message 505 | message = index[6] 506 | # show the rest of all the index after 5 507 | for i in range(7, len(index)): 508 | message += ":" + index[i] 509 | # _LOGGER.debug(message) 510 | # if contains 'EV_CONNECTED_AUTHORIZED' then we have a tag 511 | # Socket #1: main state: EV_CONNECTED_AUTHORIZED, CP: 8.8/8.9, tag: xxxxxxx 512 | if ( 513 | "EV_CONNECTED_AUTHORIZED" in message 514 | or "CHARGING_POWER_ON" in message 515 | or "CABLE_CONNECTED" in message 516 | ) and "tag:" in message: 517 | # check which socket we have 518 | socket = "" 519 | if "Socket #1" in message: 520 | socket = "1" 521 | elif "Socket #2" in message: 522 | socket = "2" 523 | if self.latest_tag is None: 524 | self.latest_tag = {} 525 | split = message.split("tag: ", 2) 526 | # store the log id in the value, we only override if the id > then the previous id 527 | tag = "socket " + socket, "start", "tag" 528 | taglog = "socket " + socket, "start", "taglog" 529 | if taglog not in self.latest_tag: 530 | self.latest_tag[taglog] = 0 531 | if tag not in self.latest_tag: 532 | self.latest_tag[tag] = None 533 | 534 | if self.latest_tag[taglog] < int(line_id): 535 | self.latest_tag[taglog] = int(line_id) 536 | self.latest_tag[tag] = split[1] 537 | 538 | # disconnect 539 | if ( 540 | "CHARGING_POWER_OFF" in message or "CHARGING_TERMINATING" in message 541 | ) and "tag:" in message: 542 | # check which socket we have 543 | socket = "" 544 | if "Socket #1" in message: 545 | socket = "1" 546 | elif "Socket #2" in message: 547 | socket = "2" 548 | if self.latest_tag is None: 549 | self.latest_tag = {} 550 | 551 | # store the log id in the value, we only override if the id > then the previous id 552 | tag = "socket " + socket, "start", "tag" 553 | taglog = "socket " + socket, "start", "taglog" 554 | if taglog not in self.latest_tag: 555 | self.latest_tag[taglog] = 0 556 | if tag not in self.latest_tag: 557 | self.latest_tag[tag] = None 558 | 559 | if self.latest_tag[taglog] < int(line_id): 560 | self.latest_tag[taglog] = int(line_id) 561 | self.latest_tag[tag] = "No Tag" 562 | # _LOGGER.warning(self.latest_tag) 563 | # _LOGGER.debug(message) 564 | 565 | async def _get_transaction(self): 566 | _LOGGER.debug("Get Transaction") 567 | offset = self.transaction_offset 568 | transactionLoop = True 569 | counter = 0 570 | unknownLine = 0 571 | while transactionLoop: 572 | response = await self._get( 573 | url=self.__get_url("transactions?offset=" + str(offset)), 574 | json_decode=False, 575 | ) 576 | # _LOGGER.debug(response) 577 | # split this text into lines with \n 578 | lines = str(response).splitlines() 579 | 580 | # if the lines are empty, break the loop 581 | if not lines or not response: 582 | transactionLoop = False 583 | break 584 | 585 | for line in lines: 586 | # _LOGGER.debug("Line: %s", line) 587 | if line is None: 588 | transactionLoop = False 589 | break 590 | 591 | try: 592 | if "version" in line: 593 | # _LOGGER.debug("Version line" + line) 594 | line = line.split(":2,", 2)[1] 595 | 596 | splitline = line.split(" ") 597 | 598 | if "txstart" in line: 599 | # _LOGGER.debug("start line: " + line) 600 | tid = line.split(":", 2)[0].split("_", 2)[0] 601 | 602 | tid = splitline[0].split("_", 2)[0] 603 | socket = splitline[3] + " " + splitline[4].split(",", 2)[0] 604 | 605 | date = splitline[5] + " " + splitline[6] 606 | kWh = splitline[7].split("kWh", 2)[0] 607 | tag = splitline[8] 608 | 609 | # 3: transaction id 610 | # 9: 1 611 | # 10: y 612 | 613 | if self.latest_tag is None: 614 | self.latest_tag = {} 615 | # self.latest_tag[socket, "start", "tag"] = tag 616 | self.latest_tag[socket, "start", "date"] = date 617 | self.latest_tag[socket, "start", "kWh"] = kWh 618 | 619 | elif "txstop" in line: 620 | # _LOGGER.debug("stop line: " + line) 621 | 622 | tid = splitline[0].split("_", 2)[0] 623 | socket = splitline[3] + " " + splitline[4].split(",", 2)[0] 624 | 625 | date = splitline[5] + " " + splitline[6] 626 | kWh = splitline[7].split("kWh", 2)[0] 627 | tag = splitline[8] 628 | 629 | # 2: transaction id 630 | # 9: y 631 | 632 | if self.latest_tag is None: 633 | self.latest_tag = {} 634 | # self.latest_tag[socket, "stop", "tag"] = tag 635 | self.latest_tag[socket, "stop", "date"] = date 636 | self.latest_tag[socket, "stop", "kWh"] = kWh 637 | 638 | # store the latest start kwh and date 639 | for key in list(self.latest_tag): 640 | if ( 641 | key[0] == socket 642 | and key[1] == "start" 643 | and key[2] == "kWh" 644 | ): 645 | self.latest_tag[socket, "last_start", "kWh"] = ( 646 | self.latest_tag[socket, "start", "kWh"] 647 | ) 648 | if ( 649 | key[0] == socket 650 | and key[1] == "start" 651 | and key[2] == "date" 652 | ): 653 | self.latest_tag[socket, "last_start", "date"] = ( 654 | self.latest_tag[socket, "start", "date"] 655 | ) 656 | 657 | elif "mv" in line: 658 | # _LOGGER.debug("mv line: " + line) 659 | tid = splitline[0].split("_", 2)[0] 660 | socket = splitline[1] + " " + splitline[2].split(",", 2)[0] 661 | date = splitline[3] + " " + splitline[4] 662 | kWh = splitline[5] 663 | 664 | if self.latest_tag is None: 665 | self.latest_tag = {} 666 | self.latest_tag[socket, "mv", "date"] = date 667 | self.latest_tag[socket, "mv", "kWh"] = kWh 668 | 669 | # _LOGGER.debug(self.latest_tag) 670 | 671 | elif "dto" in line: 672 | # get the value from begin till _dto 673 | tid = int(splitline[0].split("_", 2)[0]) 674 | if tid > offset: 675 | offset = tid 676 | continue 677 | offset = offset + 1 678 | continue 679 | elif "0_Empty" in line: 680 | # break if the transaction is empty 681 | transactionLoop = False 682 | break 683 | else: 684 | _LOGGER.debug("Unknown line: %s", str(line)) 685 | offset = offset + 1 686 | unknownLine += 1 687 | if unknownLine > 2: 688 | transactionLoop = False 689 | continue 690 | except IndexError: 691 | break 692 | 693 | # check if tid is integer 694 | try: 695 | offset = int(tid) 696 | if self.transaction_offset == offset: 697 | counter += 1 698 | else: 699 | self.transaction_offset = offset 700 | counter = 0 701 | 702 | if counter == 2: 703 | _LOGGER.debug(self.latest_tag) 704 | transactionLoop = False 705 | break 706 | except ValueError: 707 | continue 708 | 709 | # check if last line is reached 710 | if line == lines[-1]: 711 | break 712 | 713 | async def async_request( 714 | self, method: str, cmd: str, json_data=None 715 | ) -> ClientResponse | None: 716 | """Send a request to the API.""" 717 | try: 718 | return await self.request(method, cmd, json_data) 719 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 720 | _LOGGER.error("Unexpected error async request %s", str(e)) 721 | return None 722 | 723 | async def request(self, method: str, cmd: str, json_data=None) -> ClientResponse: 724 | """Send a request to the API.""" 725 | if method == METHOD_GET: 726 | response = await self._get(url=self.__get_url(cmd)) 727 | else: # METHOD_POST 728 | response = await self._post(cmd=cmd, payload=json_data) 729 | 730 | _LOGGER.debug("Request response %s", str(response)) 731 | return response 732 | 733 | def set_value(self, api_param, value): 734 | """Set a value on the API.""" 735 | # check if the api_param is already in the update_values, update the value 736 | if api_param in self.update_values: 737 | self.update_values[api_param]["value"] = value 738 | return 739 | self.update_values[api_param] = {"api_param": api_param, "value": value} 740 | # force update 741 | asyncio.run_coroutine_threadsafe(self.async_update(), self._session.loop) 742 | 743 | async def get_value(self, api_param): 744 | """Get a value from the API.""" 745 | return await self._get_value(api_param) 746 | 747 | async def set_current_limit(self, limit) -> None: 748 | """Set the current limit.""" 749 | _LOGGER.debug("Set current limit %sA", str(limit)) 750 | if limit > 32 | limit < 1: 751 | return 752 | await self.set_value("2129_0", limit) 753 | 754 | async def set_rfid_auth_mode(self, enabled): 755 | """Set the RFID Auth Mode.""" 756 | _LOGGER.debug("Set RFID Auth Mode %s", str(enabled)) 757 | 758 | value = 0 759 | if enabled: 760 | value = 2 761 | 762 | await self.set_value("2126_0", value) 763 | 764 | async def set_current_phase(self, phase) -> None: 765 | """Set the current phase.""" 766 | _LOGGER.debug("Set current phase %s", str(phase)) 767 | if phase not in ("L1", "L2", "L3"): 768 | return 769 | await self.set_value("2069_0", phase) 770 | 771 | async def set_phase_switching(self, enabled): 772 | """Set the phase switching.""" 773 | _LOGGER.debug("Set Phase Switching %s", str(enabled)) 774 | 775 | value = 0 776 | if enabled: 777 | value = 1 778 | await self.set_value("2185_0", value) 779 | 780 | async def set_green_share(self, value) -> None: 781 | """Set the green share.""" 782 | _LOGGER.debug("Set green share value %s", str(value)) 783 | if value < 0 | value > 100: 784 | return 785 | await self.set_value("3280_2", value) 786 | 787 | async def set_comfort_power(self, value) -> None: 788 | """Set the comfort power.""" 789 | _LOGGER.debug("Set Comfort Level %sW", str(value)) 790 | if value < 1400 | value > 5000: 791 | return 792 | await self.set_value("3280_3", value) 793 | 794 | def __get_url(self, action) -> str: 795 | """Get the URL for the API.""" 796 | return f"https://{self.host}/api/{action}" 797 | 798 | 799 | class AlfenDeviceInfo: 800 | """Representation of a Alfen device info.""" 801 | 802 | def __init__(self, response) -> None: 803 | """Initialize the Alfen device info.""" 804 | self.identity = response["Identity"] 805 | self.firmware_version = response["FWVersion"] 806 | self.model_id = response["Model"] 807 | 808 | self.model = ALFEN_PRODUCT_MAP.get(self.model_id, self.model_id) 809 | self.object_id = response["ObjectId"] 810 | self.type = response["Type"] 811 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Alfen Eve Proline binary sensors.""" 2 | 3 | from dataclasses import dataclass 4 | import logging 5 | from typing import Final 6 | 7 | from homeassistant.components.binary_sensor import ( 8 | BinarySensorDeviceClass, 9 | BinarySensorEntity, 10 | BinarySensorEntityDescription, 11 | ) 12 | from homeassistant.core import HomeAssistant 13 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 14 | 15 | from .const import ( 16 | CAT, 17 | ID, 18 | LICENSE_HIGH_POWER, 19 | LICENSE_LOAD_BALANCING_ACTIVE, 20 | LICENSE_LOAD_BALANCING_STATIC, 21 | LICENSE_MOBILE, 22 | LICENSE_NONE, 23 | LICENSE_PAYMENT_GIROE, 24 | LICENSE_PERSONALIZED_DISPLAY, 25 | LICENSE_RFID, 26 | LICENSE_SCN, 27 | VALUE, 28 | ) 29 | from .coordinator import AlfenConfigEntry 30 | from .entity import AlfenEntity 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | 35 | @dataclass 36 | class AlfenBinaryDescriptionMixin: 37 | """Define an entity description mixin for binary sensor entities.""" 38 | 39 | api_param: str 40 | 41 | 42 | @dataclass 43 | class AlfenBinaryDescription( 44 | BinarySensorEntityDescription, AlfenBinaryDescriptionMixin 45 | ): 46 | """Class to describe an Alfen binary sensor entity.""" 47 | 48 | 49 | ALFEN_BINARY_SENSOR_TYPES: Final[tuple[AlfenBinaryDescription, ...]] = ( 50 | AlfenBinaryDescription( 51 | key="system_date_light_savings", 52 | name="System Daylight Savings", 53 | device_class=None, 54 | api_param="205B_0", 55 | ), 56 | AlfenBinaryDescription( 57 | key="license_scn", 58 | name="License Smart Charging Network", 59 | device_class=None, 60 | api_param=None, 61 | ), 62 | AlfenBinaryDescription( 63 | key="license_active_loadbalancing", 64 | name="License Active Loadbalancing", 65 | device_class=None, 66 | api_param=None, 67 | ), 68 | AlfenBinaryDescription( 69 | key="license_static_loadbalancing", 70 | name="License Static Loadbalancing", 71 | device_class=None, 72 | api_param=None, 73 | ), 74 | AlfenBinaryDescription( 75 | key="license_high_power_sockets", 76 | name="License 32A Output per Socket", 77 | device_class=None, 78 | api_param=None, 79 | ), 80 | AlfenBinaryDescription( 81 | key="license_rfid_reader", 82 | name="License RFID Reader", 83 | device_class=None, 84 | api_param=None, 85 | ), 86 | AlfenBinaryDescription( 87 | key="license_personalized_display", 88 | name="License Personalized Display", 89 | device_class=None, 90 | api_param=None, 91 | ), 92 | AlfenBinaryDescription( 93 | key="license_mobile_3G_4G", 94 | name="License Mobile 3G & 4G", 95 | device_class=None, 96 | api_param=None, 97 | ), 98 | AlfenBinaryDescription( 99 | key="license_giro_e", 100 | name="License Giro-e Payment", 101 | device_class=None, 102 | api_param=None, 103 | ), 104 | AlfenBinaryDescription( 105 | key="https_api_login_status", 106 | name="HTTPS API Login Status", 107 | device_class=BinarySensorDeviceClass.CONNECTIVITY, 108 | api_param=None, 109 | ), 110 | ) 111 | 112 | 113 | async def async_setup_entry( 114 | hass: HomeAssistant, 115 | entry: AlfenConfigEntry, 116 | async_add_entities: AddEntitiesCallback, 117 | ) -> None: 118 | """Set up Alfen binary sensor entities from a config entry.""" 119 | 120 | binaries = [ 121 | AlfenBinarySensor(entry, description) 122 | for description in ALFEN_BINARY_SENSOR_TYPES 123 | ] 124 | 125 | async_add_entities(binaries) 126 | 127 | 128 | class AlfenBinarySensor(AlfenEntity, BinarySensorEntity): 129 | """Define an Alfen binary sensor.""" 130 | 131 | entity_description: AlfenBinaryDescription 132 | 133 | def __init__( 134 | self, entry: AlfenConfigEntry, description: AlfenBinaryDescription 135 | ) -> None: 136 | """Initialize.""" 137 | super().__init__(entry) 138 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 139 | self._attr_unique_id = f"{self.coordinator.device.id}_{description.key}" 140 | self.entity_description = description 141 | 142 | licenses = self.coordinator.device.get_licenses() 143 | 144 | # custom code for license 145 | if self.entity_description.api_param is None: 146 | # check if license is available 147 | if "21A2_0" in self.coordinator.device.properties: 148 | if self.coordinator.device.properties["21A2_0"][VALUE] == LICENSE_NONE: 149 | return 150 | if self.entity_description.key == "license_scn": 151 | self._attr_is_on = LICENSE_SCN in licenses 152 | if self.entity_description.key == "license_active_loadbalancing": 153 | self._attr_is_on = ( 154 | LICENSE_SCN in licenses or LICENSE_LOAD_BALANCING_ACTIVE in licenses 155 | ) 156 | if self.entity_description.key == "license_static_loadbalancing": 157 | self._attr_is_on = ( 158 | LICENSE_SCN in licenses 159 | or LICENSE_LOAD_BALANCING_STATIC in licenses 160 | or LICENSE_LOAD_BALANCING_STATIC in licenses 161 | ) 162 | if self.entity_description.key == "license_high_power_sockets": 163 | self._attr_is_on = LICENSE_HIGH_POWER in licenses 164 | if self.entity_description.key == "license_rfid_reader": 165 | self._attr_is_on = LICENSE_RFID in licenses 166 | if self.entity_description.key == "license_personalized_display": 167 | self._attr_is_on = LICENSE_PERSONALIZED_DISPLAY in licenses 168 | if self.entity_description.key == "license_mobile_3G_4G": 169 | self._attr_is_on = LICENSE_MOBILE in licenses 170 | if self.entity_description.key == "license_giro_e": 171 | self._attr_is_on = LICENSE_PAYMENT_GIROE in licenses 172 | 173 | # if self.entity_description.key == "license_qrcode": 174 | # self._attr_is_on = LICENSE_PAYMENT_QRCODE in licenses 175 | # if self.entity_description.key == "license_expose_smartmeterdata": 176 | # self._attr_is_on = LICENSE_EXPOSE_SMARTMETERDATA in licenses 177 | 178 | @property 179 | def available(self) -> bool: 180 | """Return True if entity is available.""" 181 | 182 | if self.entity_description.api_param is not None: 183 | return ( 184 | self.entity_description.api_param in self.coordinator.device.properties 185 | ) 186 | 187 | return True 188 | 189 | @property 190 | def is_on(self) -> bool: 191 | """Return True if entity is on.""" 192 | 193 | if self.entity_description.api_param is not None: 194 | if self.entity_description.api_param in self.coordinator.device.properties: 195 | prop = self.coordinator.device.properties[ 196 | self.entity_description.api_param 197 | ] 198 | return prop[VALUE] == 1 199 | return False 200 | 201 | if self.entity_description.key == "https_api_login_status": 202 | return self.coordinator.device.logged_in 203 | 204 | return self._attr_is_on 205 | 206 | @property 207 | def extra_state_attributes(self) -> dict | None: 208 | """Return the default attributes of the element.""" 209 | if self.entity_description.api_param in self.coordinator.device.properties: 210 | return { 211 | "category": self.coordinator.device.properties[ 212 | self.entity_description.api_param 213 | ][CAT], 214 | } 215 | 216 | if self.entity_description.key == "https_api_login_status": 217 | return {"last_updated": self.coordinator.device.last_updated} 218 | 219 | return None 220 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/button.py: -------------------------------------------------------------------------------- 1 | """Button entity for Alfen EV chargers.""" "" 2 | 3 | from dataclasses import dataclass 4 | from typing import Final 5 | 6 | from homeassistant.components.button import ButtonEntity, ButtonEntityDescription 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 9 | 10 | from .const import ( 11 | CMD, 12 | COMMAND_CLEAR_TRANSACTIONS, 13 | COMMAND_REBOOT, 14 | FORCE_UPDATE, 15 | LOGIN, 16 | LOGOUT, 17 | METHOD_POST, 18 | PARAM_COMMAND, 19 | ) 20 | from .coordinator import AlfenConfigEntry 21 | from .entity import AlfenEntity 22 | 23 | 24 | @dataclass 25 | class AlfenButtonDescriptionMixin: 26 | """Define an entity description mixin for button entities.""" 27 | 28 | method: str 29 | url_action: str 30 | json_data: str 31 | 32 | 33 | @dataclass 34 | class AlfenButtonDescription(ButtonEntityDescription, AlfenButtonDescriptionMixin): 35 | """Class to describe an Alfen button entity.""" 36 | 37 | 38 | ALFEN_BUTTON_TYPES: Final[tuple[AlfenButtonDescription, ...]] = ( 39 | AlfenButtonDescription( 40 | key="reboot_wallbox", 41 | name="Reboot Wallbox", 42 | method=METHOD_POST, 43 | url_action=CMD, 44 | json_data={PARAM_COMMAND: COMMAND_REBOOT}, 45 | ), 46 | AlfenButtonDescription( 47 | key="auth_logout", 48 | name="HTTPS API Logout", 49 | method=METHOD_POST, 50 | url_action=LOGOUT, 51 | json_data=None, 52 | ), 53 | AlfenButtonDescription( 54 | key="auth_login", 55 | name="HTTPS API Login", 56 | method=METHOD_POST, 57 | url_action=LOGIN, 58 | json_data=None, 59 | ), 60 | AlfenButtonDescription( 61 | key="wallbox_force_update", 62 | name="Force Update", 63 | method=METHOD_POST, 64 | url_action="Force Update", 65 | json_data=None, 66 | ), 67 | AlfenButtonDescription( 68 | key="clear_transaction", 69 | name="Clear Transaction", 70 | method=METHOD_POST, 71 | url_action=CMD, 72 | json_data={PARAM_COMMAND: COMMAND_CLEAR_TRANSACTIONS}, 73 | ), 74 | ) 75 | 76 | 77 | async def async_setup_entry( 78 | hass: HomeAssistant, 79 | entry: AlfenConfigEntry, 80 | async_add_entities: AddEntitiesCallback, 81 | ) -> None: 82 | """Set up Alfen switch entities from a config entry.""" 83 | 84 | buttons = [AlfenButton(entry, description) for description in ALFEN_BUTTON_TYPES] 85 | 86 | async_add_entities(buttons) 87 | 88 | 89 | class AlfenButton(AlfenEntity, ButtonEntity): 90 | """Representation of a Alfen button entity.""" 91 | 92 | entity_description: AlfenButtonDescription 93 | 94 | def __init__( 95 | self, 96 | entry: AlfenConfigEntry, 97 | description: AlfenButtonDescription, 98 | ) -> None: 99 | """Initialize the Alfen button entity.""" 100 | super().__init__(entry) 101 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 102 | self._attr_unique_id = f"{self.coordinator.device.id}-{description.key}" 103 | self.entity_description = description 104 | 105 | async def async_press(self) -> None: 106 | """Press the button.""" 107 | if self.entity_description.url_action == FORCE_UPDATE: 108 | self.coordinator.device.get_static_properties = True 109 | await self.coordinator.device.async_update() 110 | return 111 | 112 | if self.entity_description.url_action == LOGIN: 113 | await self.coordinator.device.login() 114 | return 115 | 116 | if self.entity_description.url_action == LOGOUT: 117 | await self.coordinator.device.logout() 118 | return 119 | 120 | if self.entity_description.json_data is not None: 121 | await self.coordinator.device.send_command(self.entity_description.json_data) 122 | return 123 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for the Alfen Wallbox platform.""" 2 | 3 | from typing import Any 4 | 5 | import voluptuous as vol 6 | 7 | from homeassistant.config_entries import ( 8 | CONN_CLASS_LOCAL_POLL, 9 | ConfigEntry, 10 | ConfigFlow, 11 | ConfigFlowResult, 12 | OptionsFlow, 13 | ) 14 | from homeassistant.const import ( 15 | CONF_HOST, 16 | CONF_NAME, 17 | CONF_PASSWORD, 18 | CONF_SCAN_INTERVAL, 19 | CONF_TIMEOUT, 20 | CONF_USERNAME, 21 | ) 22 | from homeassistant.core import callback 23 | from homeassistant.helpers import config_validation as cv 24 | 25 | from .const import ( 26 | CATEGORIES, 27 | CONF_REFRESH_CATEGORIES, 28 | DEFAULT_REFRESH_CATEGORIES, 29 | DEFAULT_SCAN_INTERVAL, 30 | DEFAULT_TIMEOUT, 31 | DOMAIN, 32 | ) 33 | 34 | DEFAULT_OPTIONS = { 35 | CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, 36 | CONF_TIMEOUT: DEFAULT_TIMEOUT, 37 | CONF_REFRESH_CATEGORIES: DEFAULT_REFRESH_CATEGORIES, 38 | } 39 | 40 | 41 | class AlfenOptionsFlowHandler(OptionsFlow): 42 | """Handle Alfen options.""" 43 | 44 | async def async_step_init( 45 | self, user_input: dict[str, Any] | None = None 46 | ) -> ConfigFlowResult: 47 | """Manage the options flow.""" 48 | if user_input is not None: 49 | return self.async_create_entry(data=user_input) 50 | 51 | return self.async_show_form( 52 | step_id="init", 53 | data_schema=vol.Schema( 54 | { 55 | vol.Required( 56 | CONF_SCAN_INTERVAL, 57 | default=self.config_entry.options.get( 58 | CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL 59 | ), 60 | ): vol.All(vol.Coerce(int), vol.Range(min=1, max=300)), 61 | vol.Required( 62 | CONF_TIMEOUT, 63 | default=self.config_entry.options.get( 64 | CONF_TIMEOUT, DEFAULT_TIMEOUT 65 | ), 66 | ): vol.All(vol.Coerce(int), vol.Range(min=1, max=30)), 67 | vol.Required( 68 | CONF_REFRESH_CATEGORIES, 69 | default=self.config_entry.options.get( 70 | CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES 71 | ), 72 | ): cv.multi_select(CATEGORIES), 73 | }, 74 | ), 75 | ) 76 | 77 | 78 | class AlfenFlowHandler(ConfigFlow, domain=DOMAIN): 79 | """Handle a config flow.""" 80 | 81 | VERSION = 2 82 | CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL 83 | 84 | @staticmethod 85 | @callback 86 | def async_get_options_flow(config_entry: ConfigEntry) -> AlfenOptionsFlowHandler: 87 | """Options callback for Reolink.""" 88 | return AlfenOptionsFlowHandler() 89 | 90 | async def async_step_user(self, user_input=None): 91 | """User initiated config flow.""" 92 | if user_input is not None: 93 | result = await self.async_validate_input(user_input) 94 | if result is not None: 95 | return result 96 | 97 | return self.async_show_form( 98 | step_id="user", 99 | data_schema=vol.Schema( 100 | { 101 | vol.Required(CONF_HOST): str, 102 | vol.Required(CONF_USERNAME, default="admin"): str, 103 | vol.Required(CONF_PASSWORD): str, 104 | vol.Required(CONF_NAME): str, 105 | } 106 | ), 107 | ) 108 | 109 | async def async_validate_input(self, user_input) -> ConfigFlowResult | None: 110 | """Validate the input using the Devialet API.""" 111 | 112 | if user_input[CONF_HOST] in self._async_current_entries(): 113 | return self.async_abort(reason="already_configured") 114 | 115 | return self.async_create_entry( 116 | title=user_input[CONF_HOST], 117 | data={ 118 | CONF_HOST: user_input[CONF_HOST], 119 | CONF_NAME: user_input[CONF_NAME], 120 | CONF_USERNAME: user_input[CONF_USERNAME], 121 | CONF_PASSWORD: user_input[CONF_PASSWORD], 122 | }, 123 | options=DEFAULT_OPTIONS, 124 | ) 125 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Alfen Wallbox integration.""" 2 | 3 | DOMAIN = "alfen_wallbox" 4 | 5 | ID = "id" 6 | VALUE = "value" 7 | PROPERTIES = "properties" 8 | CAT = "cat" 9 | OFFSET = "offset" 10 | TOTAL = "total" 11 | 12 | METHOD_POST = "POST" 13 | METHOD_GET = "GET" 14 | 15 | CMD = "cmd" 16 | FORCE_UPDATE = "Force Update" 17 | PROP = "prop" 18 | INFO = "info" 19 | LOGIN = "login" 20 | LOGOUT = "logout" 21 | 22 | 23 | PARAM_USERNAME = "username" 24 | PARAM_PASSWORD = "password" 25 | PARAM_COMMAND = "command" 26 | PARAM_DISPLAY_NAME = "displayname" 27 | 28 | DISPLAY_NAME_VALUE = "ha" 29 | 30 | CAT_COMM = "comm" 31 | CAT_DISPLAY = "display" 32 | CAT_GENERIC = "generic" 33 | CAT_GENERIC2 = "generic2" 34 | CAT_MBUS_TCP = "MbusTCP" 35 | CAT_METER1 = "meter1" 36 | CAT_METER2 = "meter2" 37 | CAT_METER4 = "meter4" 38 | CAT_OCPP = "ocpp" 39 | CAT_STATES = "states" 40 | CAT_TEMP = "temp" 41 | # CAT_LEDS = "leds" 42 | # CAT_ACCELERO = "accelero" 43 | CAT_TRANSACTIONS = "transactions" 44 | CAT_LOGS = "logs" 45 | 46 | COMMAND_REBOOT = "reboot" 47 | COMMAND_CLEAR_TRANSACTIONS = "txerase" 48 | 49 | CONF_REFRESH_CATEGORIES = "refresh_categories" 50 | 51 | DEFAULT_REFRESH_CATEGORIES = ( 52 | CAT_COMM, 53 | CAT_DISPLAY, 54 | CAT_GENERIC, 55 | CAT_GENERIC2, 56 | CAT_MBUS_TCP, 57 | CAT_METER1, 58 | CAT_METER2, 59 | CAT_METER4, 60 | CAT_OCPP, 61 | CAT_STATES, 62 | CAT_TEMP, 63 | CAT_LOGS, 64 | ) 65 | 66 | CATEGORIES = ( 67 | CAT_COMM, 68 | CAT_DISPLAY, 69 | CAT_GENERIC, 70 | CAT_GENERIC2, 71 | CAT_MBUS_TCP, 72 | CAT_METER1, 73 | CAT_METER2, 74 | CAT_METER4, 75 | CAT_OCPP, 76 | CAT_STATES, 77 | CAT_TEMP, 78 | CAT_TRANSACTIONS, 79 | CAT_LOGS, 80 | ) 81 | 82 | # CONF_GENERIC = "get_generic" 83 | # CONF_GENERIC2 = "get_generic2" 84 | # CONF_METER1 = "get_meter1" 85 | # CONF_METER2 = "get_meter2" 86 | # CONF_METER4 = "get_meter4" 87 | # CONF_STATES = "states" 88 | # CONF_TEMP = "temp" 89 | # CONF_OCPP = "ocpp" 90 | # CONF_MBUSTCP = "MbusTCP" 91 | # CONF_COMM = "comm" 92 | # CONF_TRANSACTION_DATA = "display" 93 | 94 | DEFAULT_SCAN_INTERVAL = 5 95 | DEFAULT_TIMEOUT = 20 96 | 97 | SERVICE_REBOOT_WALLBOX = "reboot_wallbox" 98 | SERVICE_SET_CURRENT_LIMIT = "set_current_limit" 99 | SERVICE_ENABLE_RFID_AUTHORIZATION_MODE = "enable_rfid_authorization_mode" 100 | SERVICE_DISABLE_RFID_AUTHORIZATION_MODE = "disable_rfid_authorization_mode" 101 | SERVICE_SET_CURRENT_PHASE = "set_current_phase" 102 | SERVICE_ENABLE_PHASE_SWITCHING = "enable_phase_switching" 103 | SERVICE_DISABLE_PHASE_SWITCHING = "disable_phase_switching" 104 | SERVICE_SET_GREEN_SHARE = "set_green_share" 105 | SERVICE_SET_COMFORT_POWER = "set_comfort_power" 106 | 107 | ALFEN_PRODUCT_MAP = { 108 | "NG900-60503": "Eve Single S-line, 1 phase, LED, type 2 socket", 109 | "NG900-60505": "Eve Single S-line, 1 phase, LED, type 2 socket shutters", 110 | "NG900-60507": "Eve Single S-line, 1 phase, LED, tethered cable", 111 | "NG910-60003": "Eve Single Pro-line, 1 phase, display, type 2 socket", 112 | "NG910-60005": "Eve Single Pro-line FR, 1 phase, display, type 2 shutters", 113 | "NG910-60007": "Eve Single Pro-line, 1 phase, display, tethered cable", 114 | "NG910-60023": "Eve Single Pro-line, 3 phase, display, type 2 socket", 115 | "NG910-60025": "Eve Single Pro-line FR, 3 phase, display, type 2 shutters", 116 | "NG910-60027": "Eve Single Pro-line, 3 phase, display, tethered cable", 117 | "NG910-60123": "Eve Single Pro-Line DE, 3 phase, display, type 2 socket", 118 | "NG910-60127": "Eve Single Pro-Line DE, 3 phase, display, tethered cable", 119 | "NG910-60503": "Eve Single S-line, 1 phase, LED, type 2 socket", 120 | "NG910-60505": "Eve Single S-line, 1 phase, LED, type 2 shutters", 121 | "NG910-60507": "Eve Single S-line, 1 phase, LED, tethered cable", 122 | "NG910-60523": "Eve Single S-line, 3 phase, LED, type 2 socket", 123 | "NG910-60525": "Eve Single S-line, 3 phase, LED, type 2 shutters", 124 | "NG910-60527": "Eve Single S-line, 3 phase, LED, tethered cable", 125 | "NG910-60553": "Eve Single S-line, 1 phase, LED, RFID, type 2 socket", 126 | "NG910-60555": "Eve Single S-line, 3 phase, LED, RFID, type 2 shutters", 127 | "NG910-60557": "Eve Single S-line, 3 phase, LED, RFID, tethered cable", 128 | "NG910-60573": "Eve Single S-line, 3 phase, LED, GPRS, type 2 socket", 129 | "NG910-60575": "Eve Single S-line, 3 phase, LED, GPRS, type 2 shutters", 130 | "NG910-60577": "Eve Single S-line, 3 phase, LED, GPRS, tethered cable", 131 | "NG910-60583": "Eve Single S-line, 3 phase, LED, RFID, type 2 socket", 132 | "NG910-60585": "Eve Single S-line, 3 phase, LED, RFID, type 2 shutters", 133 | "NG910-60587": "Eve Single S-line, 3 phase, LED, RFID, type 2 tethered cable", 134 | "NG910-60593": "Eve Single S-line, 3 phase, LED, GPRS, type 2 socket", 135 | "NG910-60595": "Eve Single S-line, 3 phase, LED, GPRS, type 2 shutters", 136 | "NG910-60597": "Eve Single S-line, 3 phase, LED, GPRS, type 2 tethered cable", 137 | "NG920-61031": "Eve Double Pro-line, 2 x type 2 socket, 1 phase, max. 1x32A input current", 138 | "NG920-61032": "Eve Double Pro-line, 2 x type 2 socket, 2 phase, max. 1x32A input current", 139 | "NG920-61021": "Eve Double Pro-line, 2 x type 2 socket, 3 phase, max. 1x32A input current", 140 | "NG920-61022": "Eve Double Pro-line, 2 x type 2 socket, 3 phase, max. 2x32A input current", 141 | "NG920-61001": "Eve Double Pro-line, 3 phase, 2x socket Type 2, single feeder, RCD Type A", 142 | "NG920-61002": "Eve Double Pro-line, 3 phase, 2x socket Type 2, dual feeder, RCD Type A", 143 | "NG920-61011": "Eve Double Pro-line, 2 x type 2 socket, 1-phase, max. 1x32A input current, RCD B 3F 1C T2, Display", 144 | "NG920-61012": "Eve Double Pro-line, 2 x type 2 socket, 1-phase, max. 2x32A input current, RCD B 3F 1C T2, Display", 145 | "NG920-61101": "Eve Double Pro-line DE, 2 x type 2 socket, 3-phase, max. 1x32A input current, RCD B 3F 1C T2, Display", 146 | "NG920-61102": "Eve Double Pro-line DE, 2 x type 2 socket, 3-phase, max. 2x32A input current, RCD B 3F 1C T2, Display", 147 | "NG920-61205": "Eve Double Pro-line FR, 3 phase, Display, 2x socket Type 2S (shutters), max. 1x32A input current", 148 | "NG920-61206": "Eve Double Pro-line FR, 3 phase, Display, 2x socket Type 2S (shutters), max. 2x32A input current", 149 | "NG920-61215": "Eve Double Pro-line FR, 1 phase, Display, 2x socket Type 2S (shutters), max. 1x32A input current", 150 | "NG920-61216": "Eve Double Pro-line FR, 1 phase, Display, 2x socket Type 2S (shutters), max. 2x32A input current", 151 | } 152 | 153 | LICENSE_NONE = "None" 154 | LICENSE_SCN = "LoadBalancing_SCN" 155 | LICENSE_LOAD_BALANCING_STATIC = "LoadBalancing_Static" 156 | LICENSE_LOAD_BALANCING_ACTIVE = "LoadBalancing_Active" 157 | LICENSE_HIGH_POWER = "HighPowerSockets" 158 | LICENSE_RFID = "RFIDReader" 159 | LICENSE_PERSONALIZED_DISPLAY = "PersonalizedDisplay" 160 | LICENSE_MOBILE = "Mobile3G4G" 161 | LICENSE_PAYMENT_GIROE = "Payment_GiroE" 162 | LICENSE_PAYMENT_QRCODE = "Payment_QRCode" 163 | LICENSE_EXPOSE_SMARTMETERDATA = "Expose_SmartMeterData" 164 | LICENSE_OBJECTID = "ObjectID" 165 | 166 | LICENSES = { 167 | LICENSE_NONE: 0, 168 | LICENSE_SCN: 1, 169 | LICENSE_LOAD_BALANCING_STATIC: 2, 170 | LICENSE_LOAD_BALANCING_ACTIVE: 4, 171 | LICENSE_HIGH_POWER: 16, 172 | LICENSE_RFID: 256, 173 | LICENSE_PERSONALIZED_DISPLAY: 4096, 174 | LICENSE_MOBILE: 65536, 175 | LICENSE_PAYMENT_GIROE: 1048576, 176 | LICENSE_PAYMENT_QRCODE: 131072, 177 | LICENSE_EXPOSE_SMARTMETERDATA: 16777216, 178 | LICENSE_OBJECTID: 2147483648, 179 | } 180 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/coordinator.py: -------------------------------------------------------------------------------- 1 | """Class representing a Alfen Wallbox update coordinator.""" 2 | 3 | import asyncio 4 | from asyncio import timeout 5 | from datetime import timedelta 6 | import logging 7 | from ssl import CERT_NONE 8 | 9 | from aiohttp import ClientConnectionError 10 | 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.const import ( 13 | CONF_HOST, 14 | CONF_NAME, 15 | CONF_PASSWORD, 16 | CONF_SCAN_INTERVAL, 17 | CONF_TIMEOUT, 18 | CONF_USERNAME, 19 | ) 20 | from homeassistant.core import HomeAssistant 21 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 22 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 23 | from homeassistant.util.ssl import get_default_context 24 | 25 | from .alfen import AlfenDevice 26 | from .const import ( 27 | CONF_REFRESH_CATEGORIES, 28 | DEFAULT_REFRESH_CATEGORIES, 29 | DEFAULT_SCAN_INTERVAL, 30 | DEFAULT_TIMEOUT, 31 | DOMAIN, 32 | ) 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | type AlfenConfigEntry = ConfigEntry[AlfenCoordinator] 37 | 38 | 39 | class AlfenCoordinator(DataUpdateCoordinator[None]): 40 | """Alfen update coordinator.""" 41 | 42 | def __init__(self, hass: HomeAssistant, entry: AlfenConfigEntry) -> None: 43 | """Initialize the coordinator.""" 44 | super().__init__( 45 | hass, 46 | _LOGGER, 47 | name=DOMAIN, 48 | update_interval=timedelta( 49 | seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) 50 | ), 51 | ) 52 | 53 | self.entry = entry 54 | self.hass = hass 55 | self.device = None 56 | self.timeout = self.entry.options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) 57 | 58 | async def _async_setup(self): 59 | """Set up the coordinator.""" 60 | session = async_get_clientsession(self.hass, verify_ssl=False) 61 | 62 | # Default ciphers needed as of python 3.10 63 | context = get_default_context() 64 | 65 | context.set_ciphers("DEFAULT") 66 | context.check_hostname = False 67 | context.verify_mode = CERT_NONE 68 | 69 | self.device = AlfenDevice( 70 | session, 71 | self.entry.data[CONF_HOST], 72 | self.entry.data[CONF_NAME], 73 | self.entry.data[CONF_USERNAME], 74 | self.entry.data[CONF_PASSWORD], 75 | self.entry.options.get(CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES), 76 | context, 77 | ) 78 | if not await self.async_connect(): 79 | raise UpdateFailed("Error communicating with API") 80 | 81 | async def _async_update_data(self) -> None: 82 | """Fetch data from API endpoint.""" 83 | try: 84 | async with timeout(self.timeout): 85 | if not await self.device.async_update(): 86 | raise UpdateFailed("Error updating") 87 | except TimeoutError: 88 | _LOGGER.debug("Update from %s timed out", self.entry.data[CONF_HOST]) 89 | # wait for next update 90 | # await for 60 seconds to avoid flooding the API 91 | await asyncio.sleep(60) 92 | self.device.lock = False 93 | 94 | async def async_connect(self) -> bool: 95 | """Connect to the API endpoint.""" 96 | 97 | try: 98 | async with timeout(self.timeout): 99 | return await self.device.init() 100 | except TimeoutError: 101 | _LOGGER.debug("Connection to %s timed out", self.entry.data[CONF_HOST]) 102 | return False 103 | except ClientConnectionError as e: 104 | _LOGGER.debug( 105 | "ClientConnectionError to %s %s", 106 | self.entry.data[CONF_HOST], 107 | str(e), 108 | ) 109 | return False 110 | except Exception as e: # pylint: disable=broad-except # noqa: BLE001 111 | _LOGGER.error( 112 | "Unexpected error creating device %s %s", 113 | self.entry.data[CONF_HOST], 114 | str(e), 115 | ) 116 | return False 117 | 118 | 119 | async def options_update_listener(self, entry: AlfenConfigEntry): 120 | """Handle options update.""" 121 | coordinator = entry.runtime_data 122 | coordinator.device.get_static_properties = True 123 | coordinator.device.category_options = entry.options.get( 124 | CONF_REFRESH_CATEGORIES, DEFAULT_REFRESH_CATEGORIES 125 | ) 126 | 127 | coordinator.update_interval = timedelta( 128 | seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) 129 | ) 130 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostics support for Alfen.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | from homeassistant.core import HomeAssistant 8 | 9 | from .coordinator import AlfenConfigEntry 10 | 11 | 12 | async def async_get_config_entry_diagnostics( 13 | hass: HomeAssistant, entry: AlfenConfigEntry 14 | ) -> dict[str, Any]: 15 | """Return diagnostics for a config entry.""" 16 | device = entry.runtime_data.device 17 | return { 18 | "id": device.id, 19 | "name": device.name, 20 | "info": vars(device.info), 21 | "keep_logout": device.keep_logout, 22 | "max_allowed_phases": device.max_allowed_phases, 23 | "number_socket": device.get_number_of_sockets(), 24 | "licenses": device.get_licenses(), 25 | "category_options": device.category_options, 26 | "properties": device.properties, 27 | } 28 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/entity.py: -------------------------------------------------------------------------------- 1 | """Base entity for Alfen Wallbox integration.""" 2 | 3 | from homeassistant.helpers.entity import DeviceInfo, Entity 4 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 5 | 6 | from .const import DOMAIN as ALFEN_DOMAIN 7 | from .coordinator import AlfenConfigEntry, AlfenCoordinator 8 | 9 | 10 | class AlfenEntity(CoordinatorEntity[AlfenCoordinator], Entity): 11 | """Define a base Alfen entity.""" 12 | 13 | def __init__(self, entry: AlfenConfigEntry) -> None: 14 | """Initialize the Alfen entity.""" 15 | 16 | super().__init__(entry) 17 | self.coordinator = entry.runtime_data 18 | 19 | self._attr_device_info = DeviceInfo( 20 | identifiers={(ALFEN_DOMAIN, self.coordinator.device.name)}, 21 | manufacturer="Alfen", 22 | model=self.coordinator.device.info.model, 23 | name=self.coordinator.device.name, 24 | sw_version=self.coordinator.device.info.firmware_version, 25 | ) 26 | 27 | async def async_added_to_hass(self) -> None: 28 | """Add listener for state changes.""" 29 | await super().async_added_to_hass() 30 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "alfen_wallbox", 3 | "name": "Alfen Wallbox", 4 | "codeowners": ["leeyuentuen"], 5 | "dependencies": [], 6 | "documentation": "https://github.com/leeyuentuen/alfen_wallbox", 7 | "integration_type": "hub", 8 | "iot_class": "local_polling", 9 | "issue_tracker": "https://github.com/leeyuentuen/alfen_wallbox/issues", 10 | "requirements": [], 11 | "config_flow": true, 12 | "version": "2.9.7" 13 | } 14 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/number.py: -------------------------------------------------------------------------------- 1 | """Support for Alfen Eve Proline Wallbox.""" 2 | 3 | from dataclasses import dataclass 4 | import logging 5 | from typing import Final 6 | 7 | import voluptuous as vol 8 | 9 | from homeassistant.components.number import ( 10 | NumberDeviceClass, 11 | NumberEntity, 12 | NumberEntityDescription, 13 | NumberMode, 14 | ) 15 | from homeassistant.const import ( 16 | CURRENCY_EURO, 17 | PERCENTAGE, 18 | UnitOfElectricCurrent, 19 | UnitOfPower, 20 | UnitOfTime, 21 | ) 22 | from homeassistant.core import HomeAssistant 23 | from homeassistant.helpers import config_validation as cv, entity_platform 24 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 25 | 26 | from .const import ( 27 | CAT, 28 | ID, 29 | LICENSE_HIGH_POWER, 30 | SERVICE_SET_COMFORT_POWER, 31 | SERVICE_SET_CURRENT_LIMIT, 32 | SERVICE_SET_GREEN_SHARE, 33 | VALUE, 34 | ) 35 | from .coordinator import AlfenConfigEntry 36 | from .entity import AlfenEntity 37 | 38 | _LOGGER = logging.getLogger(__name__) 39 | 40 | 41 | @dataclass 42 | class AlfenNumberDescriptionMixin: 43 | """Define an entity description mixin for select entities.""" 44 | 45 | assumed_state: bool 46 | state: float 47 | api_param: str 48 | custom_mode: str 49 | round_digits: int | None 50 | 51 | 52 | @dataclass 53 | class AlfenNumberDescription(NumberEntityDescription, AlfenNumberDescriptionMixin): 54 | """Class to describe an Alfen select entity.""" 55 | 56 | 57 | ALFEN_NUMBER_TYPES: Final[tuple[AlfenNumberDescription, ...]] = ( 58 | AlfenNumberDescription( 59 | key="alb_safe_current", 60 | name="Load Balancing Safe Current", 61 | state=None, 62 | icon="mdi:current-ac", 63 | assumed_state=False, 64 | device_class=NumberDeviceClass.CURRENT, 65 | native_min_value=1, 66 | native_max_value=16, 67 | native_step=1, 68 | custom_mode=None, 69 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 70 | api_param="2068_0", 71 | round_digits=None, 72 | ), 73 | AlfenNumberDescription( 74 | key="main_normal_max_current_socket_1", 75 | name="Power Connector Max Current Socket 1", 76 | state=None, 77 | icon="mdi:current-ac", 78 | assumed_state=False, 79 | device_class=NumberDeviceClass.CURRENT, 80 | native_min_value=0, 81 | native_max_value=16, 82 | native_step=1, 83 | custom_mode=None, 84 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 85 | api_param="2129_0", 86 | round_digits=None, 87 | ), 88 | AlfenNumberDescription( 89 | key="max_station_current", 90 | name="Max. Station Current", 91 | state=None, 92 | icon="mdi:current-ac", 93 | assumed_state=False, 94 | device_class=NumberDeviceClass.CURRENT, 95 | native_min_value=0, 96 | native_max_value=16, 97 | native_step=1, 98 | custom_mode=None, 99 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 100 | api_param="2062_0", 101 | round_digits=None, 102 | ), 103 | AlfenNumberDescription( 104 | key="lb_max_smart_meter_current", 105 | name="Load Balancing Max. Meter Current", 106 | state=None, 107 | icon="mdi:current-ac", 108 | assumed_state=False, 109 | device_class=NumberDeviceClass.CURRENT, 110 | native_min_value=0, 111 | native_max_value=40, 112 | native_step=1, 113 | custom_mode=None, 114 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 115 | api_param="2067_0", 116 | round_digits=None, 117 | ), 118 | AlfenNumberDescription( 119 | key="lb_solar_charging_green_share", 120 | name="Solar Green Share", 121 | state=None, 122 | icon="mdi:current-ac", 123 | assumed_state=False, 124 | device_class=NumberDeviceClass.POWER_FACTOR, 125 | native_min_value=0, 126 | native_max_value=100, 127 | native_step=1, 128 | custom_mode=None, 129 | unit_of_measurement=PERCENTAGE, 130 | api_param="3280_2", 131 | round_digits=None, 132 | ), 133 | AlfenNumberDescription( 134 | key="lb_solar_charging_comfort_level", 135 | name="Solar Comfort Level", 136 | state=None, 137 | icon="mdi:current-ac", 138 | assumed_state=False, 139 | device_class=NumberDeviceClass.POWER_FACTOR, 140 | native_min_value=1350, 141 | native_max_value=11000, 142 | native_step=50, 143 | custom_mode=None, 144 | unit_of_measurement=UnitOfPower.WATT, 145 | api_param="3280_3", 146 | round_digits=None, 147 | ), 148 | AlfenNumberDescription( 149 | key="dp_light_intensity", 150 | name="Display Light Intensity %", 151 | state=None, 152 | icon="mdi:lightbulb", 153 | assumed_state=False, 154 | device_class=NumberDeviceClass.POWER_FACTOR, 155 | native_min_value=0, 156 | native_max_value=100, 157 | native_step=10, 158 | custom_mode=None, 159 | unit_of_measurement=PERCENTAGE, 160 | api_param="2061_2", 161 | round_digits=None, 162 | ), 163 | AlfenNumberDescription( 164 | key="ps_installation_max_imbalance_current", 165 | name="Installation Max. Imbalance Current between phases", 166 | state=None, 167 | icon="mdi:current-ac", 168 | assumed_state=False, 169 | device_class=NumberDeviceClass.POWER_FACTOR, 170 | native_min_value=0, 171 | native_max_value=10, 172 | native_step=1, 173 | custom_mode=None, 174 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 175 | api_param="2174_0", 176 | round_digits=None, 177 | ), 178 | AlfenNumberDescription( 179 | key="lb_Charging_profiles_random_delay", 180 | name="Load Balancing Charging profiles random delay", 181 | state=None, 182 | icon="mdi:timer-sand", 183 | assumed_state=False, 184 | device_class=NumberDeviceClass.POWER_FACTOR, 185 | native_min_value=0, 186 | native_max_value=30, 187 | native_step=1, 188 | custom_mode=None, 189 | unit_of_measurement=UnitOfTime.SECONDS, 190 | api_param="21B9_0", 191 | round_digits=None, 192 | ), 193 | AlfenNumberDescription( 194 | key="auth_re_authorize_after_power_outage", 195 | name="Auth. Re-authorize after Power Outage (s)", 196 | state=None, 197 | icon="mdi:timer-sand", 198 | assumed_state=False, 199 | device_class=None, 200 | native_min_value=0, 201 | native_max_value=30, 202 | native_step=1, 203 | custom_mode=None, 204 | unit_of_measurement=UnitOfTime.SECONDS, 205 | api_param="2169_0", 206 | round_digits=None, 207 | ), 208 | AlfenNumberDescription( 209 | key="auth_connection_timeout", 210 | name="Auth. Connection Timeout (s)", 211 | state=None, 212 | icon="mdi:timer-sand", 213 | assumed_state=False, 214 | device_class=None, 215 | native_min_value=0, 216 | native_max_value=30, 217 | native_step=1, 218 | custom_mode=None, 219 | unit_of_measurement=UnitOfTime.SECONDS, 220 | api_param="2169_0", 221 | round_digits=None, 222 | ), 223 | AlfenNumberDescription( 224 | key="ws_wired_socket_timeout", 225 | name="WS Wired websocket timeout (s)", 226 | state=None, 227 | icon="mdi:timer-sand", 228 | assumed_state=False, 229 | device_class=None, 230 | native_min_value=0, 231 | native_max_value=30, 232 | native_step=1, 233 | custom_mode=None, 234 | unit_of_measurement=UnitOfTime.SECONDS, 235 | api_param="208B_1", 236 | round_digits=None, 237 | ), 238 | AlfenNumberDescription( 239 | key="ws_mobile_socket_timeout", 240 | name="WS Mobile websocket timeout (s)", 241 | state=None, 242 | icon="mdi:timer-sand", 243 | assumed_state=False, 244 | device_class=None, 245 | native_min_value=0, 246 | native_max_value=30, 247 | native_step=1, 248 | custom_mode=None, 249 | unit_of_measurement=UnitOfTime.SECONDS, 250 | api_param="208B_2", 251 | round_digits=None, 252 | ), 253 | AlfenNumberDescription( 254 | key="ocpp_wired_ocpp_send_timeout", 255 | name="OCPP Wired OCPP send timeout (s)", 256 | state=None, 257 | icon="mdi:timer-sand", 258 | assumed_state=False, 259 | device_class=None, 260 | native_min_value=0, 261 | native_max_value=30, 262 | native_step=1, 263 | custom_mode=None, 264 | unit_of_measurement=UnitOfTime.SECONDS, 265 | api_param="208D_1", 266 | round_digits=None, 267 | ), 268 | AlfenNumberDescription( 269 | key="ocpp_mobile_ocpp_send_timeout", 270 | name="OCPP Mobile OCPP send timeout (s)", 271 | state=None, 272 | icon="mdi:timer-sand", 273 | assumed_state=False, 274 | device_class=None, 275 | native_min_value=0, 276 | native_max_value=30, 277 | native_step=1, 278 | custom_mode=None, 279 | unit_of_measurement=UnitOfTime.SECONDS, 280 | api_param="208D_2", 281 | round_digits=None, 282 | ), 283 | AlfenNumberDescription( 284 | key="ocpp_wired_ocpp_reply_timeout", 285 | name="OCPP Wired OCPP reply timeout (s)", 286 | state=None, 287 | icon="mdi:timer-sand", 288 | assumed_state=False, 289 | device_class=None, 290 | native_min_value=0, 291 | native_max_value=30, 292 | native_step=1, 293 | custom_mode=None, 294 | unit_of_measurement=UnitOfTime.SECONDS, 295 | api_param="208E_1", 296 | round_digits=None, 297 | ), 298 | AlfenNumberDescription( 299 | key="ocpp_mobile_ocpp_reply_timeout", 300 | name="OCPP Mobile OCPP reply timeout (s)", 301 | state=None, 302 | icon="mdi:timer-sand", 303 | assumed_state=False, 304 | device_class=None, 305 | native_min_value=0, 306 | native_max_value=30, 307 | native_step=1, 308 | custom_mode=None, 309 | unit_of_measurement=UnitOfTime.SECONDS, 310 | api_param="208E_1", 311 | round_digits=None, 312 | ), 313 | AlfenNumberDescription( 314 | key="heartbeat_interval", 315 | name="Heartbeat interval (s)", 316 | state=None, 317 | icon="mdi:timer-sand", 318 | assumed_state=False, 319 | device_class=None, 320 | native_min_value=0, 321 | native_max_value=9000, 322 | native_step=100, 323 | custom_mode=NumberMode.BOX, 324 | unit_of_measurement=UnitOfTime.SECONDS, 325 | api_param="2086_0", 326 | round_digits=None, 327 | ), 328 | AlfenNumberDescription( 329 | key="price_start_tariff", 330 | name="Price Start Tariff", 331 | state=None, 332 | icon="mdi:currency-eur", 333 | assumed_state=False, 334 | device_class=None, 335 | native_min_value=0, 336 | native_max_value=5, 337 | native_step=0.01, 338 | custom_mode=NumberMode.BOX, 339 | unit_of_measurement=CURRENCY_EURO, 340 | api_param="3262_2", 341 | round_digits=2, 342 | ), 343 | AlfenNumberDescription( 344 | key="price_price_per_kwh", 345 | name="Price per kWh", 346 | state=None, 347 | icon="mdi:currency-eur", 348 | assumed_state=False, 349 | device_class=None, 350 | native_min_value=0, 351 | native_max_value=5, 352 | native_step=0.01, 353 | custom_mode=NumberMode.BOX, 354 | unit_of_measurement=CURRENCY_EURO, 355 | api_param="3262_3", 356 | round_digits=2, 357 | ), 358 | AlfenNumberDescription( 359 | key="price_price_per_minute", 360 | name="Price per minute", 361 | state=None, 362 | icon="mdi:currency-eur", 363 | assumed_state=False, 364 | device_class=None, 365 | native_min_value=0, 366 | native_max_value=5, 367 | native_step=0.01, 368 | custom_mode=NumberMode.BOX, 369 | unit_of_measurement=CURRENCY_EURO, 370 | api_param="3262_4", 371 | round_digits=2, 372 | ), 373 | AlfenNumberDescription( 374 | key="price_price_other", 375 | name="Price other", 376 | state=None, 377 | icon="mdi:currency-eur", 378 | assumed_state=False, 379 | device_class=None, 380 | native_min_value=-5, 381 | native_max_value=5, 382 | native_step=0.01, 383 | custom_mode=NumberMode.BOX, 384 | unit_of_measurement=CURRENCY_EURO, 385 | api_param="3262_6", 386 | round_digits=2, 387 | ), 388 | AlfenNumberDescription( 389 | key="ev_disconnection_timeout", 390 | name="Car Disconnection Timeout (s)", 391 | state=None, 392 | icon="mdi:timer-sand", 393 | assumed_state=False, 394 | device_class=None, 395 | native_min_value=0, 396 | native_max_value=30, 397 | native_step=1, 398 | custom_mode=None, 399 | unit_of_measurement=UnitOfTime.SECONDS, 400 | api_param="2136_0", 401 | round_digits=None, 402 | ), 403 | AlfenNumberDescription( 404 | key="ev_non_charging_report_threshold", 405 | name="Car Time to Report Not Charging (s)", 406 | state=None, 407 | icon="mdi:timer-sand", 408 | assumed_state=False, 409 | device_class=None, 410 | native_min_value=0, 411 | native_max_value=30, 412 | native_step=1, 413 | custom_mode=None, 414 | unit_of_measurement=UnitOfTime.SECONDS, 415 | api_param="2184_0", 416 | round_digits=None, 417 | ), 418 | AlfenNumberDescription( 419 | key="ev_auto_stop_transaction_time", 420 | name="Car Time to Unlock Not Charging (s)", 421 | state=None, 422 | icon="mdi:timer-sand", 423 | assumed_state=False, 424 | device_class=None, 425 | native_min_value=0, 426 | native_max_value=30, 427 | native_step=1, 428 | custom_mode=None, 429 | unit_of_measurement=UnitOfTime.SECONDS, 430 | api_param="2168_0", 431 | round_digits=None, 432 | ), 433 | AlfenNumberDescription( 434 | key="main_external_max_current_socket_1", 435 | name="Power Connector External Max Current Socket 1", 436 | state=None, 437 | icon="mdi:current-ac", 438 | assumed_state=False, 439 | device_class=NumberDeviceClass.CURRENT, 440 | native_min_value=0, 441 | native_max_value=16, 442 | native_step=1, 443 | custom_mode=None, 444 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 445 | api_param="212A_0", 446 | round_digits=None, 447 | ), 448 | AlfenNumberDescription( 449 | key="minimum_chameleon_current", 450 | name="Minimum Chameleon Current", 451 | state=None, 452 | icon="mdi:current-ac", 453 | assumed_state=False, 454 | device_class=NumberDeviceClass.CURRENT, 455 | native_min_value=6, 456 | native_max_value=32, 457 | native_step=1, 458 | custom_mode=None, 459 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 460 | api_param="206A_0", 461 | round_digits=None, 462 | ), 463 | AlfenNumberDescription( 464 | key="timzone_offset", 465 | name="timezone", 466 | state=None, 467 | icon="mdi:clock", 468 | assumed_state=False, 469 | device_class=None, 470 | native_min_value=-720, 471 | native_max_value=720, 472 | native_step=60, 473 | custom_mode=None, 474 | unit_of_measurement=UnitOfTime.MINUTES, 475 | api_param="206E_0", 476 | round_digits=None, 477 | ), 478 | AlfenNumberDescription( 479 | key="alarm_temperature_high", 480 | name="Alarm Temperature High", 481 | state=None, 482 | icon="mdi:thermometer", 483 | assumed_state=False, 484 | device_class=None, 485 | native_min_value=-50, 486 | native_max_value=100, 487 | native_step=1, 488 | custom_mode=None, 489 | unit_of_measurement=None, 490 | api_param="2203_0", 491 | round_digits=None, 492 | ), 493 | AlfenNumberDescription( 494 | key="alarm_temperature_low", 495 | name="Alarm Temperature low", 496 | state=None, 497 | icon="mdi:thermometer", 498 | assumed_state=False, 499 | device_class=None, 500 | native_min_value=-50, 501 | native_max_value=100, 502 | native_step=1, 503 | custom_mode=None, 504 | unit_of_measurement=None, 505 | api_param="2204_0", 506 | round_digits=None, 507 | ), 508 | ) 509 | 510 | ALFEN_NUMBER_DUAL_SOCKET_TYPES: Final[tuple[AlfenNumberDescription, ...]] = ( 511 | AlfenNumberDescription( 512 | key="main_normal_max_current_socket_2", 513 | name="Power Connector Max Current Socket 2", 514 | state=None, 515 | icon="mdi:current-ac", 516 | assumed_state=False, 517 | device_class=NumberDeviceClass.CURRENT, 518 | native_min_value=0, 519 | native_max_value=16, 520 | native_step=1, 521 | custom_mode=None, 522 | unit_of_measurement=UnitOfElectricCurrent.AMPERE, 523 | api_param="3129_0", 524 | round_digits=None, 525 | ), 526 | ) 527 | 528 | 529 | async def async_setup_entry( 530 | hass: HomeAssistant, 531 | entry: AlfenConfigEntry, 532 | async_add_entities: AddEntitiesCallback, 533 | ) -> None: 534 | """Set up Alfen select entities from a config entry.""" 535 | numbers = [AlfenNumber(entry, description) for description in ALFEN_NUMBER_TYPES] 536 | 537 | async_add_entities(numbers) 538 | 539 | coordinator = entry.runtime_data 540 | if coordinator.device.get_number_of_sockets() == 2: 541 | numbers = [ 542 | AlfenNumber(entry, description) 543 | for description in ALFEN_NUMBER_DUAL_SOCKET_TYPES 544 | ] 545 | async_add_entities(numbers) 546 | 547 | platform = entity_platform.current_platform.get() 548 | 549 | platform.async_register_entity_service( 550 | SERVICE_SET_CURRENT_LIMIT, 551 | { 552 | vol.Required("limit"): cv.positive_int, 553 | }, 554 | "async_set_current_limit", 555 | ) 556 | 557 | platform.async_register_entity_service( 558 | SERVICE_SET_GREEN_SHARE, 559 | { 560 | vol.Required(VALUE): cv.positive_int, 561 | }, 562 | "async_set_green_share", 563 | ) 564 | 565 | platform.async_register_entity_service( 566 | SERVICE_SET_COMFORT_POWER, 567 | { 568 | vol.Required(VALUE): cv.positive_int, 569 | }, 570 | "async_set_comfort_power", 571 | ) 572 | 573 | 574 | class AlfenNumber(AlfenEntity, NumberEntity): 575 | """Define an Alfen select entity.""" 576 | 577 | _attr_has_entity_name = True 578 | _attr_name = None 579 | _attr_should_poll = False 580 | entity_description: AlfenNumberDescription 581 | 582 | def __init__( 583 | self, 584 | entry: AlfenConfigEntry, 585 | description: AlfenNumberDescription, 586 | ) -> None: 587 | """Initialize the Alfen Number entity.""" 588 | super().__init__(entry) 589 | self._attr_name = f"{description.name}" 590 | self._attr_unique_id = f"{self.coordinator.device.id}_{description.key}" 591 | self._attr_assumed_state = description.assumed_state 592 | self._attr_device_class = description.device_class 593 | self._attr_icon = description.icon 594 | if ( 595 | description.custom_mode is None 596 | ): # issue with pre Home Assistant Core 2023.6 versions 597 | self._attr_mode = NumberMode.SLIDER 598 | else: 599 | self._attr_mode = description.custom_mode 600 | self._attr_native_unit_of_measurement = description.unit_of_measurement 601 | self._attr_native_value = description.state 602 | self.entity_description = description 603 | 604 | if description.native_min_value is not None: 605 | self._attr_min_value = description.native_min_value 606 | self._attr_native_min_value = description.native_min_value 607 | if description.native_max_value is not None: 608 | self._attr_max_value = description.native_max_value 609 | self._attr_native_max_value = description.native_max_value 610 | if description.native_step is not None: 611 | self._attr_native_step = description.native_step 612 | 613 | # override the amps and set them on 32A if there is a license for it 614 | override_amps_api_key = ["2068_0", "2129_0", "2062_0", "3129_0", "212A_0"] 615 | # check if device licenses has the high power socket license 616 | if LICENSE_HIGH_POWER in self.coordinator.device.get_licenses(): 617 | if description.api_param in override_amps_api_key: 618 | self._attr_max_value = 40 619 | self._attr_native_max_value = 40 620 | 621 | @property 622 | def native_value(self) -> float | None: 623 | """Return the entity value to represent the entity state.""" 624 | return self._get_current_option() 625 | 626 | async def async_set_native_value(self, value: float) -> None: 627 | """Update the current value.""" 628 | if self.entity_description.round_digits is not None: 629 | self.coordinator.device.set_value( 630 | self.entity_description.api_param, 631 | round(float(value), self.entity_description.round_digits), 632 | ) 633 | else: 634 | self.coordinator.device.set_value( 635 | self.entity_description.api_param, int(value) 636 | ) 637 | self._set_current_option() 638 | 639 | @property 640 | def extra_state_attributes(self): 641 | """Return the default attributes of the element.""" 642 | if self.entity_description.api_param in self.coordinator.device.properties: 643 | return { 644 | "category": self.coordinator.device.properties[ 645 | self.entity_description.api_param 646 | ][CAT] 647 | } 648 | 649 | return None 650 | 651 | def _get_current_option(self) -> str | None: 652 | """Return the current option.""" 653 | if self.entity_description.api_param in self.coordinator.device.properties: 654 | # _LOGGER.debug("%s Value: %s", self.entity_description.name, prop[VALUE]) 655 | prop = self.coordinator.device.properties[self.entity_description.api_param] 656 | if self.entity_description.round_digits is not None: 657 | return round(prop[VALUE], self.entity_description.round_digits) 658 | 659 | # change comfort level depends on max allowed phase 660 | if self.entity_description.key == "lb_solar_charging_comfort_level": 661 | if self.coordinator.device.max_allowed_phases == 3: 662 | self._attr_max_value = self.entity_description.native_max_value 663 | self._attr_native_max_value = ( 664 | self.entity_description.native_max_value 665 | ) 666 | else: 667 | self._attr_max_value = 3300 668 | self._attr_native_max_value = 3300 669 | 670 | return prop[VALUE] 671 | return None 672 | 673 | def _set_current_option(self): 674 | """Set the current option.""" 675 | self._attr_native_value = self._get_current_option() 676 | self.async_write_ha_state() 677 | 678 | async def async_set_current_limit(self, limit): 679 | """Set the current limit.""" 680 | await self.coordinator.device.set_current_limit(limit) 681 | self._set_current_option() 682 | 683 | async def async_set_green_share(self, value): 684 | """Set the green share.""" 685 | await self.coordinator.device.set_green_share(value) 686 | self._set_current_option() 687 | 688 | async def async_set_comfort_power(self, value): 689 | """Set the comfort power.""" 690 | await self.coordinator.device.set_comfort_power(value) 691 | self._set_current_option() 692 | 693 | async def async_update(self): 694 | """Get the latest data and updates the states.""" 695 | self._set_current_option() 696 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/select.py: -------------------------------------------------------------------------------- 1 | """Alfen Wallbox Select Entities.""" 2 | 3 | from dataclasses import dataclass 4 | from typing import Final 5 | 6 | import voluptuous as vol 7 | 8 | from homeassistant.components.select import SelectEntity, SelectEntityDescription 9 | from homeassistant.core import HomeAssistant, callback 10 | from homeassistant.helpers import entity_platform 11 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 12 | 13 | from .const import ( 14 | CAT, 15 | ID, 16 | SERVICE_DISABLE_RFID_AUTHORIZATION_MODE, 17 | SERVICE_ENABLE_RFID_AUTHORIZATION_MODE, 18 | SERVICE_SET_CURRENT_PHASE, 19 | VALUE, 20 | ) 21 | from .coordinator import AlfenConfigEntry 22 | from .entity import AlfenEntity 23 | 24 | 25 | @dataclass 26 | class AlfenSelectDescriptionMixin: 27 | """Define an entity description mixin for select entities.""" 28 | 29 | api_param: str 30 | options_dict: dict[str, int] 31 | 32 | 33 | @dataclass 34 | class AlfenSelectDescription(SelectEntityDescription, AlfenSelectDescriptionMixin): 35 | """Class to describe an Alfen select entity.""" 36 | 37 | 38 | CHARGING_MODE_DICT: Final[dict[str, int]] = {"Disable": 0, "Comfort": 1, "Green": 2} 39 | 40 | PHASE_ROTATION_DICT: Final[dict[str, str]] = { 41 | "L1": "L1", 42 | "L2": "L2", 43 | "L3": "L3", 44 | "L1,L2,L3": "L1L2L3", 45 | "L1,L3,L2": "L1L3L2", 46 | "L2,L1,L3": "L2L1L3", 47 | "L2,L3,L1": "L2L3L1", 48 | "L3,L1,L2": "L3L1L2", 49 | "L3,L2,L1": "L3L2L1", 50 | } 51 | 52 | AUTH_MODE_DICT: Final[dict[str, int]] = {"Plug and Charge": 0, "RFID": 2} 53 | 54 | LOAD_BALANCE_PROTOCOL_DICT: Final[dict[str, int]] = { 55 | "Energy Management System": -1, 56 | "Modbus TCP/IP": 4, 57 | "DSMR4.x/SMR5.0 (P1)": 5, 58 | } 59 | 60 | LOAD_BALANCE_DATA_SOURCE_DICT: Final[dict[str, int]] = { 61 | "Meter": 0, 62 | "Meter + EMS Monitoring": 1, 63 | "Energy Management System": 3, 64 | } 65 | 66 | LOAD_BALANCE_RECEIVED_MEASUREMENTS_DICT: Final[dict[str, int]] = { 67 | "Exclude Charging Ev": 0, 68 | "Include Charging Ev": 1, 69 | } 70 | 71 | DISPLAY_LANGUAGE_DICT: Final[dict[str, str]] = { 72 | "Catalan": "ca_ES", 73 | "Croatian": "hr_HR", 74 | "Czech": "cz_CZ", 75 | "Danish": "da_DK", 76 | "Dutch": "nl_NL", 77 | "English": "en_GB", 78 | "Finnish": "fi_FI", 79 | "French": "fr_FR", 80 | "German": "de_DE", 81 | "Hungarian": "hu_HU", 82 | "Icelandic": "is_IS", 83 | "Italian": "it_IT", 84 | "Latvian": "lv_LV", 85 | "Norwegian": "no_NO", 86 | "Polish": "pl_PL", 87 | "Portuguese": "pt_PT", 88 | "Romanian": "ro_RO", 89 | "Slovak": "sk_SK", 90 | "Spanish": "es_ES", 91 | "Swedish": "sv_SE", 92 | } 93 | 94 | ALLOWED_PHASE_DICT: Final[dict[str, int]] = { 95 | "1 Phase": 1, 96 | "3 Phases": 3, 97 | } 98 | 99 | PRIORITIES_DICT: Final[dict[str, int]] = {"Disable": 0, "1": 1, "2": 2, "3": 3, "4": 4} 100 | 101 | OPERATIVE_MODE_DICT: Final[dict[str, int]] = { 102 | "Operative": 0, 103 | "In-operative": 2, 104 | } 105 | 106 | GPRS_NETWORK_MODE_DICT: Final[dict[str, int]] = {"Automatic": 0, "Manual": 1} 107 | 108 | GPRS_TECHNOLOGY_DICT: Final[dict[str, int]] = { 109 | "2G (GPRS)": 0, 110 | "3G (UMTS)": 1, 111 | "4G (LTE)": 2, 112 | } 113 | 114 | DSMR_SMR_INTERFACE_DICT: Final[dict[str, int]] = { 115 | "Serial": 0, 116 | "Telnet": 1, 117 | "HomeWizard Wi-Fi P1": 2, 118 | } 119 | 120 | DIRECT_EXTERNAL_SUSPEND_SIGNAL: Final[dict[str, int]] = { 121 | "Not allowed": 0, 122 | "Allowed, suspend when closed": 1, 123 | "Allowed, suspend when open": 2, 124 | } 125 | 126 | SOCKET_TYPE_DICT: Final[dict[str, int]] = { 127 | "Fixed Cable Unknown": 0, 128 | "Mennekes": 1, 129 | "FCT": 2, 130 | "Schuko": 3, 131 | "FIXED_CABLE_TYPE_1": 4, 132 | "FIXED_CABLE_TYPE_2": 5, 133 | "UNKNOWN": 99, 134 | } 135 | 136 | CAR_DISCONNECT_ACTION_DICT: Final[dict[str, int]] = { 137 | "Continue": 0, 138 | "Abort Lock": 1, 139 | "Abort Unlock": 2, 140 | "Abort Unlock When Offline": 3, 141 | } 142 | 143 | ALFEN_SELECT_TYPES: Final[tuple[AlfenSelectDescription, ...]] = ( 144 | AlfenSelectDescription( 145 | key="lb_solar_charging_mode", 146 | name="Solar Charging Mode", 147 | icon="mdi:solar-power", 148 | options=list(CHARGING_MODE_DICT), 149 | options_dict=CHARGING_MODE_DICT, 150 | api_param="3280_1", 151 | ), 152 | AlfenSelectDescription( 153 | key="lb_phase_connection", 154 | name="Load Balancing Phase Connection", 155 | icon=None, 156 | options=list(PHASE_ROTATION_DICT), 157 | options_dict=PHASE_ROTATION_DICT, 158 | api_param="2069_0", 159 | ), 160 | AlfenSelectDescription( 161 | key="auth_mode", 162 | name="Auth. Mode", 163 | icon="mdi:key", 164 | options=list(AUTH_MODE_DICT), 165 | options_dict=AUTH_MODE_DICT, 166 | api_param="2126_0", 167 | ), 168 | AlfenSelectDescription( 169 | key="load_balancing_protocol", 170 | name="Load Balancing Protocol", 171 | icon="mdi:scale-balance", 172 | options=list(LOAD_BALANCE_PROTOCOL_DICT), 173 | options_dict=LOAD_BALANCE_PROTOCOL_DICT, 174 | api_param="5217_0", 175 | ), 176 | AlfenSelectDescription( 177 | key="lb_active_balancing_received_measurements", 178 | name="Load Balancing Received Measurements", 179 | icon="mdi:scale-balance", 180 | options=list(LOAD_BALANCE_RECEIVED_MEASUREMENTS_DICT), 181 | options_dict=LOAD_BALANCE_RECEIVED_MEASUREMENTS_DICT, 182 | api_param="206F_0", 183 | ), 184 | AlfenSelectDescription( 185 | key="display_language", 186 | name="Display Language", 187 | icon="mdi:translate", 188 | options=list(DISPLAY_LANGUAGE_DICT), 189 | options_dict=DISPLAY_LANGUAGE_DICT, 190 | api_param="205D_0", 191 | ), 192 | AlfenSelectDescription( 193 | key="bo_network_1_connection_priority", 194 | name="Backoffice Network 1 Connection Priority (Ethernet)", 195 | icon="mdi:ethernet-cable", 196 | options=list(PRIORITIES_DICT), 197 | options_dict=PRIORITIES_DICT, 198 | api_param="20F0_E", 199 | ), 200 | AlfenSelectDescription( 201 | key="bo_network_2_connection_priority", 202 | name="Backoffice Network 2 Connection Priority (GPRS)", 203 | icon="mdi:antenna", 204 | options=list(PRIORITIES_DICT), 205 | options_dict=PRIORITIES_DICT, 206 | api_param="20F1_E", 207 | ), 208 | AlfenSelectDescription( 209 | key="socket_1_operation_mode", 210 | name="Socket 1 Operation Mode", 211 | icon="mdi:power-socket-eu", 212 | options=list(OPERATIVE_MODE_DICT), 213 | options_dict=OPERATIVE_MODE_DICT, 214 | api_param="205F_0", 215 | ), 216 | AlfenSelectDescription( 217 | key="gprs_network_mode", 218 | name="GPRS Network Mode", 219 | icon="mdi:antenna", 220 | options=list(GPRS_NETWORK_MODE_DICT), 221 | options_dict=GPRS_NETWORK_MODE_DICT, 222 | api_param="2113_0", 223 | ), 224 | AlfenSelectDescription( 225 | key="gprs_technology", 226 | name="GPRS Technology", 227 | icon="mdi:antenna", 228 | options=list(GPRS_TECHNOLOGY_DICT), 229 | options_dict=GPRS_TECHNOLOGY_DICT, 230 | api_param="2114_0", 231 | ), 232 | AlfenSelectDescription( 233 | key="lb_dsmr_smr_interface", 234 | name="Load Balancing DSMR/SMR Interface", 235 | icon="mdi:scale-balance", 236 | options=list(DSMR_SMR_INTERFACE_DICT), 237 | options_dict=DSMR_SMR_INTERFACE_DICT, 238 | api_param="2191_1", 239 | ), 240 | AlfenSelectDescription( 241 | key="lb_data_source", 242 | name="Load Balancing Data Source", 243 | icon="mdi:scale-balance", 244 | options=list(LOAD_BALANCE_DATA_SOURCE_DICT), 245 | options_dict=LOAD_BALANCE_DATA_SOURCE_DICT, 246 | api_param="2530_1", 247 | ), 248 | AlfenSelectDescription( 249 | key="ps_installation_max_allowed_phase", 250 | name="Installation Max. Allowed Phases", 251 | icon="mdi:scale-balance", 252 | options=list(ALLOWED_PHASE_DICT), 253 | options_dict=ALLOWED_PHASE_DICT, 254 | api_param="2189_0", 255 | ), 256 | AlfenSelectDescription( 257 | key="ps_installation_direct_external_suspend_signal", 258 | name="Installation Direct External Suspend Signal", 259 | icon="mdi:scale-balance", 260 | options=list(DIRECT_EXTERNAL_SUSPEND_SIGNAL), 261 | options_dict=DIRECT_EXTERNAL_SUSPEND_SIGNAL, 262 | api_param="216C_0", 263 | ), 264 | AlfenSelectDescription( 265 | key="ps_socket_type_socket_1", 266 | name="Socket Type Socket 1", 267 | icon="mdi:cable-data", 268 | options=list(SOCKET_TYPE_DICT), 269 | options_dict=SOCKET_TYPE_DICT, 270 | api_param="2125_0", 271 | ), 272 | AlfenSelectDescription( 273 | key="ev_disconnect_action", 274 | name="Car Disconnect Action", 275 | icon="mdi:cable-data", 276 | options=list(CAR_DISCONNECT_ACTION_DICT), 277 | options_dict=CAR_DISCONNECT_ACTION_DICT, 278 | api_param="2137_0", 279 | ), 280 | ) 281 | 282 | ALFEN_SELECT_DUAL_SOCKET_TYPES: Final[tuple[AlfenSelectDescription, ...]] = ( 283 | AlfenSelectDescription( 284 | key="ps_socket_type_socket_2", 285 | name="Socket Type Socket 2", 286 | icon="mdi:cable-data", 287 | options=list(SOCKET_TYPE_DICT), 288 | options_dict=SOCKET_TYPE_DICT, 289 | api_param="3125_0", 290 | ), 291 | ) 292 | 293 | 294 | async def async_setup_entry( 295 | hass: HomeAssistant, 296 | entry: AlfenConfigEntry, 297 | async_add_entities: AddEntitiesCallback, 298 | ) -> None: 299 | """Add Alfen Select from a config_entry.""" 300 | 301 | selects = [AlfenSelect(entry, description) for description in ALFEN_SELECT_TYPES] 302 | 303 | async_add_entities(selects) 304 | 305 | coordinator = entry.runtime_data 306 | if coordinator.device.get_number_of_sockets() == 2: 307 | numbers = [ 308 | AlfenSelect(coordinator.device, description) 309 | for description in ALFEN_SELECT_DUAL_SOCKET_TYPES 310 | ] 311 | async_add_entities(numbers) 312 | 313 | platform = entity_platform.current_platform.get() 314 | 315 | platform.async_register_entity_service( 316 | SERVICE_SET_CURRENT_PHASE, 317 | { 318 | vol.Required("phase"): str, 319 | }, 320 | "async_set_current_phase", 321 | ) 322 | 323 | platform.async_register_entity_service( 324 | SERVICE_ENABLE_RFID_AUTHORIZATION_MODE, 325 | {}, 326 | "async_enable_rfid_auth_mode", 327 | ) 328 | 329 | platform.async_register_entity_service( 330 | SERVICE_DISABLE_RFID_AUTHORIZATION_MODE, 331 | {}, 332 | "async_disable_rfid_auth_mode", 333 | ) 334 | 335 | 336 | class AlfenSelect(AlfenEntity, SelectEntity): 337 | """Define Alfen select.""" 338 | 339 | values_dict: dict[int, str] 340 | entity_description: AlfenSelectDescription 341 | 342 | def __init__( 343 | self, entry: AlfenConfigEntry, description: AlfenSelectDescription 344 | ) -> None: 345 | """Initialize.""" 346 | super().__init__(entry) 347 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 348 | 349 | self._attr_unique_id = f"{self.coordinator.device.id}_{description.key}" 350 | self._attr_options = description.options 351 | self.entity_description = description 352 | self.values_dict = {v: k for k, v in description.options_dict.items()} 353 | self._async_update_attrs() 354 | 355 | async def async_select_option(self, option: str) -> None: 356 | """Change the selected option.""" 357 | 358 | value = {v: k for k, v in self.values_dict.items()}[option] 359 | self.coordinator.device.set_value( 360 | self.entity_description.api_param, value 361 | ) 362 | self.async_write_ha_state() 363 | 364 | @property 365 | def current_option(self) -> str | None: 366 | """Return the current option.""" 367 | value = self._get_current_option() 368 | return self.values_dict.get(value) 369 | 370 | @property 371 | def extra_state_attributes(self): 372 | """Return the default attributes of the element.""" 373 | if self.entity_description.api_param in self.coordinator.device.properties: 374 | return { 375 | "category": self.coordinator.device.properties[ 376 | self.entity_description.api_param 377 | ][CAT] 378 | } 379 | return None 380 | 381 | def _get_current_option(self) -> str | None: 382 | """Return the current option.""" 383 | if self.entity_description.api_param in self.coordinator.device.properties: 384 | prop = self.coordinator.device.properties[self.entity_description.api_param] 385 | if self.entity_description.key == "ps_installation_max_allowed_phase": 386 | self.coordinator.device.max_allowed_phases = prop[VALUE] 387 | return prop[VALUE] 388 | return None 389 | 390 | async def async_update(self): 391 | """Update the entity.""" 392 | self._async_update_attrs() 393 | 394 | @callback 395 | def _async_update_attrs(self) -> None: 396 | """Update select attributes.""" 397 | self._attr_current_option = self._get_current_option() 398 | 399 | async def async_set_current_phase(self, phase): 400 | """Set the current phase.""" 401 | await self.coordinator.device.set_current_phase(phase) 402 | await self.async_select_option(phase) 403 | 404 | async def async_enable_rfid_auth_mode(self): 405 | """Enable RFID authorization mode.""" 406 | await self.coordinator.device.set_rfid_auth_mode(True) 407 | self.coordinator.device.set_value(self.entity_description.api_param, 2) 408 | self.async_write_ha_state() 409 | 410 | async def async_disable_rfid_auth_mode(self): 411 | """Disable RFID authorization mode.""" 412 | await self.coordinator.device.set_rfid_auth_mode(False) 413 | self.coordinator.device.set_value(self.entity_description.api_param, 0) 414 | self.async_write_ha_state() 415 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Alfen Eve Single Proline Wallbox.""" 2 | 3 | from dataclasses import dataclass 4 | import datetime 5 | from typing import Final 6 | 7 | from homeassistant.components.sensor import ( 8 | SensorDeviceClass, 9 | SensorEntity, 10 | SensorEntityDescription, 11 | SensorStateClass, 12 | ) 13 | from homeassistant.const import ( 14 | PERCENTAGE, 15 | SIGNAL_STRENGTH_DECIBELS, 16 | UnitOfElectricCurrent, 17 | UnitOfElectricPotential, 18 | UnitOfEnergy, 19 | UnitOfFrequency, 20 | UnitOfPower, 21 | UnitOfTemperature, 22 | UnitOfTime, 23 | ) 24 | from homeassistant.core import HomeAssistant, callback 25 | from homeassistant.helpers import entity_platform 26 | from homeassistant.helpers.entity import DeviceInfo 27 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 28 | from homeassistant.helpers.typing import StateType 29 | 30 | from .const import CAT, ID, SERVICE_REBOOT_WALLBOX, VALUE 31 | from .coordinator import AlfenConfigEntry 32 | from .entity import AlfenEntity 33 | 34 | 35 | @dataclass 36 | class AlfenSensorDescriptionMixin: 37 | """Define an entity description mixin for sensor entities.""" 38 | 39 | api_param: str 40 | unit: str 41 | round_digits: int | None 42 | 43 | 44 | @dataclass 45 | class AlfenSensorDescription(SensorEntityDescription, AlfenSensorDescriptionMixin): 46 | """Class to describe an Alfen sensor entity.""" 47 | 48 | 49 | STATUS_DICT: Final[dict[int, str]] = { 50 | 0: "Unknown", 51 | 1: "Off", 52 | 2: "Booting", 53 | 3: "Check Mains", 54 | 4: "Available", 55 | 5: "Authorizing", 56 | 6: "Authorized", 57 | 7: "Cable connected", 58 | 8: "EV Connected", 59 | 9: "Preparing Charging", 60 | 10: "Wait Vehicle Charging", 61 | 11: "Charging Normal", 62 | 12: "Charging Simplified", 63 | 13: "Suspended Over-Current", 64 | 14: "Suspended HF Switching", 65 | 15: "Suspended EV Disconnected", 66 | 16: "Finish Wait Vehicle", 67 | 17: "Finish Wait Disconnect", 68 | 18: "Error Protective Earth", 69 | 19: "Error Power Failure", 70 | 20: "Error Contactor Fault", 71 | 21: "Error Charging", 72 | 22: "Error Power Failure", 73 | 23: "Error Error Temperature", 74 | 24: "Error Illegal CP Value", 75 | 25: "Error Illegal PP Value", 76 | 26: "Error Too Many Restarts", 77 | 27: "Error", 78 | 28: "Error Message", 79 | 29: "Error Message Not Authorised", 80 | 30: "Error Message Cable Not Supported", 81 | 31: "Error Message S2 Not Opened", 82 | 32: "Error Message Time-Out", 83 | 33: "Reserved", 84 | 34: "In Operative", 85 | 35: "Load Balancing Limited", 86 | 36: "Load Balancing Forced Off", 87 | 38: "Not Charging", 88 | 39: "Solar Charging Wait", 89 | 40: "Charging Non Charging", 90 | 41: "Solar Charging", 91 | 42: "Charge Point Ready, Waiting For Power", 92 | 43: "Partial Solar Charging", 93 | } 94 | 95 | DISPLAY_ERROR_DICT: Final[dict[int, str]] = { 96 | 0: "No Error", 97 | 1: "Not able to charge. Please call for support.", 98 | 2: "Charging not started yet, to continue please reconnect cable", 99 | 3: "Too many retries. Please check your charging cable", 100 | 4: "One moment please... Your charging session will resume shortly.", 101 | 5: "One moment please... Your charging session will resume shortly.", 102 | 6: "One moment please... Your charging session will resume shortly.", 103 | 7: "S2 not open. Please reconnect cable.", 104 | 101: "Error in installation. Please check installation", 105 | 102: "Not able to charge. Please call for support.", 106 | 103: "Input voltage too low, not able to charge.", 107 | 104: "Not able to charge. Please call for support.", 108 | 105: "Not able to charge. Please call for support.", 109 | 106: "Not able to charge. Please call for support.", 110 | 108: "Not displayed", 111 | 109: "Not displayed", 112 | 201: "Error in installation. Please check installation or call for support.", 113 | 202: "Input voltage too low, not able to charge. Please call your installer.", 114 | 203: "Inside temperature high. Charging will resume shortly.", 115 | 204: "Temporary set to unavailable.", 116 | 206: "Temporary set to unavailable. Contact CPO or try again later.", 117 | 208: "Not displayed", 118 | 209: "Not displayed", 119 | 210: "Not displayed", 120 | 211: "Not able to lock cable. Please call for support.", 121 | 212: "Error in installation. Please check installation or call for support.", 122 | 213: "Not displayed", 123 | 301: "One moment please your charging session will resume shortly.", 124 | 302: "One moment please your charging session will resume shortly.", 125 | 303: "One moment please your charging session will resume shortly.", 126 | 304: "Charging not started yet to continue please reconnect cable.", 127 | 401: "Inside temperature high. Charging will resume shortly.", 128 | 402: "Inside temperature low. Charging will resume shortly.", 129 | 404: "Not able to lock cable. Please reconnect cable.", 130 | 405: "Cable not supported. Please try connecting your cable again.", 131 | 406: "No communication with vehicle. Please check your charging cable.", 132 | 407: "Not displayed", 133 | } 134 | 135 | MODE_3_STAT_DICT: Final[dict[int, str]] = { 136 | 160: "STATE_A", 137 | 161: "STATE_A1", 138 | 162: "STATE_A1", 139 | 177: "STATE_B1", 140 | 178: "STATE_B2", 141 | 193: "STATE_C1", 142 | 194: "STATE_C2", 143 | 209: "STATE_D1", 144 | 210: "STATE_D2", 145 | 224: "STATE_E", 146 | 240: "STATE_F", 147 | } 148 | 149 | ALLOWED_PHASE_DICT: Final[dict[int, str]] = {1: "1 Phase", 3: "3 Phases"} 150 | 151 | POWER_STATES_DICT: Final[dict[int, str]] = { 152 | 0: "Normal Operation", 153 | 1: "Inactive", 154 | 2: "Connected ISO15118", 155 | 3: "Wait for EV Connect", 156 | 4: "EV Connected", 157 | 5: "Active", 158 | 6: "Wait for S2 Close", 159 | 7: "Wait for S2 Open", 160 | 8: "Suspended", 161 | 9: "Ventilating", 162 | 10: "Wakeup State E", 163 | 11: "Wakeup State B1", 164 | 12: "Error", 165 | 13: "Error EV Detect", 166 | 14: "Wait for EV Disconnect", 167 | 15: "Prepared", 168 | 16: "Connected ISO15118 Error", 169 | 17: "Count", 170 | } 171 | 172 | MAIN_STATE_DICT: Final[dict[int, str]] = { 173 | -1: "Illegal", 174 | 0: "Unknown", 175 | 1: "Booting", 176 | 2: "Available", 177 | 3: "Cable Connected", 178 | 4: "Cable Connected Timeout", 179 | 5: "EV Connected", 180 | 6: "Button Activated", 181 | 7: "NFC Available", 182 | 8: "NFC Authorised", 183 | 9: "Wait for EV Connect", 184 | 10: "Charging Test Relays", 185 | 11: "Charging Power Off", 186 | 12: "Charging Power Off Low Max Current", 187 | 13: "Charging Power Starting", 188 | 14: "Charging Power On", 189 | 15: "Charging Power On Simplified", 190 | 16: "Charging Wait for EV Reconnect", 191 | 17: "Charging Terminating", 192 | 18: "Charging Wakeup", 193 | 19: "Wait for Disconnect", 194 | 20: "Wait for Release Authorisation", 195 | 21: "Charging Recover from Outage", 196 | 22: "Error", 197 | 23: "Error Message", 198 | 24: "Error Message Cable not Supported", 199 | 25: "Error Illegal Mode 3", 200 | 26: "Error Too Many Restarts", 201 | 27: "Error Charging", 202 | 28: "Error Charging Overcurrent", 203 | 29: "Error Charging HF Contactor Switching", 204 | 30: "Error S2 Not Opened", 205 | 31: "Error Protective Earth", 206 | 32: "Error Relays", 207 | 33: "Error Low Supply Voltage", 208 | 34: "Error Internal Voltage", 209 | 35: "Error Powermeter", 210 | 36: "Error Temperature", 211 | 37: "Suspended", 212 | 38: "Inoperative", 213 | 39: "Reserved", 214 | 40: "Error Charging RCD Signaled", 215 | 41: "Charging Power Off Ventilating", 216 | 42: "Charging Power Off Suspended", 217 | 43: "Charging Pwoer OFf Phase Change", 218 | 44: "Wait for Start Metervalue", 219 | 45: "Wait for Stop Metervalue", 220 | 46: "Error Socket Motor", 221 | 47: "Cable Conencted Type E", 222 | 48: "Cable Connected Time out Type E", 223 | 49: "Charging Type E", 224 | 50: "Wait for Disconnect Type E", 225 | 51: "Charging Suspended Type E", 226 | 52: "Charging Low Max Current Type E", 227 | 53: "Invalid Card", 228 | 54: "EV Connected Unauthorized", 229 | 55: "Wait for Disconnect PP", 230 | } 231 | 232 | MAIN_STATE__TMP_DICT: Final[dict[int, str]] = { 233 | 0: "Unknown", 234 | 1: "Available", 235 | 2: "Authorising", 236 | 4: "EV Connected", 237 | 5: "Active", 238 | 8: "Rejected", 239 | 15: "Booting", 240 | 16: "Cable Connected", 241 | 17: "Count", 242 | 19: "Cable Connected Authorising", 243 | 20: "Cable Connected Authorised", 244 | 24: "Cable Connected Rejected", 245 | 48: "EV Connected", 246 | 50: "EV Connected Authorising", 247 | 52: "EV Connected Authorised", 248 | 56: "EV Connected Rejected", 249 | 65: "Cable Locked", 250 | 66: "Cable Started", 251 | 67: "Charging", 252 | 68: "Charging Finishing", 253 | 69: "Charging Finished", 254 | 70: "Cable Unlock", 255 | 71: "Suspended EV", 256 | 72: "Suspended EVSE", 257 | 79: "Wait for Cable Disconnect", 258 | 128: "Timeout Waiting for Cable", 259 | 129: "Timeout Waiting for EV Connect", 260 | 130: "Timeout Waiting for Authorisation", 261 | 131: "Timeout Waiting for S2", 262 | 132: "Timeout Waiting for Cable Removal", 263 | 159: "Offline", 264 | 160: "Inoperative", 265 | 161: "Reserved", 266 | 192: "Error Mask", 267 | 193: "Error Relay", 268 | 194: "Error Temperature", 269 | 195: "Error Overcurrent", 270 | 196: "Error Socket Motor", 271 | 197: "Error Illegal Mode 3", 272 | 198: "Error Energy Meter", 273 | 199: "Error Phase", 274 | 200: "Error Internal RCD", 275 | 201: "Error HF Switching", 276 | 202: "Error Low Supply Voltage", 277 | } 278 | 279 | OCPP_BOOT_NOTIFICATION_STATUS_DICT: Final[dict[int, str]] = { 280 | 0: "Not Sent", 281 | 1: "Awaiting Reply", 282 | 2: "Rejected", 283 | 3: "Accepted", 284 | 4: "Pending", 285 | } 286 | 287 | MODBUS_CONNECTION_STATES_DICT: Final[dict[int, str]] = { 288 | 0: "Idle", 289 | 1: "Initializing", 290 | 2: "Normal", 291 | 3: "Warning", 292 | 4: "Error", 293 | } 294 | 295 | ALFEN_SENSOR_TYPES: Final[tuple[AlfenSensorDescription, ...]] = ( 296 | AlfenSensorDescription( 297 | key="status_socket_1", 298 | name="Status Code Socket 1", 299 | icon="mdi:ev-station", 300 | api_param="2501_2", 301 | unit=None, 302 | round_digits=None, 303 | ), 304 | AlfenSensorDescription( 305 | key="uptime", 306 | name="Uptime", 307 | icon="mdi:timer-outline", 308 | api_param="2060_0", 309 | unit=UnitOfTime.DAYS, 310 | round_digits=None, 311 | state_class=SensorStateClass.TOTAL_INCREASING, 312 | ), 313 | AlfenSensorDescription( 314 | key="uptime_hours", 315 | name="Uptime Hours", 316 | icon="mdi:timer-outline", 317 | api_param="2060_0", 318 | unit=UnitOfTime.HOURS, 319 | round_digits=None, 320 | state_class=SensorStateClass.TOTAL_INCREASING, 321 | ), 322 | AlfenSensorDescription( 323 | key="last_modify_datetime", 324 | name="Last Modify Config datetime", 325 | icon="mdi:timer-outline", 326 | api_param="2187_0", 327 | unit=None, 328 | state_class=SensorDeviceClass.DATE, 329 | round_digits=None, 330 | ), 331 | AlfenSensorDescription( 332 | key="system_date_time", 333 | name="System Datetime", 334 | icon="mdi:timer-outline", 335 | api_param="2059_0", 336 | unit=None, 337 | round_digits=None, 338 | state_class=SensorStateClass.TOTAL_INCREASING, 339 | ), 340 | AlfenSensorDescription( 341 | key="bootups", 342 | name="Bootups", 343 | icon="mdi:reload", 344 | api_param="2056_0", 345 | unit=None, 346 | round_digits=None, 347 | state_class=SensorStateClass.TOTAL_INCREASING, 348 | ), 349 | AlfenSensorDescription( 350 | key="frequency_socket_1", 351 | name="Frequency Socket 1", 352 | icon="mdi:information-outline", 353 | api_param="2221_12", 354 | unit=UnitOfFrequency.HERTZ, 355 | round_digits=0, 356 | ), 357 | AlfenSensorDescription( 358 | key="voltage_l1_socket_1", 359 | name="Voltage L1N Socket 1", 360 | icon="mdi:flash", 361 | api_param="2221_3", 362 | unit=UnitOfElectricPotential.VOLT, 363 | round_digits=1, 364 | state_class=SensorStateClass.MEASUREMENT, 365 | device_class=SensorDeviceClass.VOLTAGE, 366 | ), 367 | AlfenSensorDescription( 368 | key="voltage_l2_socket_1", 369 | name="Voltage L2N Socket 1", 370 | icon="mdi:flash", 371 | api_param="2221_4", 372 | unit=UnitOfElectricPotential.VOLT, 373 | round_digits=1, 374 | state_class=SensorStateClass.MEASUREMENT, 375 | device_class=SensorDeviceClass.VOLTAGE, 376 | ), 377 | AlfenSensorDescription( 378 | key="voltage_l3_socket_1", 379 | name="Voltage L3N Socket 1", 380 | icon="mdi:flash", 381 | api_param="2221_5", 382 | unit=UnitOfElectricPotential.VOLT, 383 | round_digits=1, 384 | state_class=SensorStateClass.MEASUREMENT, 385 | device_class=SensorDeviceClass.VOLTAGE, 386 | ), 387 | # AlfenSensorDescription( 388 | # key="voltage_l1l2_socket_1", 389 | # name="Voltage L1L2 Socket 1", 390 | # icon="mdi:flash", 391 | # api_param="2221_6", 392 | # unit=UnitOfElectricPotential.VOLT, 393 | # round_digits=1, 394 | # state_class=SensorStateClass.MEASUREMENT, 395 | # device_class=SensorDeviceClass.VOLTAGE, 396 | # ), 397 | # AlfenSensorDescription( 398 | # key="voltage_l2l3_socket_1", 399 | # name="Voltage L2L3 Socket 1", 400 | # icon="mdi:flash", 401 | # api_param="2221_7", 402 | # unit=UnitOfElectricPotential.VOLT, 403 | # round_digits=1, 404 | # state_class=SensorStateClass.MEASUREMENT, 405 | # device_class=SensorDeviceClass.VOLTAGE, 406 | # ), 407 | # AlfenSensorDescription( 408 | # key="voltage_l3l1_socket_1", 409 | # name="Voltage L3L1 Socket 1", 410 | # icon="mdi:flash", 411 | # api_param="2221_8", 412 | # unit=UnitOfElectricPotential.VOLT, 413 | # round_digits=1, 414 | # state_class=SensorStateClass.MEASUREMENT, 415 | # device_class=SensorDeviceClass.VOLTAGE, 416 | # ), 417 | AlfenSensorDescription( 418 | key="current_n_socket_1", 419 | name="Current N Socket 1", 420 | icon="mdi:current-ac", 421 | api_param="2221_9", 422 | unit=UnitOfElectricCurrent.AMPERE, 423 | round_digits=2, 424 | state_class=SensorStateClass.MEASUREMENT, 425 | device_class=SensorDeviceClass.CURRENT, 426 | ), 427 | AlfenSensorDescription( 428 | key="current_l1_socket_1", 429 | name="Current L1 Socket 1", 430 | icon="mdi:current-ac", 431 | api_param="2221_A", 432 | unit=UnitOfElectricCurrent.AMPERE, 433 | round_digits=2, 434 | state_class=SensorStateClass.MEASUREMENT, 435 | device_class=SensorDeviceClass.CURRENT, 436 | ), 437 | AlfenSensorDescription( 438 | key="current_l2_socket_1", 439 | name="Current L2 Socket 1", 440 | icon="mdi:current-ac", 441 | api_param="2221_B", 442 | unit=UnitOfElectricCurrent.AMPERE, 443 | round_digits=2, 444 | state_class=SensorStateClass.MEASUREMENT, 445 | device_class=SensorDeviceClass.CURRENT, 446 | ), 447 | AlfenSensorDescription( 448 | key="current_l3_socket_1", 449 | name="Current L3 Socket 1", 450 | icon="mdi:current-ac", 451 | api_param="2221_C", 452 | unit=UnitOfElectricCurrent.AMPERE, 453 | round_digits=2, 454 | state_class=SensorStateClass.MEASUREMENT, 455 | device_class=SensorDeviceClass.CURRENT, 456 | ), 457 | AlfenSensorDescription( 458 | key="current_total_socket_1", 459 | name="Current total Socket 1", 460 | icon="mdi:current-ac", 461 | api_param="2221_D", 462 | unit=UnitOfElectricCurrent.AMPERE, 463 | round_digits=2, 464 | state_class=SensorStateClass.MEASUREMENT, 465 | device_class=SensorDeviceClass.CURRENT, 466 | ), 467 | AlfenSensorDescription( 468 | key="active_power_total_socket_1", 469 | name="Active Power Total Socket 1", 470 | icon="mdi:circle-slice-3", 471 | api_param="2221_16", 472 | unit=UnitOfPower.WATT, 473 | round_digits=2, 474 | state_class=SensorStateClass.MEASUREMENT, 475 | device_class=SensorDeviceClass.POWER, 476 | ), 477 | AlfenSensorDescription( 478 | key="meter_reading_socket_1", 479 | name="Meter Reading Socket 1", 480 | icon="mdi:counter", 481 | api_param="2221_22", 482 | unit=UnitOfEnergy.KILO_WATT_HOUR, 483 | round_digits=None, 484 | state_class=SensorStateClass.TOTAL_INCREASING, 485 | device_class=SensorDeviceClass.ENERGY, 486 | ), 487 | AlfenSensorDescription( 488 | key="temperature", 489 | name="Temperature", 490 | icon="mdi:thermometer", 491 | api_param="2201_0", 492 | unit=UnitOfTemperature.CELSIUS, 493 | round_digits=1, 494 | state_class=SensorStateClass.MEASUREMENT, 495 | device_class=SensorDeviceClass.TEMPERATURE, 496 | ), 497 | AlfenSensorDescription( 498 | key="max_temperature", 499 | name="Max Temperature", 500 | icon="mdi:thermometer", 501 | api_param="2249_0", 502 | unit=UnitOfTemperature.CELSIUS, 503 | round_digits=1, 504 | state_class=SensorStateClass.MEASUREMENT, 505 | device_class=SensorDeviceClass.TEMPERATURE, 506 | ), 507 | AlfenSensorDescription( 508 | key="min_temperature", 509 | name="Minimum Temperature", 510 | icon="mdi:thermometer", 511 | api_param="2249_1", 512 | unit=UnitOfTemperature.CELSIUS, 513 | round_digits=1, 514 | state_class=SensorStateClass.MEASUREMENT, 515 | device_class=SensorDeviceClass.TEMPERATURE, 516 | ), 517 | AlfenSensorDescription( 518 | key="main_static_lb_max_current_socket_1", 519 | name="Main Static LB Max Current Socket 1", 520 | icon="mdi:current-ac", 521 | api_param="212B_0", 522 | unit=UnitOfElectricCurrent.AMPERE, 523 | round_digits=1, 524 | state_class=SensorStateClass.MEASUREMENT, 525 | device_class=SensorDeviceClass.CURRENT, 526 | ), 527 | AlfenSensorDescription( 528 | key="main_active_lb_max_current_socket_1", 529 | name="Main Active LB Max Current Socket 1", 530 | icon="mdi:current-ac", 531 | api_param="212D_0", 532 | unit=UnitOfElectricCurrent.AMPERE, 533 | round_digits=1, 534 | state_class=SensorStateClass.MEASUREMENT, 535 | device_class=SensorDeviceClass.CURRENT, 536 | ), 537 | AlfenSensorDescription( 538 | key="charging_box_identifier", 539 | name="Charging Box Identifier", 540 | icon="mdi:ev-station", 541 | api_param="2053_0", 542 | unit=None, 543 | round_digits=None, 544 | ), 545 | AlfenSensorDescription( 546 | key="boot_reason", 547 | name="System Boot Reason", 548 | icon="mdi:reload", 549 | api_param="2057_0", 550 | unit=None, 551 | round_digits=None, 552 | ), 553 | AlfenSensorDescription( 554 | key="p1_measurements_1", 555 | name="P1 Meter Phase 1 Current", 556 | icon="mdi:current-ac", 557 | api_param="212F_1", 558 | unit=UnitOfElectricCurrent.AMPERE, 559 | round_digits=2, 560 | state_class=SensorStateClass.MEASUREMENT, 561 | device_class=SensorDeviceClass.CURRENT, 562 | ), 563 | AlfenSensorDescription( 564 | key="p1_measurements_2", 565 | name="P1 Meter Phase 2 Current", 566 | icon="mdi:current-ac", 567 | api_param="212F_2", 568 | unit=UnitOfElectricCurrent.AMPERE, 569 | round_digits=2, 570 | state_class=SensorStateClass.MEASUREMENT, 571 | device_class=SensorDeviceClass.CURRENT, 572 | ), 573 | AlfenSensorDescription( 574 | key="p1_measurements_3", 575 | name="P1 Meter Phase 3 Current", 576 | icon="mdi:current-ac", 577 | api_param="212F_3", 578 | unit=UnitOfElectricCurrent.AMPERE, 579 | round_digits=2, 580 | state_class=SensorStateClass.MEASUREMENT, 581 | device_class=SensorDeviceClass.CURRENT, 582 | ), 583 | AlfenSensorDescription( 584 | key="gprs_apn_name", 585 | name="GPRS APN Name", 586 | icon="mdi:antenna", 587 | api_param="2100_0", 588 | unit=None, 589 | round_digits=None, 590 | ), 591 | AlfenSensorDescription( 592 | key="gprs_apn_user", 593 | name="GPRS APN User", 594 | icon="mdi:antenna", 595 | api_param="2101_0", 596 | unit=None, 597 | round_digits=None, 598 | ), 599 | AlfenSensorDescription( 600 | key="gprs_apn_password", 601 | name="GPRS APN Password", 602 | icon="mdi:antenna", 603 | api_param="2102_0", 604 | unit=None, 605 | round_digits=None, 606 | ), 607 | AlfenSensorDescription( 608 | key="gprs_sim_pin", 609 | name="GPRS SIM Pin", 610 | icon="mdi:antenna", 611 | api_param="2103_0", 612 | unit=None, 613 | round_digits=None, 614 | ), 615 | AlfenSensorDescription( 616 | key="gprs_sim_imsi", 617 | name="GPRS SIM IMSI", 618 | icon="mdi:antenna", 619 | api_param="2104_0", 620 | unit=None, 621 | round_digits=None, 622 | ), 623 | AlfenSensorDescription( 624 | key="gprs_sim_iccid", 625 | name="GPRS SIM Serial", 626 | icon="mdi:antenna", 627 | api_param="2105_0", 628 | unit=None, 629 | round_digits=None, 630 | ), 631 | AlfenSensorDescription( 632 | key="gprs_provider", 633 | name="GPRS Provider", 634 | icon="mdi:antenna", 635 | api_param="2112_0", 636 | unit=None, 637 | round_digits=None, 638 | ), 639 | AlfenSensorDescription( 640 | key="comm_bo_url_wired_server_domain_and_port", 641 | name="Wired Url Server Domain And Port", 642 | icon="mdi:cable-data", 643 | api_param="2071_1", 644 | unit=None, 645 | round_digits=None, 646 | ), 647 | AlfenSensorDescription( 648 | key="comm_bo_url_wired_server_path", 649 | name="Wired Url Wired Server Path", 650 | icon="mdi:cable-data", 651 | api_param="2071_2", 652 | unit=None, 653 | round_digits=None, 654 | ), 655 | # AlfenSensorDescription( 656 | # key="comm_dhcp_address_1", 657 | # name="GPRS DHCP Address", 658 | # icon="mdi:antenna", 659 | # api_param="2072_1", 660 | # unit=None, 661 | # round_digits=None, 662 | # ), 663 | AlfenSensorDescription( 664 | key="comm_netmask_address_1", 665 | name="GPRS Netmask", 666 | icon="mdi:antenna", 667 | api_param="2073_1", 668 | unit=None, 669 | round_digits=None, 670 | ), 671 | AlfenSensorDescription( 672 | key="comm_gateway_address_1", 673 | name="GPRS Gateway Address", 674 | icon="mdi:antenna", 675 | api_param="2074_1", 676 | unit=None, 677 | round_digits=None, 678 | ), 679 | AlfenSensorDescription( 680 | key="comm_ip_address_1", 681 | name="GPRS IP Address", 682 | icon="mdi:antenna", 683 | api_param="2075_1", 684 | unit=None, 685 | round_digits=None, 686 | ), 687 | AlfenSensorDescription( 688 | key="comm_bo_short_name", 689 | name="Backoffice Short Name", 690 | icon="mdi:antenna", 691 | api_param="2076_0", 692 | unit=None, 693 | round_digits=None, 694 | ), 695 | AlfenSensorDescription( 696 | key="comm_bo_url_gprs_server_domain_and_port", 697 | name="GPRS Url Server Domain And Port", 698 | icon="mdi:antenna", 699 | api_param="2078_1", 700 | unit=None, 701 | round_digits=None, 702 | ), 703 | AlfenSensorDescription( 704 | key="comm_bo_url_gprs_server_path", 705 | name="GPRS Url Server Path", 706 | icon="mdi:antenna", 707 | api_param="2078_2", 708 | unit=None, 709 | round_digits=None, 710 | ), 711 | AlfenSensorDescription( 712 | key="comm_gprs_dns_1", 713 | name="GPRS DNS 1", 714 | icon="mdi:antenna", 715 | api_param="2079_1", 716 | unit=None, 717 | round_digits=None, 718 | ), 719 | AlfenSensorDescription( 720 | key="comm_gprs_dns_2", 721 | name="GPRS DNS 2", 722 | icon="mdi:antenna", 723 | api_param="2080_1", 724 | unit=None, 725 | round_digits=None, 726 | ), 727 | AlfenSensorDescription( 728 | key="gprs_signal_strength", 729 | name="GPRS Signal", 730 | icon="mdi:antenna", 731 | api_param="2110_0", 732 | unit=SIGNAL_STRENGTH_DECIBELS, 733 | round_digits=None, 734 | state_class=SensorStateClass.MEASUREMENT, 735 | device_class=SensorDeviceClass.SIGNAL_STRENGTH, 736 | ), 737 | AlfenSensorDescription( 738 | key="mobile_weak_signal_threshold", 739 | name="Mobile Weak Signal Threshold", 740 | icon="mdi:information-outline", 741 | api_param="2111_0", 742 | unit=None, 743 | round_digits=0, 744 | ), 745 | # AlfenSensorDescription( 746 | # key="comm_dhcp_address_2", 747 | # name="Wired DHCP", 748 | # icon="mdi:cable-data", 749 | # api_param="207A_1", 750 | # unit=None, 751 | # round_digits=None, 752 | # ), 753 | AlfenSensorDescription( 754 | key="comm_netmask_address_2", 755 | name="Wired Netmask", 756 | icon="mdi:cable-data", 757 | api_param="207B_1", 758 | unit=None, 759 | round_digits=None, 760 | ), 761 | AlfenSensorDescription( 762 | key="comm_gateway_address_2", 763 | name="Wired Gateway Address", 764 | icon="mdi:cable-data", 765 | api_param="207C_1", 766 | unit=None, 767 | round_digits=None, 768 | ), 769 | AlfenSensorDescription( 770 | key="comm_ip_address_2", 771 | name="Wired IP Address", 772 | icon="mdi:cable-data", 773 | api_param="207D_1", 774 | unit=None, 775 | round_digits=None, 776 | ), 777 | AlfenSensorDescription( 778 | key="comm_wired_dns_1", 779 | name="Wired DNS 1", 780 | icon="mdi:cable-data", 781 | api_param="207E_1", 782 | unit=None, 783 | round_digits=None, 784 | ), 785 | AlfenSensorDescription( 786 | key="comm_wired_dns_2", 787 | name="Wired DNS 2", 788 | icon="mdi:cable-data", 789 | api_param="207F_1", 790 | unit=None, 791 | round_digits=None, 792 | ), 793 | AlfenSensorDescription( 794 | key="comm_wired_mac", 795 | name="Wired MAC address", 796 | icon="mdi:cable-data", 797 | api_param="2052_1", 798 | unit=None, 799 | round_digits=None, 800 | ), 801 | AlfenSensorDescription( 802 | key="comm_protocol_name", 803 | name="Protocol Name", 804 | icon="mdi:information-outline", 805 | api_param="2081_0", 806 | unit=None, 807 | round_digits=None, 808 | ), 809 | AlfenSensorDescription( 810 | key="comm_protocol_version", 811 | name="Protocol Version", 812 | icon="mdi:information-outline", 813 | api_param="2082_0", 814 | unit=None, 815 | round_digits=None, 816 | ), 817 | AlfenSensorDescription( 818 | key="object_id", 819 | name="Charger Number", 820 | icon="mdi:information-outline", 821 | api_param="2051_0", 822 | unit=None, 823 | round_digits=None, 824 | ), 825 | AlfenSensorDescription( 826 | key="comm_car_cp_voltage_high_socket_1", 827 | name="Car CP Voltage High Socket 1", 828 | icon="mdi:lightning-bolt", 829 | api_param="2511_0", 830 | unit=UnitOfElectricPotential.VOLT, 831 | round_digits=2, 832 | state_class=SensorStateClass.MEASUREMENT, 833 | device_class=SensorDeviceClass.VOLTAGE, 834 | ), 835 | AlfenSensorDescription( 836 | key="comm_car_cp_voltage_low_socket_1", 837 | name="Car CP Voltage Low Socket 1", 838 | icon="mdi:lightning-bolt", 839 | api_param="2511_1", 840 | unit=UnitOfElectricPotential.VOLT, 841 | round_digits=2, 842 | state_class=SensorStateClass.MEASUREMENT, 843 | device_class=SensorDeviceClass.VOLTAGE, 844 | ), 845 | AlfenSensorDescription( 846 | key="comm_car_pp_resistance_socket_1", 847 | name="Car PP resistance Socket 1", 848 | icon="mdi:resistor", 849 | api_param="2511_2", 850 | unit="Ω", 851 | round_digits=1, 852 | state_class=SensorStateClass.MEASUREMENT, 853 | ), 854 | AlfenSensorDescription( 855 | key="comm_car_pwm_duty_cycle_socket_1", 856 | name="Car PWM Duty Cycle Socket 1", 857 | icon="mdi:percent", 858 | api_param="2511_3", 859 | unit=PERCENTAGE, 860 | round_digits=1, 861 | state_class=SensorStateClass.MEASUREMENT, 862 | device_class=SensorDeviceClass.POWER_FACTOR, 863 | ), 864 | AlfenSensorDescription( 865 | key="ps_connector_1_max_allowed_phase", 866 | name="Connector 1 Max Allowed of Phases", 867 | icon="mdi:scale-balance", 868 | unit=None, 869 | api_param="312E_0", 870 | round_digits=None, 871 | ), 872 | AlfenSensorDescription( 873 | key="ui_state_1", 874 | name="Display State Socket 1", 875 | icon="mdi:information-outline", 876 | unit=None, 877 | api_param="3190_1", 878 | round_digits=None, 879 | ), 880 | AlfenSensorDescription( 881 | key="ui_error_number_1", 882 | name="Display Error Number Socket 1", 883 | icon="mdi:information-outline", 884 | unit=None, 885 | api_param="3190_2", 886 | round_digits=None, 887 | ), 888 | AlfenSensorDescription( 889 | key="mode_3_state_socket_1", 890 | name="Mode3 State Socket 1", 891 | icon="mdi:information-outline", 892 | unit=None, 893 | api_param="2501_4", 894 | round_digits=None, 895 | ), 896 | AlfenSensorDescription( 897 | key="cpo_name", 898 | name="CPO Name", 899 | icon="mdi:information-outline", 900 | unit=None, 901 | api_param="2722_0", 902 | round_digits=None, 903 | ), 904 | AlfenSensorDescription( 905 | key="power_state_socket_1", 906 | name="Power State Socket 1", 907 | icon="mdi:information-outline", 908 | unit=None, 909 | api_param="2501_3", 910 | round_digits=None, 911 | ), 912 | AlfenSensorDescription( 913 | key="main_state_socket_1", 914 | name="Main State Socket 1", 915 | icon="mdi:information-outline", 916 | unit=None, 917 | api_param="2501_1", 918 | round_digits=None, 919 | ), 920 | AlfenSensorDescription( 921 | key="ocpp_boot_notification_state", 922 | name="OCPP Boot notification State", 923 | icon="mdi:information-outline", 924 | unit=None, 925 | api_param="3600_1", 926 | round_digits=None, 927 | ), 928 | AlfenSensorDescription( 929 | key="modbus_tcp_ip_connection_state", 930 | name="Modbus TCP/IP Connection State", 931 | icon="mdi:information-outline", 932 | unit=None, 933 | api_param="2540_0", 934 | round_digits=None, 935 | ), 936 | AlfenSensorDescription( 937 | key="main_active_max_current_socket_1", 938 | name="Main Active Max Current Socket 1", 939 | icon="mdi:current-ac", 940 | api_param="212C_0", 941 | unit=UnitOfElectricCurrent.AMPERE, 942 | round_digits=2, 943 | state_class=SensorStateClass.MEASUREMENT, 944 | device_class=SensorDeviceClass.CURRENT, 945 | ), 946 | AlfenSensorDescription( 947 | key="main_start_max_current", 948 | name="Main Start Max Current", 949 | icon="mdi:current-ac", 950 | api_param="2128_0", 951 | unit=UnitOfElectricCurrent.AMPERE, 952 | round_digits=2, 953 | ), 954 | AlfenSensorDescription( 955 | key="main_external_max_current_socket_1", 956 | name="Main External Max Current Socket 1", 957 | icon="mdi:current-ac", 958 | api_param="212A_0", 959 | unit=UnitOfElectricCurrent.AMPERE, 960 | state_class=SensorStateClass.MEASUREMENT, 961 | device_class=SensorDeviceClass.CURRENT, 962 | round_digits=2, 963 | ), 964 | AlfenSensorDescription( 965 | key="smart_meter_l1", 966 | name="Smart Meter Power L1", 967 | icon="mdi:transmission-tower", 968 | api_param=None, 969 | unit=UnitOfPower.WATT, 970 | round_digits=2, 971 | state_class=SensorStateClass.MEASUREMENT, 972 | device_class=SensorDeviceClass.POWER, 973 | ), 974 | AlfenSensorDescription( 975 | key="smart_meter_l2", 976 | name="Smart Meter Power L2", 977 | icon="mdi:transmission-tower", 978 | api_param=None, 979 | unit=UnitOfPower.WATT, 980 | round_digits=2, 981 | state_class=SensorStateClass.MEASUREMENT, 982 | device_class=SensorDeviceClass.POWER, 983 | ), 984 | AlfenSensorDescription( 985 | key="smart_meter_l3", 986 | name="Smart Meter Power L3", 987 | icon="mdi:transmission-tower", 988 | api_param=None, 989 | unit=UnitOfPower.WATT, 990 | round_digits=2, 991 | state_class=SensorStateClass.MEASUREMENT, 992 | device_class=SensorDeviceClass.POWER, 993 | ), 994 | AlfenSensorDescription( 995 | key="smart_meter_total", 996 | name="Smart Meter Power Total", 997 | icon="mdi:transmission-tower", 998 | api_param=None, 999 | unit=UnitOfPower.WATT, 1000 | round_digits=2, 1001 | state_class=SensorStateClass.MEASUREMENT, 1002 | device_class=SensorDeviceClass.POWER, 1003 | ), 1004 | AlfenSensorDescription( 1005 | key="smart_meter_voltage_l1", 1006 | name="Smart Meter Voltage L1N", 1007 | icon="mdi:flash", 1008 | api_param="5221_3", 1009 | unit=UnitOfElectricPotential.VOLT, 1010 | round_digits=1, 1011 | state_class=SensorStateClass.MEASUREMENT, 1012 | device_class=SensorDeviceClass.VOLTAGE, 1013 | ), 1014 | AlfenSensorDescription( 1015 | key="smart_meter_voltage_l2", 1016 | name="Smart Meter Voltage L2N", 1017 | icon="mdi:flash", 1018 | api_param="5221_4", 1019 | unit=UnitOfElectricPotential.VOLT, 1020 | round_digits=1, 1021 | state_class=SensorStateClass.MEASUREMENT, 1022 | device_class=SensorDeviceClass.VOLTAGE, 1023 | ), 1024 | AlfenSensorDescription( 1025 | key="smart_meter_voltage_l3", 1026 | name="Smart Meter Voltage L3N", 1027 | icon="mdi:flash", 1028 | api_param="5221_5", 1029 | unit=UnitOfElectricPotential.VOLT, 1030 | round_digits=1, 1031 | state_class=SensorStateClass.MEASUREMENT, 1032 | device_class=SensorDeviceClass.VOLTAGE, 1033 | ), 1034 | # AlfenSensorDescription( 1035 | # key="smart_meter_voltage_l1L2", 1036 | # name="Smart Meter Voltage L1L2", 1037 | # icon="mdi:flash", 1038 | # api_param="5221_6", 1039 | # unit=UnitOfElectricPotential.VOLT, 1040 | # round_digits=1, 1041 | # state_class=SensorStateClass.MEASUREMENT, 1042 | # device_class=SensorDeviceClass.VOLTAGE, 1043 | # ), 1044 | # AlfenSensorDescription( 1045 | # key="smart_meter_voltage_l2l3", 1046 | # name="Smart Meter Voltage L2L3 Socket 2", 1047 | # icon="mdi:flash", 1048 | # api_param="5221_7", 1049 | # unit=UnitOfElectricPotential.VOLT, 1050 | # round_digits=1, 1051 | # state_class=SensorStateClass.MEASUREMENT, 1052 | # device_class=SensorDeviceClass.VOLTAGE, 1053 | # ), 1054 | # AlfenSensorDescription( 1055 | # key="smart_meter_voltage_l3l1", 1056 | # name="Smart Meter Voltage L3L1 Socket 2", 1057 | # icon="mdi:flash", 1058 | # api_param="5221_8", 1059 | # unit=UnitOfElectricPotential.VOLT, 1060 | # round_digits=1, 1061 | # state_class=SensorStateClass.MEASUREMENT, 1062 | # device_class=SensorDeviceClass.VOLTAGE, 1063 | # ), 1064 | AlfenSensorDescription( 1065 | key="smart_meter_active_power_total", 1066 | name="Smart Meter Active Power Total", 1067 | icon="mdi:flash", 1068 | api_param="5221_16", 1069 | unit=UnitOfPower.WATT, 1070 | round_digits=2, 1071 | state_class=SensorStateClass.MEASUREMENT, 1072 | device_class=SensorDeviceClass.POWER, 1073 | ), 1074 | AlfenSensorDescription( 1075 | key="smart_meter_current_l1", 1076 | name="Smart Meter Current L1", 1077 | icon="mdi:current-ac", 1078 | api_param="5221_A", 1079 | unit=UnitOfElectricCurrent.AMPERE, 1080 | round_digits=2, 1081 | state_class=SensorStateClass.MEASUREMENT, 1082 | device_class=SensorDeviceClass.CURRENT, 1083 | ), 1084 | AlfenSensorDescription( 1085 | key="smart_meter_current_l2", 1086 | name="Smart Meter Current L2", 1087 | icon="mdi:current-ac", 1088 | api_param="5221_B", 1089 | unit=UnitOfElectricCurrent.AMPERE, 1090 | round_digits=2, 1091 | state_class=SensorStateClass.MEASUREMENT, 1092 | device_class=SensorDeviceClass.CURRENT, 1093 | ), 1094 | AlfenSensorDescription( 1095 | key="smart_meter_current_l3", 1096 | name="Smart Meter Current L3", 1097 | icon="mdi:current-ac", 1098 | api_param="5221_C", 1099 | unit=UnitOfElectricCurrent.AMPERE, 1100 | round_digits=2, 1101 | state_class=SensorStateClass.MEASUREMENT, 1102 | device_class=SensorDeviceClass.CURRENT, 1103 | ), 1104 | AlfenSensorDescription( 1105 | key="smart_meter_current_total", 1106 | name="Smart Meter Current total", 1107 | icon="mdi:current-ac", 1108 | api_param="5221_D", 1109 | unit=UnitOfElectricCurrent.AMPERE, 1110 | round_digits=2, 1111 | state_class=SensorStateClass.MEASUREMENT, 1112 | device_class=SensorDeviceClass.CURRENT, 1113 | ), 1114 | AlfenSensorDescription( 1115 | key="number_of_socket", 1116 | name="Number of Socket", 1117 | icon="mdi:information-outline", 1118 | unit=None, 1119 | api_param="205E_0", 1120 | round_digits=None, 1121 | ), 1122 | AlfenSensorDescription( 1123 | key="main_external_min_current_socket_1", 1124 | name="Main External Min Current Socket 1", 1125 | icon="mdi:current-ac", 1126 | api_param="2160_0", 1127 | unit=UnitOfElectricCurrent.AMPERE, 1128 | round_digits=2, 1129 | state_class=SensorStateClass.MEASUREMENT, 1130 | device_class=SensorDeviceClass.CURRENT, 1131 | ), 1132 | AlfenSensorDescription( 1133 | key="main_station_active_max_current_socket_1", 1134 | name="Main Station Active Max Current Socket 1", 1135 | icon="mdi:current-ac", 1136 | api_param="2161_0", 1137 | unit=UnitOfElectricCurrent.AMPERE, 1138 | round_digits=2, 1139 | state_class=SensorStateClass.MEASUREMENT, 1140 | device_class=SensorDeviceClass.CURRENT, 1141 | ), 1142 | AlfenSensorDescription( 1143 | key="custom_tag_socket_1", 1144 | name="Tag Socket 1", 1145 | icon="mdi:badge-account-outline", 1146 | api_param=None, 1147 | unit=None, 1148 | round_digits=None, 1149 | ), 1150 | AlfenSensorDescription( 1151 | key="custom_transaction_socket_1_charging", 1152 | name="Transaction Socket 1 Charging", 1153 | icon="mdi:battery-charging", 1154 | api_param=None, 1155 | unit=UnitOfEnergy.KILO_WATT_HOUR, 1156 | round_digits=None, 1157 | ), 1158 | AlfenSensorDescription( 1159 | key="custom_transaction_socket_1_charging_time", 1160 | name="Transaction Socket 1 Charging Time", 1161 | icon="mdi:clock", 1162 | api_param=None, 1163 | unit=UnitOfTime.MINUTES, 1164 | round_digits=0, 1165 | ), 1166 | AlfenSensorDescription( 1167 | key="custom_transaction_socket_1_charged", 1168 | name="Transaction Socket 1 Last Charge", 1169 | icon="mdi:battery-charging", 1170 | api_param=None, 1171 | unit=UnitOfEnergy.KILO_WATT_HOUR, 1172 | round_digits=None, 1173 | ), 1174 | AlfenSensorDescription( 1175 | key="custom_transaction_socket_1_charged_time", 1176 | name="Transaction Socket 1 Last Charge Time", 1177 | icon="mdi:clock", 1178 | api_param=None, 1179 | unit=UnitOfTime.MINUTES, 1180 | round_digits=0, 1181 | ), 1182 | AlfenSensorDescription( 1183 | key="manufacturer_hardware_version", 1184 | name="Manufacturer Hardware Version", 1185 | icon="mdi:information-outline", 1186 | api_param="1009_0", 1187 | unit=None, 1188 | round_digits=None, 1189 | ), 1190 | AlfenSensorDescription( 1191 | key="manufacturer_software_version", 1192 | name="Manufacturer software Version", 1193 | icon="mdi:information-outline", 1194 | api_param="100A_0", 1195 | unit=None, 1196 | round_digits=None, 1197 | ), 1198 | AlfenSensorDescription( 1199 | key="firmware_version", 1200 | name="Firmware Version", 1201 | icon="mdi:information-outline", 1202 | api_param="2054_0", 1203 | unit=None, 1204 | round_digits=None, 1205 | ), 1206 | AlfenSensorDescription( 1207 | key="bootloader_version", 1208 | name="Bootloader Version", 1209 | icon="mdi:information-outline", 1210 | api_param="3182_0", 1211 | unit=None, 1212 | round_digits=None, 1213 | ), 1214 | AlfenSensorDescription( 1215 | key="ocpp_boot_last_time_send", 1216 | name="OCPP Boot Last Time Send", 1217 | icon="mdi:information-outline", 1218 | api_param="3600_2", 1219 | unit=None, 1220 | round_digits=0, 1221 | ), 1222 | AlfenSensorDescription( 1223 | key="ocpp_boot_accept_time", 1224 | name="OCPP Boot Accept Time", 1225 | icon="mdi:information-outline", 1226 | api_param="3600_3", 1227 | unit=None, 1228 | round_digits=0, 1229 | ), 1230 | # AlfenSensorDescription( 1231 | # key="ocpp_Heartbeat_last_received", 1232 | # name="OCPP Heartbeat Last Received", 1233 | # icon="mdi:information-outline", 1234 | # state_class=None, 1235 | # api_param="3600_6", 1236 | # unit=None, 1237 | # round_digits=0, 1238 | # ), 1239 | AlfenSensorDescription( 1240 | key="ocpp_Heartbeat_last_failed", 1241 | name="OCPP Heartbeat Last Failed", 1242 | icon="mdi:information-outline", 1243 | api_param="3600_7", 1244 | state_class=None, 1245 | unit=None, 1246 | round_digits=0, 1247 | ), 1248 | AlfenSensorDescription( 1249 | key="ocpp_Heartbeat_last_sent", 1250 | name="OCPP Heartbeat Last Sent", 1251 | icon="mdi:information-outline", 1252 | api_param="3600_8", 1253 | state_class=None, 1254 | unit=None, 1255 | round_digits=0, 1256 | ), 1257 | ) 1258 | 1259 | ALFEN_SENSOR_DUAL_SOCKET_TYPES: Final[tuple[AlfenSensorDescription, ...]] = ( 1260 | AlfenSensorDescription( 1261 | key="ps_connector_2_max_allowed_phase", 1262 | name="Connector 2 Max Allowed of Phases", 1263 | icon="mdi:scale-balance", 1264 | unit=None, 1265 | api_param="312F_0", 1266 | round_digits=None, 1267 | ), 1268 | AlfenSensorDescription( 1269 | key="main_state_socket_2", 1270 | name="Main State Socket 2", 1271 | icon="mdi:information-outline", 1272 | unit=None, 1273 | api_param="2502_1", 1274 | round_digits=None, 1275 | ), 1276 | AlfenSensorDescription( 1277 | key="status_socket_2", 1278 | name="Status Code Socket 2", 1279 | icon="mdi:ev-station", 1280 | api_param="2502_2", 1281 | unit=None, 1282 | round_digits=None, 1283 | ), 1284 | AlfenSensorDescription( 1285 | key="power_state_socket_2", 1286 | name="Power State Socket 2", 1287 | icon="mdi:information-outline", 1288 | unit=None, 1289 | api_param="2502_3", 1290 | round_digits=None, 1291 | ), 1292 | AlfenSensorDescription( 1293 | key="mode_3_state_socket_2", 1294 | name="Mode3 State Socket 2", 1295 | icon="mdi:information-outline", 1296 | unit=None, 1297 | api_param="2502_4", 1298 | round_digits=None, 1299 | ), 1300 | AlfenSensorDescription( 1301 | key="comm_car_cp_voltage_high_socket_2", 1302 | name="Car CP Voltage High Socket 2", 1303 | icon="mdi:lightning-bolt", 1304 | api_param="2512_0", 1305 | unit=UnitOfElectricPotential.VOLT, 1306 | round_digits=2, 1307 | state_class=SensorStateClass.MEASUREMENT, 1308 | device_class=SensorDeviceClass.VOLTAGE, 1309 | ), 1310 | AlfenSensorDescription( 1311 | key="comm_car_cp_voltage_low_socket_2", 1312 | name="Car CP Voltage Low Socket 2", 1313 | icon="mdi:lightning-bolt", 1314 | api_param="2512_1", 1315 | unit=UnitOfElectricPotential.VOLT, 1316 | round_digits=2, 1317 | state_class=SensorStateClass.MEASUREMENT, 1318 | device_class=SensorDeviceClass.VOLTAGE, 1319 | ), 1320 | AlfenSensorDescription( 1321 | key="comm_car_pp_resistance_socket_2", 1322 | name="Car PP resistance Socket 2", 1323 | icon="mdi:resistor", 1324 | api_param="2512_2", 1325 | unit="Ω", 1326 | round_digits=1, 1327 | state_class=SensorStateClass.MEASUREMENT, 1328 | ), 1329 | AlfenSensorDescription( 1330 | key="comm_car_pwm_duty_cycle_socket_2", 1331 | name="Car PWM Duty Cycle Socket 2", 1332 | icon="mdi:percent", 1333 | api_param="2512_3", 1334 | unit=PERCENTAGE, 1335 | round_digits=1, 1336 | state_class=SensorStateClass.MEASUREMENT, 1337 | device_class=SensorDeviceClass.POWER_FACTOR, 1338 | ), 1339 | AlfenSensorDescription( 1340 | key="main_external_max_current_socket_2", 1341 | name="Main External Max Current Socket 2", 1342 | icon="mdi:current-ac", 1343 | api_param="312A_0", 1344 | unit=UnitOfElectricCurrent.AMPERE, 1345 | state_class=SensorStateClass.MEASUREMENT, 1346 | device_class=SensorDeviceClass.CURRENT, 1347 | round_digits=2, 1348 | ), 1349 | AlfenSensorDescription( 1350 | key="main_static_lb_max_current_socket_2", 1351 | name="Main Static LB Max Current Socket 2", 1352 | icon="mdi:current-ac", 1353 | api_param="312B_0", 1354 | unit=UnitOfElectricCurrent.AMPERE, 1355 | round_digits=2, 1356 | state_class=SensorStateClass.MEASUREMENT, 1357 | device_class=SensorDeviceClass.CURRENT, 1358 | ), 1359 | AlfenSensorDescription( 1360 | key="main_active_lb_max_current_socket_2", 1361 | name="Main Active LB Max Current Socket 2", 1362 | icon="mdi:current-ac", 1363 | api_param="312D_0", 1364 | unit=UnitOfElectricCurrent.AMPERE, 1365 | round_digits=2, 1366 | state_class=SensorStateClass.MEASUREMENT, 1367 | device_class=SensorDeviceClass.CURRENT, 1368 | ), 1369 | AlfenSensorDescription( 1370 | key="main_external_min_current_socket_2", 1371 | name="Main External Min Current Socket 2", 1372 | icon="mdi:current-ac", 1373 | api_param="3160_0", 1374 | unit=UnitOfElectricCurrent.AMPERE, 1375 | round_digits=2, 1376 | state_class=SensorStateClass.MEASUREMENT, 1377 | device_class=SensorDeviceClass.CURRENT, 1378 | ), 1379 | AlfenSensorDescription( 1380 | key="main_active_max_current_socket_2", 1381 | name="Main Active Max Current Socket 2", 1382 | icon="mdi:current-ac", 1383 | api_param="312C_0", 1384 | unit=UnitOfElectricCurrent.AMPERE, 1385 | round_digits=2, 1386 | state_class=SensorStateClass.MEASUREMENT, 1387 | device_class=SensorDeviceClass.CURRENT, 1388 | ), 1389 | AlfenSensorDescription( 1390 | key="ui_state_2", 1391 | name="Display State Socket 2", 1392 | icon="mdi:information-outline", 1393 | unit=None, 1394 | api_param="3191_1", 1395 | round_digits=None, 1396 | ), 1397 | AlfenSensorDescription( 1398 | key="ui_error_number_2", 1399 | name="Display Error Number Socket 2", 1400 | icon="mdi:information-outline", 1401 | unit=None, 1402 | api_param="3191_2", 1403 | round_digits=None, 1404 | ), 1405 | AlfenSensorDescription( 1406 | key="frequency_socket_2", 1407 | name="Frequency Socket 2", 1408 | icon="mdi:information-outline", 1409 | api_param="3221_12", 1410 | unit=UnitOfFrequency.HERTZ, 1411 | round_digits=0, 1412 | ), 1413 | AlfenSensorDescription( 1414 | key="meter_reading_socket_2", 1415 | name="Meter Reading Socket 2", 1416 | icon="mdi:counter", 1417 | api_param="3221_22", 1418 | unit=UnitOfEnergy.KILO_WATT_HOUR, 1419 | round_digits=None, 1420 | state_class=SensorStateClass.TOTAL_INCREASING, 1421 | device_class=SensorDeviceClass.ENERGY, 1422 | ), 1423 | AlfenSensorDescription( 1424 | key="active_power_total_socket_2", 1425 | name="Active Power Total Socket 2", 1426 | icon="mdi:circle-slice-3", 1427 | api_param="3221_16", 1428 | unit=UnitOfPower.WATT, 1429 | round_digits=2, 1430 | state_class=SensorStateClass.MEASUREMENT, 1431 | device_class=SensorDeviceClass.POWER, 1432 | ), 1433 | AlfenSensorDescription( 1434 | key="voltage_l1_socket_2", 1435 | name="Voltage L1N Socket 2", 1436 | icon="mdi:flash", 1437 | api_param="3221_3", 1438 | unit=UnitOfElectricPotential.VOLT, 1439 | round_digits=1, 1440 | state_class=SensorStateClass.MEASUREMENT, 1441 | device_class=SensorDeviceClass.VOLTAGE, 1442 | ), 1443 | AlfenSensorDescription( 1444 | key="voltage_l2_socket_2", 1445 | name="Voltage L2N Socket 2", 1446 | icon="mdi:flash", 1447 | api_param="3221_4", 1448 | unit=UnitOfElectricPotential.VOLT, 1449 | round_digits=1, 1450 | state_class=SensorStateClass.MEASUREMENT, 1451 | device_class=SensorDeviceClass.VOLTAGE, 1452 | ), 1453 | AlfenSensorDescription( 1454 | key="voltage_l3_socket_2", 1455 | name="Voltage L3N Socket 2", 1456 | icon="mdi:flash", 1457 | api_param="3221_5", 1458 | unit=UnitOfElectricPotential.VOLT, 1459 | round_digits=1, 1460 | state_class=SensorStateClass.MEASUREMENT, 1461 | device_class=SensorDeviceClass.VOLTAGE, 1462 | ), 1463 | # AlfenSensorDescription( 1464 | # key="voltage_l1l2_socket_2", 1465 | # name="Voltage L1L2 Socket 2", 1466 | # icon="mdi:flash", 1467 | # api_param="3221_6", 1468 | # unit=UnitOfElectricPotential.VOLT, 1469 | # round_digits=1, 1470 | # state_class=SensorStateClass.MEASUREMENT, 1471 | # device_class=SensorDeviceClass.VOLTAGE, 1472 | # ), 1473 | # AlfenSensorDescription( 1474 | # key="voltage_l2l3_socket_2", 1475 | # name="Voltage L2L3 Socket 2", 1476 | # icon="mdi:flash", 1477 | # api_param="3221_7", 1478 | # unit=UnitOfElectricPotential.VOLT, 1479 | # round_digits=1, 1480 | # state_class=SensorStateClass.MEASUREMENT, 1481 | # device_class=SensorDeviceClass.VOLTAGE, 1482 | # ), 1483 | # AlfenSensorDescription( 1484 | # key="voltage_l3l1_socket_2", 1485 | # name="Voltage L3L1 Socket 2", 1486 | # icon="mdi:flash", 1487 | # api_param="3221_8", 1488 | # unit=UnitOfElectricPotential.VOLT, 1489 | # round_digits=1, 1490 | # state_class=SensorStateClass.MEASUREMENT, 1491 | # device_class=SensorDeviceClass.VOLTAGE, 1492 | # ), 1493 | AlfenSensorDescription( 1494 | key="current_n_socket_2", 1495 | name="Current N Socket 2", 1496 | icon="mdi:current-ac", 1497 | api_param="3221_9", 1498 | unit=UnitOfElectricCurrent.AMPERE, 1499 | round_digits=2, 1500 | state_class=SensorStateClass.MEASUREMENT, 1501 | device_class=SensorDeviceClass.CURRENT, 1502 | ), 1503 | AlfenSensorDescription( 1504 | key="current_l1_socket_2", 1505 | name="Current L1 Socket 2", 1506 | icon="mdi:current-ac", 1507 | api_param="3221_A", 1508 | unit=UnitOfElectricCurrent.AMPERE, 1509 | round_digits=2, 1510 | state_class=SensorStateClass.MEASUREMENT, 1511 | device_class=SensorDeviceClass.CURRENT, 1512 | ), 1513 | AlfenSensorDescription( 1514 | key="current_l2_socket_2", 1515 | name="Current L2 Socket 2", 1516 | icon="mdi:current-ac", 1517 | api_param="3221_B", 1518 | unit=UnitOfElectricCurrent.AMPERE, 1519 | round_digits=2, 1520 | state_class=SensorStateClass.MEASUREMENT, 1521 | device_class=SensorDeviceClass.CURRENT, 1522 | ), 1523 | AlfenSensorDescription( 1524 | key="current_l3_socket_2", 1525 | name="Current L3 Socket 2", 1526 | icon="mdi:current-ac", 1527 | api_param="3221_C", 1528 | unit=UnitOfElectricCurrent.AMPERE, 1529 | round_digits=2, 1530 | state_class=SensorStateClass.MEASUREMENT, 1531 | device_class=SensorDeviceClass.CURRENT, 1532 | ), 1533 | AlfenSensorDescription( 1534 | key="current_total_socket_2", 1535 | name="Current total Socket 2", 1536 | icon="mdi:current-ac", 1537 | api_param="3221_D", 1538 | unit=UnitOfElectricCurrent.AMPERE, 1539 | round_digits=2, 1540 | state_class=SensorStateClass.MEASUREMENT, 1541 | device_class=SensorDeviceClass.CURRENT, 1542 | ), 1543 | AlfenSensorDescription( 1544 | key="custom_tag_socket_2", 1545 | name="Tag Socket 2", 1546 | icon="mdi:badge-account-outline", 1547 | api_param=None, 1548 | unit=None, 1549 | round_digits=None, 1550 | ), 1551 | AlfenSensorDescription( 1552 | key="custom_transaction_socket_2_stop", 1553 | name="Transaction Socket 2 Stop", 1554 | icon="mdi:badge-account-outline", 1555 | api_param=None, 1556 | unit=None, 1557 | round_digits=None, 1558 | ), 1559 | AlfenSensorDescription( 1560 | key="custom_transaction_socket_2_charging", 1561 | name="Transaction Socket 2 Charging", 1562 | icon="mdi:battery-charging", 1563 | api_param=None, 1564 | unit=UnitOfEnergy.KILO_WATT_HOUR, 1565 | round_digits=None, 1566 | ), 1567 | AlfenSensorDescription( 1568 | key="custom_transaction_socket_2_charging_time", 1569 | name="Transaction Socket 2 Charging Time", 1570 | icon="mdi:clock", 1571 | api_param=None, 1572 | unit=UnitOfTime.MINUTES, 1573 | round_digits=0, 1574 | ), 1575 | AlfenSensorDescription( 1576 | key="custom_transaction_socket_2_charged", 1577 | name="Transaction Socket 2 Last Charge", 1578 | icon="mdi:battery-charging", 1579 | api_param=None, 1580 | unit=UnitOfEnergy.KILO_WATT_HOUR, 1581 | round_digits=None, 1582 | ), 1583 | AlfenSensorDescription( 1584 | key="custom_transaction_socket_2_charged_time", 1585 | name="Transaction Socket 2 Last Charge Time", 1586 | icon="mdi:clock", 1587 | api_param=None, 1588 | unit=UnitOfTime.MINUTES, 1589 | round_digits=0, 1590 | ), 1591 | ) 1592 | 1593 | 1594 | async def async_setup_platform( 1595 | hass: HomeAssistant, 1596 | config: AlfenConfigEntry, 1597 | async_add_entities: AddEntitiesCallback, 1598 | discovery_info=None, 1599 | ): 1600 | """Set up the Alfen sensor.""" 1601 | 1602 | 1603 | async def async_setup_entry( 1604 | hass: HomeAssistant, 1605 | entry: AlfenConfigEntry, 1606 | async_add_entities: AddEntitiesCallback, 1607 | ): 1608 | """Set up using config_entry.""" 1609 | sensors = [AlfenSensor(entry, description) for description in ALFEN_SENSOR_TYPES] 1610 | 1611 | async_add_entities(sensors) 1612 | async_add_entities([AlfenMainSensor(entry, ALFEN_SENSOR_TYPES[0])]) 1613 | 1614 | coordinator = entry.runtime_data 1615 | if coordinator.device.get_number_of_sockets() == 2: 1616 | sensors = [ 1617 | AlfenSensor(entry, description) 1618 | for description in ALFEN_SENSOR_DUAL_SOCKET_TYPES 1619 | ] 1620 | async_add_entities(sensors) 1621 | 1622 | platform = entity_platform.current_platform.get() 1623 | 1624 | platform.async_register_entity_service( 1625 | SERVICE_REBOOT_WALLBOX, 1626 | {}, 1627 | "async_reboot_wallbox", 1628 | ) 1629 | 1630 | 1631 | class AlfenMainSensor(AlfenEntity): 1632 | """Representation of a Alfen Main Sensor.""" 1633 | 1634 | entity_description: AlfenSensorDescription 1635 | 1636 | def __init__( 1637 | self, entry: AlfenConfigEntry, description: AlfenSensorDescription 1638 | ) -> None: 1639 | """Initialize the sensor.""" 1640 | super().__init__(entry) 1641 | 1642 | self._sensor = "sensor" 1643 | self.entity_description = description 1644 | 1645 | @property 1646 | def unique_id(self): 1647 | """Return a unique ID.""" 1648 | return f"{self.coordinator.device.id}-{self._sensor}" 1649 | 1650 | @property 1651 | def icon(self): 1652 | """Return the icon.""" 1653 | return "mdi:car-electric" 1654 | 1655 | @property 1656 | def state(self): 1657 | """Return the state of the sensor.""" 1658 | if self.entity_description.api_param in self.coordinator.device.properties: 1659 | prop = self.coordinator.device.properties[self.entity_description.api_param] 1660 | if prop[ID] == self.entity_description.api_param: 1661 | # exception 1662 | # status only from socket 1 1663 | if prop[ID] == "2501_2": 1664 | return STATUS_DICT.get(prop[VALUE], "Unknown") 1665 | 1666 | if self.entity_description.round_digits is not None: 1667 | return round(prop[VALUE], self.entity_description.round_digits) 1668 | 1669 | return prop[VALUE] 1670 | 1671 | return "Unknown" 1672 | 1673 | @property 1674 | def extra_state_attributes(self): 1675 | """Return the default attributes of the element.""" 1676 | if self.entity_description.api_param in self.coordinator.device.properties: 1677 | prop = self.coordinator.device.properties[self.entity_description.api_param] 1678 | if prop[ID] == self.entity_description.api_param: 1679 | return {"category": prop[CAT]} 1680 | return None 1681 | 1682 | async def async_reboot_wallbox(self): 1683 | """Reboot the wallbox.""" 1684 | await self.coordinator.device.reboot_wallbox() 1685 | 1686 | async def async_update(self): 1687 | """Update the sensor.""" 1688 | await self.coordinator.device.async_update() 1689 | 1690 | @property 1691 | def device_info(self): 1692 | """Return a device description for device registry.""" 1693 | return self.coordinator.device.device_info 1694 | 1695 | 1696 | class AlfenSensor(AlfenEntity, SensorEntity): 1697 | """Representation of a Alfen Sensor.""" 1698 | 1699 | entity_description: AlfenSensorDescription 1700 | 1701 | def __init__( 1702 | self, entry: AlfenConfigEntry, description: AlfenSensorDescription 1703 | ) -> None: 1704 | """Initialize the sensor.""" 1705 | super().__init__(entry) 1706 | 1707 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 1708 | self._attr_unique_id = f"{self._attr_unique_id}-{description.key}" 1709 | self.entity_description = description 1710 | if description.state_class is not None: 1711 | self._attr_state_class = description.state_class 1712 | if description.device_class is not None: 1713 | self._attr_device_class = description.device_class 1714 | 1715 | self._async_update_attrs() 1716 | 1717 | def _get_current_value(self) -> StateType | None: 1718 | """Get the current value.""" 1719 | if self.entity_description.api_param in self.coordinator.device.properties: 1720 | return self.coordinator.device.properties[ 1721 | self.entity_description.api_param 1722 | ][VALUE] 1723 | return None 1724 | 1725 | @callback 1726 | def _async_update_attrs(self) -> None: 1727 | """Update the state and attributes.""" 1728 | self._attr_native_value = self._get_current_value() 1729 | 1730 | @property 1731 | def unique_id(self) -> str: 1732 | """Return a unique ID.""" 1733 | return f"{self.coordinator.device.id}-{self.entity_description.key}" 1734 | 1735 | @property 1736 | def name(self) -> str: 1737 | """Return the name of the sensor.""" 1738 | return self._attr_name 1739 | 1740 | @property 1741 | def icon(self) -> str | None: 1742 | """Return the icon of the sensor.""" 1743 | return self.entity_description.icon 1744 | 1745 | @property 1746 | def native_value(self) -> StateType: 1747 | """Return the state of the sensor.""" 1748 | return round(self.state, 2) 1749 | 1750 | @property 1751 | def native_unit_of_measurement(self) -> str | None: 1752 | """Return the unit the value is expressed in.""" 1753 | return self.entity_description.unit 1754 | 1755 | def _processTransactionKWh( 1756 | self, socket: str, entity_description: AlfenSensorDescription 1757 | ): 1758 | if self.coordinator.device.latest_tag is None: 1759 | return "Unknown" 1760 | ## calculate the usage 1761 | startkWh = None 1762 | mvkWh = None 1763 | stopkWh = None 1764 | lastkWh = None 1765 | 1766 | for key, value in self.coordinator.device.latest_tag.items(): 1767 | if key[0] == socket and key[1] == "start" and key[2] == "kWh": 1768 | startkWh = value 1769 | continue 1770 | if key[0] == socket and key[1] == "mv" and key[2] == "kWh": 1771 | mvkWh = value 1772 | continue 1773 | if key[0] == socket and key[1] == "stop" and key[2] == "kWh": 1774 | stopkWh = value 1775 | continue 1776 | if key[0] == socket and key[1] == "last_start" and key[2] == "kWh": 1777 | lastkWh = value 1778 | continue 1779 | 1780 | # if the entity_key end with _charging, then we are calculating the charging 1781 | if ( 1782 | startkWh is not None 1783 | and mvkWh is not None 1784 | and entity_description.key.endswith("_charging") 1785 | ): 1786 | # if we have stopkWh and it is higher then mvkWh, then we are not charging anymore and we should return 0 1787 | if stopkWh is not None and float(stopkWh) >= float(mvkWh): 1788 | return 0 1789 | value = round(float(mvkWh) - float(startkWh), 2) 1790 | if entity_description.round_digits is not None: 1791 | return round( 1792 | value, 1793 | ( 1794 | entity_description.round_digits 1795 | if entity_description.round_digits > 0 1796 | else None 1797 | ), 1798 | ) 1799 | return value 1800 | 1801 | # if the entity_key end with _charged, then we are calculating the charged 1802 | if ( 1803 | lastkWh is not None 1804 | and stopkWh is not None 1805 | and entity_description.key.endswith("_charged") 1806 | ): 1807 | if float(stopkWh) >= float(lastkWh): 1808 | value = round(float(stopkWh) - float(lastkWh), 2) 1809 | if entity_description.round_digits is not None: 1810 | return round( 1811 | value, 1812 | ( 1813 | entity_description.round_digits 1814 | if entity_description.round_digits > 0 1815 | else None 1816 | ), 1817 | ) 1818 | return value 1819 | return None 1820 | 1821 | def _processTransactionTime( 1822 | self, socket: str, entity_description: AlfenSensorDescription 1823 | ): 1824 | if self.coordinator.device.latest_tag is None: 1825 | return "Unknown" 1826 | 1827 | startDate = None 1828 | mvDate = None 1829 | stopDate = None 1830 | lastDate = None 1831 | 1832 | startDate2 = None 1833 | mvDate2 = None 1834 | stopDate2 = None 1835 | lastDate2 = None 1836 | 1837 | for key, value in self.coordinator.device.latest_tag.items(): 1838 | if key[0] == "socket 1" and key[1] == "start" and key[2] == "date": 1839 | startDate = value 1840 | continue 1841 | if key[0] == "socket 1" and key[1] == "mv" and key[2] == "date": 1842 | mvDate = value 1843 | continue 1844 | if key[0] == "socket 1" and key[1] == "stop" and key[2] == "date": 1845 | stopDate = value 1846 | continue 1847 | if key[0] == "socket 1" and key[1] == "last_start" and key[2] == "date": 1848 | lastDate = value 1849 | continue 1850 | 1851 | # socket 2 1852 | if key[0] == "socket 2" and key[1] == "start" and key[2] == "date": 1853 | startDate2 = value 1854 | continue 1855 | if key[0] == "socket 2" and key[1] == "mv" and key[2] == "date": 1856 | mvDate2 = value 1857 | continue 1858 | if key[0] == "socket 2" and key[1] == "stop" and key[2] == "date": 1859 | stopDate2 = value 1860 | continue 1861 | if key[0] == "socket 2" and key[1] == "last_start" and key[2] == "date": 1862 | lastDate2 = value 1863 | continue 1864 | 1865 | if ( 1866 | startDate is not None 1867 | and mvDate is not None 1868 | and entity_description.key == "custom_transaction_socket_1_charging_time" 1869 | ): 1870 | return self._getChargingTime( 1871 | startDate, mvDate, stopDate, entity_description 1872 | ) 1873 | 1874 | if ( 1875 | startDate2 is not None 1876 | and mvDate2 is not None 1877 | and entity_description.key == "custom_transaction_socket_2_charging_time" 1878 | ): 1879 | return self._getChargingTime( 1880 | startDate2, mvDate2, stopDate2, entity_description 1881 | ) 1882 | 1883 | if ( 1884 | lastDate is not None 1885 | and stopDate is not None 1886 | and entity_description.key == "custom_transaction_socket_1_charged_time" 1887 | ): 1888 | return self._getChargedTime(lastDate, stopDate, entity_description) 1889 | 1890 | if ( 1891 | lastDate2 is not None 1892 | and stopDate2 is not None 1893 | and entity_description.key == "custom_transaction_socket_2_charged_time" 1894 | ): 1895 | return self._getChargedTime(lastDate2, stopDate2, entity_description) 1896 | return None 1897 | 1898 | def _customTransactionCode(self, socker_number: int): 1899 | if self.entity_description.key == f"custom_tag_socket_{socker_number}": 1900 | if self.coordinator.device.latest_tag is None: 1901 | return "No Tag" 1902 | for key, value in self.coordinator.device.latest_tag.items(): 1903 | if ( 1904 | key[0] == f"socket {socker_number}" 1905 | and key[1] == "start" 1906 | and key[2] == "tag" 1907 | ): 1908 | return value 1909 | return "No Tag" 1910 | 1911 | if self.entity_description.key in ( 1912 | f"custom_transaction_socket_{socker_number}_charging", 1913 | f"custom_transaction_socket_{socker_number}_charged", 1914 | ): 1915 | value = self._processTransactionKWh( 1916 | f"socket {socker_number}", self.entity_description 1917 | ) 1918 | if value is not None: 1919 | return value 1920 | 1921 | if self.entity_description.key in [ 1922 | f"custom_transaction_socket_{socker_number}_charging_time", 1923 | f"custom_transaction_socket_{socker_number}_charged_time", 1924 | ]: 1925 | value = self._processTransactionTime( 1926 | "socket " + str(socker_number), self.entity_description 1927 | ) 1928 | if value is not None: 1929 | return value 1930 | return None 1931 | 1932 | def _getChargedTime(self, lastDate, stopDate, entity_description): 1933 | lastDate = datetime.datetime.strptime(lastDate, "%Y-%m-%d %H:%M:%S") 1934 | stopDate = datetime.datetime.strptime(stopDate, "%Y-%m-%d %H:%M:%S") 1935 | 1936 | if stopDate < lastDate: 1937 | return None 1938 | # return the value in minutes 1939 | value = round((stopDate - lastDate).total_seconds() / 60, 2) 1940 | if entity_description.round_digits is not None: 1941 | return round( 1942 | value, 1943 | ( 1944 | entity_description.round_digits 1945 | if entity_description.round_digits > 0 1946 | else None 1947 | ), 1948 | ) 1949 | return value 1950 | 1951 | def _getChargingTime(self, startDate, mvDate, stopDate, entity_description): 1952 | startDate = datetime.datetime.strptime(startDate, "%Y-%m-%d %H:%M:%S") 1953 | mvDate = datetime.datetime.strptime(mvDate, "%Y-%m-%d %H:%M:%S") 1954 | if stopDate is not None: 1955 | stopDate = datetime.datetime.strptime(stopDate, "%Y-%m-%d %H:%M:%S") 1956 | 1957 | # if there is a stopdate greater then startDate, then we are not charging anymore 1958 | if stopDate is not None and stopDate > startDate: 1959 | return 0 1960 | 1961 | # return the value in minutes 1962 | value = round((mvDate - startDate).total_seconds() / 60, 2) 1963 | if entity_description.round_digits is not None: 1964 | return round( 1965 | value, 1966 | ( 1967 | entity_description.round_digits 1968 | if entity_description.round_digits > 0 1969 | else None 1970 | ), 1971 | ) 1972 | return value 1973 | 1974 | @property 1975 | def state(self) -> StateType: # noqa: C901 1976 | """Return the state of the sensor.""" 1977 | # state of none Api param 1978 | if self.entity_description.api_param is None: 1979 | voltage_l1 = None 1980 | voltage_l2 = None 1981 | voltage_l3 = None 1982 | current_l1 = None 1983 | current_l2 = None 1984 | current_l3 = None 1985 | 1986 | for prop in self.coordinator.device.properties: 1987 | value = self.coordinator.device.properties[prop][VALUE] 1988 | if prop == "5221_3": 1989 | voltage_l1 = value 1990 | elif prop == "5221_4": 1991 | voltage_l2 = value 1992 | elif prop == "5221_5": 1993 | voltage_l3 = value 1994 | elif prop == "212F_1": 1995 | current_l1 = value 1996 | elif prop == "212F_2": 1997 | current_l2 = value 1998 | elif prop == "212F_3": 1999 | current_l3 = value 2000 | 2001 | if self.entity_description.key == "smart_meter_l1": 2002 | if voltage_l1 is not None and current_l1 is not None: 2003 | return round(float(voltage_l1) * float(current_l1), 2) 2004 | if self.entity_description.key == "smart_meter_l2": 2005 | if voltage_l2 is not None and current_l2 is not None: 2006 | return round(float(voltage_l2) * float(current_l2), 2) 2007 | if self.entity_description.key == "smart_meter_l3": 2008 | if voltage_l3 is not None and current_l3 is not None: 2009 | return round(float(voltage_l3) * float(current_l3), 2) 2010 | if self.entity_description.key == "smart_meter_total": 2011 | if ( 2012 | voltage_l1 is not None 2013 | and current_l1 is not None 2014 | and voltage_l2 is not None 2015 | and current_l2 is not None 2016 | and voltage_l3 is not None 2017 | and current_l3 is not None 2018 | ): 2019 | return round( 2020 | ( 2021 | float(voltage_l1) * float(current_l1) 2022 | + float(voltage_l2) * float(current_l2) 2023 | + float(voltage_l3) * float(current_l3) 2024 | ), 2025 | 2, 2026 | ) 2027 | 2028 | # Custom code for transaction and tag 2029 | for socketNr in [1, 2]: 2030 | value = self._customTransactionCode(socketNr) 2031 | if value is not None: 2032 | return value 2033 | 2034 | if self.entity_description.api_param in self.coordinator.device.properties: 2035 | prop = self.coordinator.device.properties[self.entity_description.api_param] 2036 | # some exception of return value 2037 | 2038 | # Display state status 2039 | if self.entity_description.api_param in ("3190_1", "3191_1"): 2040 | if prop[VALUE] == 28: 2041 | return "See error Number" 2042 | 2043 | return STATUS_DICT.get(prop[VALUE], "Unknown") 2044 | 2045 | # meter_reading from w to kWh 2046 | if self.entity_description.api_param in ("2221_22", "3221_22"): 2047 | return round((prop[VALUE] / 1000), 2) 2048 | 2049 | # Car PWM Duty cycle % 2050 | if self.entity_description.api_param == "2511_3": 2051 | return round((prop[VALUE] / 100), self.entity_description.round_digits) 2052 | 2053 | # change milliseconds to HH:MM:SS 2054 | if self.entity_description.key == "uptime": 2055 | return str(datetime.timedelta(milliseconds=prop[VALUE])).split( 2056 | ".", maxsplit=1 2057 | )[0] 2058 | 2059 | if self.entity_description.key == "uptime_hours": 2060 | result = 0 2061 | value = str(datetime.timedelta(milliseconds=prop[VALUE])) 2062 | days = value.split(" day") 2063 | if len(days) > 1: 2064 | result = int(days[0]) * 24 2065 | hours = days[1].split(", ")[1].split(":", maxsplit=1)[0] 2066 | else: 2067 | hours = value.split(":", maxsplit=1)[0] 2068 | result += int(hours) 2069 | return result 2070 | 2071 | # change milliseconds to d/m/y HH:MM:SS 2072 | if self.entity_description.api_param in ("2187_0", "2059_0"): 2073 | return datetime.datetime.fromtimestamp(prop[VALUE] / 1000).strftime( 2074 | "%d/%m/%Y %H:%M:%S" 2075 | ) 2076 | # change milliseconds to HH:MM:SS 2077 | if self.entity_description.api_param in ( 2078 | "3600_2", 2079 | "3600_3", 2080 | "3600_6", 2081 | "3600_7", 2082 | "3600_8", 2083 | ): 2084 | return datetime.datetime.fromtimestamp(prop[VALUE] / 1000).strftime( 2085 | "%H:%M:%S" 2086 | ) 2087 | 2088 | # Allowed phase 1 or Allowed Phase 2 2089 | if (self.entity_description.api_param == "312E_0") | ( 2090 | self.entity_description.api_param == "312F_0" 2091 | ): 2092 | return ALLOWED_PHASE_DICT.get(prop[VALUE], "Unknown") 2093 | 2094 | if self.entity_description.round_digits is not None: 2095 | # check prop[VALUE] if it is an integer 2096 | return round(prop[VALUE], self.entity_description.round_digits) 2097 | 2098 | # mode3_state 2099 | if self.entity_description.api_param in ("2501_4", "2502_4"): 2100 | return MODE_3_STAT_DICT.get(prop[VALUE], "Unknown") 2101 | 2102 | # Socket CPRO State 2103 | if self.entity_description.api_param in ("2501_3", "2502_3"): 2104 | return POWER_STATES_DICT.get(prop[VALUE], "Unknown") 2105 | 2106 | # Main CSM State 2107 | if self.entity_description.api_param in ("2501_1", "2502_1"): 2108 | return MAIN_STATE_DICT.get(prop[VALUE], "Unknown") 2109 | 2110 | # OCPP Boot notification 2111 | if self.entity_description.api_param == "3600_1": 2112 | return OCPP_BOOT_NOTIFICATION_STATUS_DICT.get(prop[VALUE], "Unknown") 2113 | 2114 | # OCPP Boot notification 2115 | if self.entity_description.api_param == "2540_0": 2116 | return MODBUS_CONNECTION_STATES_DICT.get(prop[VALUE], "Unknown") 2117 | 2118 | # wallbox display message 2119 | if self.entity_description.api_param in ("3190_2", "3191_2"): 2120 | return ( 2121 | str(prop[VALUE]) 2122 | + ": " 2123 | + DISPLAY_ERROR_DICT.get(prop[VALUE], "Unknown") 2124 | ) 2125 | 2126 | # Status code 2127 | if self.entity_description.api_param in ("2501_2", "2502_2"): 2128 | return STATUS_DICT.get(prop[VALUE], "Unknown") 2129 | 2130 | return prop[VALUE] 2131 | return None 2132 | 2133 | @property 2134 | def extra_state_attributes(self): 2135 | """Return the default attributes of the element.""" 2136 | if self.entity_description.api_param in self.coordinator.device.properties: 2137 | return { 2138 | "category": self.coordinator.device.properties[ 2139 | self.entity_description.api_param 2140 | ][CAT] 2141 | } 2142 | return None 2143 | 2144 | @property 2145 | def unit_of_measurement(self) -> str: 2146 | """Return the unit of measurement.""" 2147 | return self.entity_description.unit 2148 | 2149 | async def async_update(self): 2150 | """Get the latest data and updates the states.""" 2151 | self._async_update_attrs() 2152 | 2153 | @property 2154 | def device_info(self) -> DeviceInfo: 2155 | """Return a device description for device registry.""" 2156 | return self.coordinator.device.device_info 2157 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/services.yaml: -------------------------------------------------------------------------------- 1 | reboot_wallbox: 2 | description: Reboot alfen wallbox 3 | fields: 4 | entity_id: 5 | description: Name(s) of entities to change. 6 | example: "alfen_wallbox.garage" 7 | 8 | set_current_limit: 9 | description: Set current charge limit 10 | fields: 11 | entity_id: 12 | description: Name(s) of entities to change. 13 | example: "number.wallbox_current_limit" 14 | limit: 15 | description: New limit. 16 | example: 16 17 | 18 | enable_rfid_authorization_mode: 19 | description: Enables RFID auth mode 20 | fields: 21 | entity_id: 22 | description: Name(s) of entities to change. 23 | example: "select.wallbox_authorization_mode" 24 | 25 | disable_rfid_authorization_mode: 26 | description: Disables RFID auth mode 27 | fields: 28 | entity_id: 29 | description: Name(s) of entities to change. 30 | example: "select.wallbox_authorization_mode" 31 | 32 | set_current_phase: 33 | description: Set current phase mapping 34 | fields: 35 | entity_id: 36 | description: Name(s) of entities to change. 37 | example: "select.wallbox_active_load_balancing_phase_connection" 38 | phase: 39 | description: phase. 40 | example: "L1" 41 | 42 | enable_phase_switching: 43 | description: Enable phase switching 44 | fields: 45 | entity_id: 46 | description: Name(s) of entities to change. 47 | example: "switch.wallbox_enable_phase_switching" 48 | 49 | disable_phase_switching: 50 | description: Disable phase switching 51 | fields: 52 | entity_id: 53 | description: Name(s) of entities to change. 54 | example: "switch.wallbox_enable_phase_switching" 55 | 56 | set_green_share: 57 | description: Set Green Share Percentage 58 | fields: 59 | entity_id: 60 | description: Name(s) of entities to change. 61 | example: "number.wallbox_solar_green_share" 62 | value: 63 | description: New value. 64 | example: 80 65 | 66 | set_comfort_power: 67 | description: Set Solar Comfort Power 68 | fields: 69 | entity_id: 70 | description: Name(s) of entities to change. 71 | example: "number.wallbox_solar_comfort_level" 72 | value: 73 | description: New value. 74 | example: 1400 75 | 76 | get_transactions: 77 | fields: 78 | start: 79 | required: false 80 | example: "2024-01-01 00:00:00" 81 | selector: 82 | datetime: 83 | end: 84 | required: false 85 | example: "2024-01-01 23:00:00" 86 | selector: 87 | datetime: 88 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configure Alfen Wallbox", 6 | "description": "Enter IP address and credentials of your Alfen Wallbox.", 7 | "data": { 8 | "host": "Host", 9 | "name": "Friendly name", 10 | "username": "User name", 11 | "password": "Password" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "device_timeout": "Timeout connecting to the device.", 17 | "device_fail": "Unexpected error creating device.", 18 | "already_configured": "Device is already configured" 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "scan_interval": "Scan interval", 26 | "timeout": "Timeout", 27 | "refresh_categories": "Select the categories to update periodically" 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/switch.py: -------------------------------------------------------------------------------- 1 | """Support for Alfen Eve Single Proline Wallbox.""" 2 | 3 | from dataclasses import dataclass 4 | from typing import Any, Final 5 | 6 | from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers import entity_platform 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .const import ( 12 | CAT, 13 | ID, 14 | SERVICE_DISABLE_PHASE_SWITCHING, 15 | SERVICE_ENABLE_PHASE_SWITCHING, 16 | VALUE, 17 | ) 18 | from .coordinator import AlfenConfigEntry 19 | from .entity import AlfenEntity 20 | 21 | 22 | @dataclass 23 | class AlfenSwitchDescriptionMixin: 24 | """Define an entity description mixin for binary sensor entities.""" 25 | 26 | api_param: str 27 | 28 | 29 | @dataclass 30 | class AlfenSwitchDescription(SwitchEntityDescription, AlfenSwitchDescriptionMixin): 31 | """Class to describe an Alfen binary sensor entity.""" 32 | 33 | 34 | ALFEN_BINARY_SENSOR_TYPES: Final[tuple[AlfenSwitchDescription, ...]] = ( 35 | AlfenSwitchDescription( 36 | key="lb_enable_phase_switching", 37 | name="Load Balancing Enable Phase Switching", 38 | api_param="2185_0", 39 | ), 40 | AlfenSwitchDescription( 41 | key="dp_light_auto_dim", 42 | name="Display Light Auto Dim", 43 | api_param="2061_1", 44 | ), 45 | AlfenSwitchDescription( 46 | key="lb_solar_charging_boost", 47 | name="Solar Charging Boost Socket 1", 48 | api_param="3280_4", 49 | ), 50 | AlfenSwitchDescription( 51 | key="lb_solar_charging_boost_socket_2", 52 | name="Solar Charging Boost Socket 2", 53 | api_param="3280_5", 54 | ), 55 | AlfenSwitchDescription( 56 | key="auth_white_list", 57 | name="Auth. Whitelist", 58 | api_param="213B_0", 59 | ), 60 | AlfenSwitchDescription( 61 | key="auth_local_list", 62 | name="Auth. Local List", 63 | api_param="213D_0", 64 | ), 65 | AlfenSwitchDescription( 66 | key="auth_restart_after_power_outage", 67 | name="Auth. Restart after Power Outage", 68 | api_param="215E_0", 69 | ), 70 | AlfenSwitchDescription( 71 | key="auth_remote_transaction_request", 72 | name="Auth. Remote Transaction requests", 73 | api_param="209B_0", 74 | ), 75 | AlfenSwitchDescription( 76 | key="proxy_enabled", 77 | name="Proxy Enabled", 78 | api_param="2117_0", 79 | ), 80 | AlfenSwitchDescription( 81 | key="active_load_balancing", 82 | name="Active Load Balancing", 83 | api_param="2064_0", 84 | ), 85 | ) 86 | 87 | 88 | async def async_setup_entry( 89 | hass: HomeAssistant, 90 | entry: AlfenConfigEntry, 91 | async_add_entities: AddEntitiesCallback, 92 | ) -> None: 93 | """Set up Alfen switch entities from a config entry.""" 94 | 95 | switches = [ 96 | AlfenSwitchSensor(entry, description) 97 | for description in ALFEN_BINARY_SENSOR_TYPES 98 | ] 99 | 100 | async_add_entities(switches) 101 | 102 | platform = entity_platform.current_platform.get() 103 | 104 | platform.async_register_entity_service( 105 | SERVICE_ENABLE_PHASE_SWITCHING, 106 | {}, 107 | "async_enable_phase_switching", 108 | ) 109 | 110 | platform.async_register_entity_service( 111 | SERVICE_DISABLE_PHASE_SWITCHING, 112 | {}, 113 | "async_disable_phase_switching", 114 | ) 115 | 116 | 117 | class AlfenSwitchSensor(AlfenEntity, SwitchEntity): 118 | """Define an Alfen binary sensor.""" 119 | 120 | entity_description: AlfenSwitchDescription 121 | 122 | def __init__( 123 | self, entry: AlfenConfigEntry, description: AlfenSwitchDescription 124 | ) -> None: 125 | """Initialize.""" 126 | super().__init__(entry) 127 | 128 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 129 | self._attr_unique_id = f"{self.coordinator.device.id}_{description.key}" 130 | self.entity_description = description 131 | 132 | @property 133 | def available(self) -> bool: 134 | """Return True if entity is available.""" 135 | return self.entity_description.api_param in self.coordinator.device.properties 136 | 137 | @property 138 | def is_on(self) -> bool: 139 | """Return True if entity is on.""" 140 | if self.entity_description.api_param in self.coordinator.device.properties: 141 | prop = self.coordinator.device.properties[self.entity_description.api_param] 142 | return prop[VALUE] in [1, 3] 143 | 144 | return False 145 | 146 | @property 147 | def extra_state_attributes(self): 148 | """Return the default attributes of the element.""" 149 | if self.entity_description.api_param in self.coordinator.device.properties: 150 | return { 151 | "category": self.coordinator.device.properties[ 152 | self.entity_description.api_param 153 | ][CAT], 154 | } 155 | return None 156 | 157 | async def async_turn_on(self, **kwargs: Any) -> None: 158 | """Turn the light on.""" 159 | # Do the turning on. 160 | if self.entity_description.api_param == "2064_0": 161 | on_value = 3 162 | else: 163 | on_value = 1 164 | 165 | self.coordinator.device.set_value(self.entity_description.api_param, on_value) 166 | await self.coordinator.device.async_update() 167 | 168 | async def async_turn_off(self, **kwargs: Any) -> None: 169 | """Turn the entity off.""" 170 | self.coordinator.device.set_value(self.entity_description.api_param, 0) 171 | await self.coordinator.device.async_update() 172 | 173 | async def async_enable_phase_switching(self): 174 | """Enable phase switching.""" 175 | await self.coordinator.device.set_phase_switching(True) 176 | await self.async_turn_on() 177 | 178 | async def async_disable_phase_switching(self): 179 | """Disable phase switching.""" 180 | await self.coordinator.device.set_phase_switching(False) 181 | await self.async_turn_off() 182 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/text.py: -------------------------------------------------------------------------------- 1 | """Support for Alfen Eve Single Proline Wallbox.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | from typing import Final 7 | 8 | from homeassistant.components.counter import VALUE 9 | from homeassistant.components.text import TextEntity, TextEntityDescription, TextMode 10 | from homeassistant.core import HomeAssistant, callback 11 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 12 | 13 | from .const import CAT, ID 14 | from .coordinator import AlfenConfigEntry 15 | from .entity import AlfenEntity 16 | 17 | 18 | @dataclass 19 | class AlfenTextDescriptionMixin: 20 | """Define an entity description mixin for text entities.""" 21 | 22 | api_param: str 23 | 24 | 25 | @dataclass 26 | class AlfenTextDescription(TextEntityDescription, AlfenTextDescriptionMixin): 27 | """Class to describe an Alfen text entity.""" 28 | 29 | 30 | ALFEN_TEXT_TYPES: Final[tuple[AlfenTextDescription, ...]] = ( 31 | AlfenTextDescription( 32 | key="auth_plug_and_charge_id", 33 | name="Auth. Plug & Charge ID", 34 | icon="mdi:key", 35 | mode=TextMode.TEXT, 36 | api_param="2063_0", 37 | ), 38 | AlfenTextDescription( 39 | key="proxy_address_port", 40 | name="Proxy Address And Port", 41 | icon="mdi:earth", 42 | mode=TextMode.TEXT, 43 | api_param="2115_0", 44 | ), 45 | AlfenTextDescription( 46 | key="proxy_username", 47 | name="Proxy Username", 48 | icon="mdi:account", 49 | mode=TextMode.TEXT, 50 | api_param="2116_0", 51 | ), 52 | AlfenTextDescription( 53 | key="proxy_password", 54 | name="Proxy Password", 55 | icon="mdi:key", 56 | mode=TextMode.PASSWORD, 57 | api_param="2116_1", 58 | ), 59 | AlfenTextDescription( 60 | key="price_other_description", 61 | name="Price other description", 62 | icon="mdi:tag-text-outline", 63 | mode=TextMode.TEXT, 64 | api_param="3262_7", 65 | ), 66 | ) 67 | 68 | 69 | async def async_setup_entry( 70 | hass: HomeAssistant, 71 | entry: AlfenConfigEntry, 72 | async_add_entities: AddEntitiesCallback, 73 | ) -> None: 74 | """Add Alfen Select from a config_entry.""" 75 | 76 | texts = [AlfenText(entry, description) for description in ALFEN_TEXT_TYPES] 77 | 78 | async_add_entities(texts) 79 | 80 | 81 | class AlfenText(AlfenEntity, TextEntity): 82 | """Representation of a Alfen text entity.""" 83 | 84 | entity_description: AlfenTextDescription 85 | 86 | def __init__( 87 | self, entry: AlfenConfigEntry, description: AlfenTextDescription 88 | ) -> None: 89 | """Initialize the Alfen text entity.""" 90 | super().__init__(entry) 91 | 92 | self._attr_name = f"{self.coordinator.device.name} {description.name}" 93 | self._attr_mode = description.mode 94 | self._attr_unique_id = f"{self.coordinator.device.id}_{description.key}" 95 | self.entity_description = description 96 | self._async_update_attrs() 97 | 98 | @callback 99 | def _async_update_attrs(self) -> None: 100 | """Update text attributes.""" 101 | self._attr_native_value = self._get_current_value() 102 | 103 | def _get_current_value(self) -> str | None: 104 | """Return the current value.""" 105 | if self.entity_description.api_param in self.coordinator.device.properties: 106 | return self.coordinator.device.properties[ 107 | self.entity_description.api_param 108 | ][VALUE] 109 | return None 110 | 111 | async def async_set_value(self, value: str) -> None: 112 | """Update the value.""" 113 | self._attr_native_value = value 114 | self.coordinator.device.set_value( 115 | self.entity_description.api_param, value 116 | ) 117 | self.async_write_ha_state() 118 | 119 | @property 120 | def extra_state_attributes(self): 121 | """Return the default attributes of the element.""" 122 | if self.entity_description.api_param in self.coordinator.device.properties: 123 | return { 124 | "category": self.coordinator.device.properties[ 125 | self.entity_description.api_param 126 | ][CAT], 127 | } 128 | return None 129 | -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configure Alfen Wallbox", 6 | "description": "Enter IP address and credentials of your Alfen Wallbox.", 7 | "data": { 8 | "host": "Host", 9 | "name": "Friendly name", 10 | "username": "User name", 11 | "password": "Password" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "device_timeout": "Timeout connecting to the device.", 17 | "device_fail": "Unexpected error creating device.", 18 | "already_configured": "Device is already configured" 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "scan_interval": "Vernieuw interval", 26 | "timeout": "Timeout", 27 | "refresh_categories": "Select the categories to update periodically" 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /custom_components/alfen_wallbox/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configureer Alfen Wallbox", 6 | "description": "Voer het IP address en inloggegevens van de Alfen Wallbox in.", 7 | "data": { 8 | "host": "Host", 9 | "name": "Naam", 10 | "username": "Gebruikersnaam", 11 | "password": "Wachtwoord" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "device_timeout": "Timeout connecting to the device.", 17 | "device_fail": "Unexpected error creating device.", 18 | "already_configured": "Device is already configured.", 19 | "invalid_current_limit": "Invalid current limit (1 - 32A)." 20 | } 21 | }, 22 | "options": { 23 | "step": { 24 | "init": { 25 | "data": { 26 | "scan_interval": "Vernieuw interval", 27 | "timeout": "Timeout", 28 | "refresh_categories": "Selecteer de categoriën om periodiek te updaten" 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /doc/alfen_props.md: -------------------------------------------------------------------------------- 1 | # Alfen API endpoints 2 | 3 | ## General info 4 | All `GET` requests will deliver content with the content type `{'content-type': 'alfen/json; charset=utf-8'}`. 5 | 6 | For `POST` requests you have to use `{'content-type': 'application/json'}`. 7 | 8 | Before each request you have to login first and logout afterwards. The sessions are managed by the wallbox and you don't need to set a session token or something similar, I guess the wallbox uses the IP adress to authenticate the requests. 9 | 10 | The info API doesn't need authentication / a login request. 11 | 12 | The wallbox uses an invalid self signed certificate, you need to disable all SSL checks to perform the API calls. 13 | 14 | ## Login 15 | `HTTP POST https:///api/login` 16 | ``` 17 | { 18 | "username": "admin", 19 | "password": "" 20 | } 21 | ``` 22 | 23 | ## Logout 24 | `HTTP POST https:///api/logout` 25 | 26 | 27 | ## Info 28 | `HTTP GET https:///api/info` 29 | 30 | ## Restart 31 | `HTTP POST https:///api/cmd` 32 | ``` 33 | {"command":"reboot"} 34 | ``` 35 | 36 | ## Log 37 | `HTTP GET https:///api/log?offset=` 38 | 39 | >Default offset (256) 40 | 41 | # Props (POST) 42 | 43 | `HTTP POST https:///api/prop` 44 | 45 | Sample Request 46 | ``` 47 | { 48 | "216C_0": { 49 | "id": "216C_0", 50 | "value": 2 51 | } 52 | } 53 | ``` 54 | 55 | # Props (GET) 56 | `HTTP GET https:///api/prop?ids=` 57 | 58 | Sample Response 59 | ``` 60 | { 61 | "version": 2, 62 | "properties": [ 63 | { 64 | "id": "2060_0", 65 | "access": 1, 66 | "type": 27, 67 | "len": 0, 68 | "cat": "generic", 69 | "value": 6271674 70 | }, 71 | { 72 | "id": "2056_0", 73 | "access": 1, 74 | "type": 7, 75 | "len": 0, 76 | "cat": "generic", 77 | "value": 27 78 | }, 79 | { 80 | "id": "2221_3", 81 | "access": 1, 82 | "type": 8, 83 | "len": 0, 84 | "cat": "meter1", 85 | "value": 222.19999694824219 86 | }, 87 | { 88 | "id": "2221_4", 89 | "access": 1, 90 | "type": 8, 91 | "len": 0, 92 | "cat": "meter1", 93 | "value": 222.29998779296875 94 | }, 95 | { 96 | "id": "2221_5", 97 | "access": 1, 98 | "type": 8, 99 | "len": 0, 100 | "cat": "meter1", 101 | "value": 221.97000122070312 102 | }, 103 | { 104 | "id": "2221_A", 105 | "access": 1, 106 | "type": 8, 107 | "len": 0, 108 | "cat": "meter1", 109 | "value": 4.56500005722046 110 | }, 111 | { 112 | "id": "2221_B", 113 | "access": 1, 114 | "type": 8, 115 | "len": 0, 116 | "cat": "meter1", 117 | "value": 0 118 | }, 119 | { 120 | "id": "2221_C", 121 | "access": 1, 122 | "type": 8, 123 | "len": 0, 124 | "cat": "meter1", 125 | "value": 0 126 | }, 127 | { 128 | "id": "2221_16", 129 | "access": 1, 130 | "type": 8, 131 | "len": 0, 132 | "cat": "meter1", 133 | "value": 981.4000244140625 134 | }, 135 | { 136 | "id": "2201_0", 137 | "access": 1, 138 | "type": 8, 139 | "len": 0, 140 | "cat": "temp", 141 | "value": 42.875 142 | } 143 | ], 144 | "offset": 0, 145 | "total": 10 146 | } 147 | ``` 148 | 149 | ### Alfen prop codes 150 | 151 | | Code | description | unit | 152 | | ----------- | ----------- | --- | 153 | |2060_0| system uptime| /1000 for minutes | 154 | |2056_0| Number of bootups| | 155 | |2221_3| Voltage L1| V | 156 | |2221_4| Voltage L2| V | 157 | |2221_5| Voltage L3| V | 158 | |2221_A| Current L1| A | 159 | |2221_B| Current L2| A | 160 | |2221_C| Current L3| A | 161 | |2221_16| Active power total | /1000 for kW | 162 | |2201_0| Temperature| C | 163 | |2501_2| State | | 164 | 165 | ### Alfen states (2501_2) 166 | |ID|Value| 167 | |----|----| 168 | |4|Charge point available| 169 | |7|Cable connected| 170 | |10|Vehicle connected, start (Charging stopped)| 171 | |11|Normal Charging| 172 | # Firmware 173 | `HTTP GET https:///api/firmware` 174 | 175 | Sample response 176 | ``` 177 | { 178 | "OD_fileFirmwareUpdateStatus": { 179 | "id": "2911_0", 180 | "value": 0 181 | }, 182 | "uploadInProgress": false, 183 | "version": 2 184 | } 185 | ``` 186 | -------------------------------------------------------------------------------- /doc/screenshots/Screen Shot 2022-06-01 at 13.34.44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/Screen Shot 2022-06-01 at 13.34.44.png -------------------------------------------------------------------------------- /doc/screenshots/attribute category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/attribute category.png -------------------------------------------------------------------------------- /doc/screenshots/categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/categories.png -------------------------------------------------------------------------------- /doc/screenshots/configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/configure.png -------------------------------------------------------------------------------- /doc/screenshots/wallbox-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/wallbox-1.png -------------------------------------------------------------------------------- /doc/screenshots/wallbox-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/wallbox-2.png -------------------------------------------------------------------------------- /doc/screenshots/wallbox-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeyuentuen/alfen_wallbox/70808f9b2860d00ea49423f50d24fb6b52824472/doc/screenshots/wallbox-3.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Alfen Wallbox" 3 | } -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Alfen Wallbox (EVSE) - HomeAssistant Integration 2 | 3 | This is a custom component to allow control of Alfen Wallboxes in [HomeAssistant](https://home-assistant.io). 4 | The component is a fork of the [Alfen Wallbox custom integration](https://github.com/egnerfl/alfen_wallbox). 5 | ## Installation 6 | 7 | ### Install using HACS (recommended) 8 | If you do not have HACS installed yet visit https://hacs.xyz for installation instructions. 9 | In HACS go to the Integrations section hit the big + at the bottom right and search for **Alfen Wallbox**. 10 | 11 | ### Install manually 12 | Clone or copy this repository and copy the folder 'custom_components/alfen_wallbox' into '/custom_components/alfen_wallbox' 13 | 14 | ## Configuration 15 | 16 | Once installed the Alfen Wallbox integration can be configured via the Home Assistant integration interface 17 | where you can enter the IP address of the device. 18 | --------------------------------------------------------------------------------