diff --git a/custom_components/alfen_wallbox/alfen.py b/custom_components/alfen_wallbox/alfen.py index 8aa2ffd..96614ac 100644 --- a/custom_components/alfen_wallbox/alfen.py +++ b/custom_components/alfen_wallbox/alfen.py @@ -1,15 +1,11 @@ import logging from aiohttp import ClientSession -import requests -import time import ssl -from enum import Enum from datetime import timedelta -import datetime from homeassistant.util import Throttle -from .const import DOMAIN, ALFEN_PRODUCT_MAP +from .const import CAT, CAT_GENERIC, CAT_GENERIC2, CAT_METER1, CAT_OCPP, CAT_STATES, CAT_TEMP, CMD, DOMAIN, ALFEN_PRODUCT_MAP, ID, METHOD_GET, METHOD_POST, OFFSET, INFO, LOGIN, LOGOUT, PARAM_COMMAND, PARAM_PASSWORD, PARAM_USERNAME, PROP, PROPERTIES, TOTAL, VALUE HEADER_JSON = {"content-type": "alfen/json; charset=utf-8"} POST_HEADER_JSON = {"content-type": "application/json"} @@ -76,10 +72,11 @@ async def async_update(self): async def login(self): response = await self._session.request( ssl=self.ssl, - method="POST", + method=METHOD_POST, headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, + url=self.__get_url(LOGIN), + json={PARAM_USERNAME: self.username, + PARAM_PASSWORD: self.password}, ) _LOGGER.debug(f"Login response {response}") @@ -87,46 +84,65 @@ async def login(self): async def logout(self): response = await self._session.request( ssl=self.ssl, - method="POST", + method=METHOD_POST, headers=HEADER_JSON, - url=self.__get_url("logout"), + url=self.__get_url(LOGOUT), ) _LOGGER.debug(f"Logout response {response}") - async def update_value(self, api_param, value): + async def _update_value(self, api_param, value): response = await self._session.request( ssl=self.ssl, - method="POST", + method=METHOD_POST, headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={api_param: {"id": api_param, "value": value}}, + url=self.__get_url(PROP), + json={api_param: {ID: api_param, VALUE: value}}, ) _LOGGER.info(f"Set {api_param} value {value} response {response}") + return response.status == 200 + + async def _get_value(self, api_param): + response = await self._session.request( + ssl=self.ssl, + method=METHOD_GET, + headers=HEADER_JSON, + url=self.__get_url( + "{}?{}={}".format(PROP, ID, api_param) + ), + ) + _LOGGER.info(f"Status Response {response}") + response_json = await response.json(content_type=None) + if self.properties is None: + self.properties = [] + for resp in response_json[PROPERTIES]: + for prop in self.properties: + if prop[ID] == resp[ID]: + prop[VALUE] = resp[VALUE] + break async def _do_update(self): await self.login() properties = [] - for i in ("generic", "generic2", "meter1", "states", "temp", "ocpp"): + for cat in (CAT_GENERIC, CAT_GENERIC2, CAT_METER1, CAT_STATES, CAT_TEMP, CAT_OCPP): nextRequest = True offset = 0 while (nextRequest): response = await self._session.request( ssl=self.ssl, - method="GET", + method=METHOD_GET, headers=HEADER_JSON, - url=self.__get_url( - "prop?cat={}&offset={}".format(i, offset) - ), + url=self.__get_url("{}?{}={}&{}={}".format( + PROP, CAT, cat, OFFSET, offset)), ) _LOGGER.debug(f"Status Response {response}") response_json = await response.json(content_type=None) if response_json is not None: - properties += response_json["properties"] - nextRequest = response_json["total"] > ( - offset + len(response_json["properties"])) - offset += len(response_json["properties"]) + properties += response_json[PROPERTIES] + nextRequest = response_json[TOTAL] > ( + offset + len(response_json[PROPERTIES])) + offset += len(response_json[PROPERTIES]) await self.logout() @@ -134,7 +150,7 @@ async def _do_update(self): async def async_get_info(self): response = await self._session.request( - ssl=self.ssl, method="GET", url=self.__get_url("info") + ssl=self.ssl, method=METHOD_GET, url=self.__get_url(INFO) ) _LOGGER.debug(f"Response {response}") @@ -156,55 +172,40 @@ async def async_get_info(self): self.info = AlfenDeviceInfo(response_json) async def reboot_wallbox(self): - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) + await self.login() + response = await self._session.request( ssl=self.ssl, - method="POST", + method=METHOD_POST, headers=POST_HEADER_JSON, - url=self.__get_url("cmd"), - json={"command": "reboot"}, + url=self.__get_url(CMD), + json={PARAM_COMMAND: "reboot"}, ) _LOGGER.debug(f"Reboot response {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) + await self.logout() + + async def set_value(self, api_param, value): + await self.login() + success = await self._update_value(api_param, value) + await self.logout() + if success: + # we expect that the value is updated so we are just update the value in the properties + for prop in self.properties: + if prop[ID] == api_param: + _LOGGER.debug(f"Set {api_param} value {value}") + prop[VALUE] = value + break + + async def get_value(self, api_param): + await self.login() + await self._get_value(api_param) + await self.logout() async def set_current_limit(self, limit): _LOGGER.debug(f"Set current limit {limit}A") if limit > 32 | limit < 1: return self.async_abort(reason="invalid_current_limit") - - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"2129_0": {"id": "2129_0", "value": limit}}, - ) - _LOGGER.debug(f"Set current limit response {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("2129_0", limit) async def set_rfid_auth_mode(self, enabled): _LOGGER.debug(f"Set RFID Auth Mode {enabled}") @@ -213,57 +214,15 @@ async def set_rfid_auth_mode(self, enabled): if enabled: value = 2 - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"2126_0": {"id": "2126_0", "value": value}}, - ) - _LOGGER.debug(f"Set RFID Auth Mode {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("2126_0", value) async def set_current_phase(self, phase): _LOGGER.debug(f"Set current phase {phase}") - if phase != "L1" and phase != "L2" and phase != "L3": + if phase not in ('L1', 'L2', 'L3'): return self.async_abort( reason="invalid phase mapping allowed value: L1, L2, L3" ) - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"2069_0": {"id": "2069_0", "value": phase}}, - ) - _LOGGER.debug(f"Set current phase response {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("2069_0", phase) async def set_phase_switching(self, enabled): _LOGGER.debug(f"Set Phase Switching {enabled}") @@ -272,84 +231,19 @@ async def set_phase_switching(self, enabled): if enabled: value = 1 - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"2185_0": {"id": "2185_0", "value": value}}, - ) - _LOGGER.debug(f"Set Phase Switching {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("2185_0", value) async def set_green_share(self, value): _LOGGER.debug(f"Set green share value {value}%") if value < 0 | value > 100: return self.async_abort(reason="invalid_value") - - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"3280_2": {"id": "3280_2", "value": value}}, - ) - _LOGGER.debug(f"Set green share value response {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("3280_2", value) async def set_comfort_power(self, value): _LOGGER.debug(f"Set Comfort Level {value}W") if value < 1400 | value > 5000: return self.async_abort(reason="invalid_value") - - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("login"), - json={"username": self.username, "password": self.password}, - ) - response = await self._session.request( - ssl=self.ssl, - method="POST", - headers=POST_HEADER_JSON, - url=self.__get_url("prop"), - json={"3280_3": {"id": "3280_3", "value": value}}, - ) - _LOGGER.debug(f"Set green share value response {response}") - await self._session.request( - ssl=self.ssl, - method="POST", - headers=HEADER_JSON, - url=self.__get_url("logout"), - ) - await self._do_update() + self.set_value("3280_3", value) def __get_url(self, action): return "https://{}/api/{}".format(self.host, action) diff --git a/custom_components/alfen_wallbox/binary_sensor.py b/custom_components/alfen_wallbox/binary_sensor.py index 6eb0d24..e416d50 100644 --- a/custom_components/alfen_wallbox/binary_sensor.py +++ b/custom_components/alfen_wallbox/binary_sensor.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Final + +from config.custom_components.alfen_wallbox.const import ID, VALUE from .alfen import AlfenDevice from .entity import AlfenEntity @@ -69,14 +71,14 @@ def __init__(self, @property def available(self) -> bool: for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: + if prop[ID] == self.entity_description.api_param: return True return False @property def is_on(self) -> bool: for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: - return prop["value"] == 1 + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] == 1 return False diff --git a/custom_components/alfen_wallbox/config_flow.py b/custom_components/alfen_wallbox/config_flow.py index 2ef6247..6c2095f 100644 --- a/custom_components/alfen_wallbox/config_flow.py +++ b/custom_components/alfen_wallbox/config_flow.py @@ -11,11 +11,12 @@ from .alfen import AlfenDevice -from .const import KEY_IP, TIMEOUT +from .const import DOMAIN, KEY_IP, TIMEOUT _LOGGER = logging.getLogger(__name__) -@config_entries.HANDLERS.register("alfen_wallbox") + +@config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" @@ -60,7 +61,7 @@ async def async_step_user(self, user_input=None): vol.Required(CONF_USERNAME, default="admin"): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_NAME): str - }) + }) ) return await self._create_device(user_input[CONF_HOST], user_input[CONF_NAME], user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) diff --git a/custom_components/alfen_wallbox/const.py b/custom_components/alfen_wallbox/const.py index a5e6717..eaf9266 100644 --- a/custom_components/alfen_wallbox/const.py +++ b/custom_components/alfen_wallbox/const.py @@ -4,6 +4,34 @@ KEY_MAC = "mac" KEY_IP = "ip" +ID = "id" +VALUE = "value" +PROPERTIES = "properties" +CAT = "cat" +OFFSET = "offset" +TOTAL = "total" + +METHOD_POST = "POST" +METHOD_GET = "GET" + +CMD = "cmd" +PROP = "prop" +INFO = "info" +LOGIN = "login" +LOGOUT = "logout" + +PARAM_USERNAME = "username" +PARAM_PASSWORD = "password" +PARAM_COMMAND = "command" + +CAT_GENERIC = "generic" +CAT_GENERIC2 = "generic2" +CAT_METER1 = "meter1" +CAT_STATES = "states" +CAT_TEMP = "temp" +CAT_OCPP = "ocpp" + + TIMEOUT = 60 SERVICE_REBOOT_WALLBOX = "reboot_wallbox" diff --git a/custom_components/alfen_wallbox/entity.py b/custom_components/alfen_wallbox/entity.py index f0c11ef..450687a 100644 --- a/custom_components/alfen_wallbox/entity.py +++ b/custom_components/alfen_wallbox/entity.py @@ -30,9 +30,4 @@ async def async_added_to_hass(self) -> None: async def update_state(self, api_param, value): """Get the state of the entity.""" - - await self._device.login() - - await self._device.update_value(api_param, value) - - await self._device.logout() + await self._device.set_value(api_param, value) diff --git a/custom_components/alfen_wallbox/number.py b/custom_components/alfen_wallbox/number.py index f11d3d2..79ea69d 100644 --- a/custom_components/alfen_wallbox/number.py +++ b/custom_components/alfen_wallbox/number.py @@ -1,3 +1,4 @@ +from config.custom_components.alfen_wallbox.const import ID, VALUE from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberEntityDescription, NumberMode from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,7 +54,7 @@ class AlfenNumberDescription(NumberEntityDescription, AlfenNumberDescriptionMixi icon="mdi:current-ac", assumed_state=False, device_class=NumberDeviceClass.CURRENT, - native_min_value=1, + native_min_value=0, native_max_value=16, native_step=1, mode=NumberMode.BOX, @@ -67,7 +68,7 @@ class AlfenNumberDescription(NumberEntityDescription, AlfenNumberDescriptionMixi icon="mdi:current-ac", assumed_state=False, device_class=NumberDeviceClass.CURRENT, - native_min_value=1, + native_min_value=0, native_max_value=16, native_step=1, mode=NumberMode.BOX, @@ -81,7 +82,7 @@ class AlfenNumberDescription(NumberEntityDescription, AlfenNumberDescriptionMixi icon="mdi:current-ac", assumed_state=False, device_class=NumberDeviceClass.CURRENT, - native_min_value=1, + native_min_value=0, native_max_value=16, native_step=1, mode=NumberMode.BOX, @@ -200,8 +201,8 @@ async def async_set_native_value(self, value: float) -> None: def native_value(self) -> float | None: """Return the entity value to represent the entity state.""" for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: - return prop["value"] + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] return None async def async_set_native_value(self, value: float) -> None: @@ -209,6 +210,13 @@ async def async_set_native_value(self, value: float) -> None: await self.update_state(self.entity_description.api_param, value) self.async_write_ha_state() + def _get_current_option(self) -> str | None: + """Return the current option.""" + for prop in self._device.properties: + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] + return None + @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" diff --git a/custom_components/alfen_wallbox/select.py b/custom_components/alfen_wallbox/select.py index 4a26220..e4db26a 100644 --- a/custom_components/alfen_wallbox/select.py +++ b/custom_components/alfen_wallbox/select.py @@ -2,6 +2,8 @@ from typing import Final, Any from dataclasses import dataclass + +from config.custom_components.alfen_wallbox.const import ID, VALUE from .entity import AlfenEntity from homeassistant.config_entries import ConfigEntry @@ -259,8 +261,8 @@ def current_option(self) -> str | None: def _get_current_option(self) -> str | None: """Return the current option.""" for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: - return prop["value"] + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] return None async def async_update(self): diff --git a/custom_components/alfen_wallbox/sensor.py b/custom_components/alfen_wallbox/sensor.py index cf2a4a8..c1e557e 100644 --- a/custom_components/alfen_wallbox/sensor.py +++ b/custom_components/alfen_wallbox/sensor.py @@ -28,6 +28,7 @@ from .alfen import AlfenDevice from .const import ( + ID, SERVICE_REBOOT_WALLBOX, SERVICE_SET_CURRENT_LIMIT, SERVICE_ENABLE_RFID_AUTHORIZATION_MODE, @@ -37,6 +38,7 @@ SERVICE_DISABLE_PHASE_SWITCHING, SERVICE_SET_GREEN_SHARE, SERVICE_SET_COMFORT_POWER, + VALUE, ) _LOGGER = logging.getLogger(__name__) @@ -632,7 +634,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_GREEN_SHARE, { - vol.Required("value"): cv.positive_int, + vol.Required(VALUE): cv.positive_int, }, "async_set_green_share", ) @@ -640,7 +642,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_COMFORT_POWER, { - vol.Required("value"): cv.positive_int, + vol.Required(VALUE): cv.positive_int, }, "async_set_comfort_power", ) @@ -669,16 +671,16 @@ def icon(self): def state(self): """Return the state of the sensor.""" for prop in self._device.properties: - if prop['id'] == self.entity_description.api_param: + if prop[ID] == self.entity_description.api_param: # exception # status - if (prop['id'] == "2501_2"): - return STATUS_DICT.get(prop['value'], 'Unknown') + if (prop[ID] == "2501_2"): + return STATUS_DICT.get(prop[VALUE], 'Unknown') if self.entity_description.round_digits is not None: - return round(prop['value'], self.entity_description.round_digits) + return round(prop[VALUE], self.entity_description.round_digits) - return prop['value'] + return prop[VALUE] return 'Unknown' @@ -758,8 +760,8 @@ def __init__(self, def _get_current_value(self): """Get the current value.""" for prop in self._device.properties: - if prop['id'] == self.entity_description.api_param: - return prop['value'] + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] return None @callback @@ -796,33 +798,33 @@ def native_unit_of_measurement(self): def state(self): """Return the state of the sensor.""" for prop in self._device.properties: - if prop['id'] == self.entity_description.api_param: + if prop[ID] == self.entity_description.api_param: # some exception of return value # meter_reading from w to kWh if self.entity_description.api_param == "2221_22": - return round((prop["value"] / 1000), 2) + return round((prop[VALUE] / 1000), 2) # Car PWM Duty cycle % if self.entity_description.api_param == "2511_3": - return round((prop["value"] / 100), self.entity_description.round_digits) + return round((prop[VALUE] / 100), self.entity_description.round_digits) # change milliseconds to HH:MM:SS if self.entity_description.api_param == "2060_0": - return str(datetime.timedelta(milliseconds=prop['value'])).split('.', maxsplit=1)[0] + return str(datetime.timedelta(milliseconds=prop[VALUE])).split('.', maxsplit=1)[0] # change milliseconds to d/m/y HH:MM:SS if self.entity_description.api_param == "2187_0" or self.entity_description.api_param == "2059_0": - return datetime.datetime.fromtimestamp(prop['value'] / 1000).strftime("%d/%m/%Y %H:%M:%S") + return datetime.datetime.fromtimestamp(prop[VALUE] / 1000).strftime("%d/%m/%Y %H:%M:%S") # Allowed phase 1 or Allowed Phase 2 if (self.entity_description.api_param == "312E_0") | (self.entity_description.api_param == "312F_0"): - return ALLOWED_PHASE_DICT.get(prop['value'], 'Unknown') + return ALLOWED_PHASE_DICT.get(prop[VALUE], 'Unknown') if self.entity_description.round_digits is not None: - return round(prop['value'], self.entity_description.round_digits) + return round(prop[VALUE], self.entity_description.round_digits) - return prop['value'] + return prop[VALUE] @property def unit_of_measurement(self): diff --git a/custom_components/alfen_wallbox/switch.py b/custom_components/alfen_wallbox/switch.py index 32033a2..abf630a 100644 --- a/custom_components/alfen_wallbox/switch.py +++ b/custom_components/alfen_wallbox/switch.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Any, Final + +from config.custom_components.alfen_wallbox.const import ID, VALUE from .alfen import AlfenDevice from .entity import AlfenEntity @@ -57,8 +59,6 @@ async def async_setup_entry( class AlfenSwitchSensor(AlfenEntity, SwitchEntity): """Define an Alfen binary sensor.""" - # entity_description: AlfenSwitchDescriptionMixin - def __init__(self, device: AlfenDevice, description: AlfenSwitchDescription @@ -73,15 +73,15 @@ def __init__(self, @property def available(self) -> bool: for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: + if prop[ID] == self.entity_description.api_param: return True return False @property def is_on(self) -> bool: for prop in self._device.properties: - if prop["id"] == self.entity_description.api_param: - return prop["value"] == 1 + if prop[ID] == self.entity_description.api_param: + return prop[VALUE] == 1 return False