├── .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 | [](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 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------