diff --git a/config/custom_components/peblar/README.MD b/config/custom_components/peblar/README.MD new file mode 100644 index 00000000000000..862d50bfb7597f --- /dev/null +++ b/config/custom_components/peblar/README.MD @@ -0,0 +1,100 @@ +# Peblar Home Assistant Integration + +The Peblar Home Assistant Integration provides seamless integration with Peblar devices, enabling monitoring and control directly from Home Assistant. This integration supports configuring connection details, authenticating with the Peblar API, and setting up sensors and number entities. + +--- + +## Features + +- **Connection Management:** Configure the IP address and access token for your Peblar device. +- **Sensors:** Monitor key metrics like charging current, total energy, session energy, and charge power. +- **Number Entities:** Control parameters such as maximum charging current. +- **Reauthentication Support:** Easily reauthenticate in case of API authentication errors. + +--- + +## Installation + +### Installation via HACS + +1. Ensure that [HACS (Home Assistant Community Store)](https://hacs.xyz/) is installed in your Home Assistant setup. +2. Go to **HACS > Integrations**. +3. Click the three dots in the top-right corner and select **Custom Repositories**. +4. Add the URL of this repository and select `Integration` as the category. +5. Search for `Peblar` in HACS and click **Download**. + +### Manual Installation + +1. Download or clone the integration files. +2. Place the files in the `custom_components/peblar` directory within your Home Assistant configuration folder. + +### Step 2: Restart Home Assistant + +Restart Home Assistant to load the new integration. + +### Step 3: Add the Integration + +1. Navigate to **Settings > Devices & Services** in Home Assistant. +2. Click **Add Integration** and search for `Peblar`. +3. Follow the prompts to configure the integration. +--- + +## Configuration + +When setting up the Peblar integration, you will need: + +- **IP Address:** The IP address of your Peblar device. +- **Access Token:** The access token for authenticating with the Peblar API. + + +## Supported Entities + +### Sensors + +| Sensor | Unit | Device Class | State Class | +|----------------------------|--------------------|--------------|------------------| +| Charger Max Charging Current | mA | Current | Measurement | +| Charger Total Energy | Wh | Energy | Measurement | +| Charger Session Energy | Wh | Energy | Measurement | +| Charger Charge Power | W | Power | Measurement | + +### Number Entities + +| Entity | Min Value | Max Value | Step | Description | +|----------------------------|-----------|-----------|------|------------------------------| +| Charger Max Charging Current | 0 | 20000 | 1 | Set the maximum charging current | + +--- + +## Error Handling + +### Common Errors + +- **`cannot_connect`**: Unable to connect to the Peblar device. Check the IP address and ensure the device is online. +- **`invalid_auth`**: Authentication failed. Verify your access token. +- **`reauth_invalid`**: Reauthentication failed. Ensure the IP address and access token are correct. + +### Reauthentication + +If reauthentication is required: + +1. Open the Peblar integration settings in Home Assistant. +2. Update the IP address and/or access token. +3. Save the changes to reauthenticate. + +--- + +### Contributions + +Contributions are welcome! Feel free to open issues or submit pull requests. + +--- + +## License + +This project is licensed under the MIT License. See the `LICENSE` file for details. + +--- + +For more information or support, please refer to the [Home Assistant documentation](https://www.home-assistant.io) or contact the integration maintainer. + diff --git a/config/custom_components/peblar/__init__.py b/config/custom_components/peblar/__init__.py new file mode 100644 index 00000000000000..cd9e32b8285fcc --- /dev/null +++ b/config/custom_components/peblar/__init__.py @@ -0,0 +1,48 @@ +"""The Peblar integration.""" + +from __future__ import annotations + +from .peblar import Peblar + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_ACCESS_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed + +from .const import DOMAIN, UPDATE_INTERVAL +from .coordinator import InvalidAuth, PeblarCoordinator, async_validate_input + +PLATFORMS = [Platform.NUMBER, Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Peblar from a config entry.""" + peblar = Peblar( + entry.data[CONF_ACCESS_TOKEN], + entry.data[CONF_IP_ADDRESS], + ) + try: + await async_validate_input(hass, peblar) + except InvalidAuth as ex: + raise ConfigEntryAuthFailed from ex + + peblar_coordinator = PeblarCoordinator( + peblar, + hass, + ) + await peblar_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = peblar_coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/config/custom_components/peblar/__pycache__/__init__.cpython-313.pyc b/config/custom_components/peblar/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000000000..150b87cb3b8f65 Binary files /dev/null and b/config/custom_components/peblar/__pycache__/__init__.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/config_flow.cpython-313.pyc b/config/custom_components/peblar/__pycache__/config_flow.cpython-313.pyc new file mode 100644 index 00000000000000..da7ee4aee76ee6 Binary files /dev/null and b/config/custom_components/peblar/__pycache__/config_flow.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/connectric.cpython-313.pyc b/config/custom_components/peblar/__pycache__/connectric.cpython-313.pyc new file mode 100644 index 00000000000000..c27908dc0e27f9 Binary files /dev/null and b/config/custom_components/peblar/__pycache__/connectric.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/const.cpython-313.pyc b/config/custom_components/peblar/__pycache__/const.cpython-313.pyc new file mode 100644 index 00000000000000..cd26a917c90f75 Binary files /dev/null and b/config/custom_components/peblar/__pycache__/const.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/coordinator.cpython-313.pyc b/config/custom_components/peblar/__pycache__/coordinator.cpython-313.pyc new file mode 100644 index 00000000000000..872c4eb59b39ca Binary files /dev/null and b/config/custom_components/peblar/__pycache__/coordinator.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/entity.cpython-313.pyc b/config/custom_components/peblar/__pycache__/entity.cpython-313.pyc new file mode 100644 index 00000000000000..20d6e3079ebd20 Binary files /dev/null and b/config/custom_components/peblar/__pycache__/entity.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/number.cpython-313.pyc b/config/custom_components/peblar/__pycache__/number.cpython-313.pyc new file mode 100644 index 00000000000000..1bd14be2ef777a Binary files /dev/null and b/config/custom_components/peblar/__pycache__/number.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/peblar.cpython-313.pyc b/config/custom_components/peblar/__pycache__/peblar.cpython-313.pyc new file mode 100644 index 00000000000000..8dfe7a03eb05cf Binary files /dev/null and b/config/custom_components/peblar/__pycache__/peblar.cpython-313.pyc differ diff --git a/config/custom_components/peblar/__pycache__/sensor.cpython-313.pyc b/config/custom_components/peblar/__pycache__/sensor.cpython-313.pyc new file mode 100644 index 00000000000000..929007aba9628f Binary files /dev/null and b/config/custom_components/peblar/__pycache__/sensor.cpython-313.pyc differ diff --git a/config/custom_components/peblar/config_flow.py b/config/custom_components/peblar/config_flow.py new file mode 100644 index 00000000000000..21259c29fbc35a --- /dev/null +++ b/config/custom_components/peblar/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Peblar integration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol +from .peblar import Peblar + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import InvalidAuth, async_validate_input + +COMPONENT_DOMAIN = DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_ACCESS_TOKEN): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: + """Validate the user input allows to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + peblar = Peblar(data["access_token"], data["ip_address"]) + + await async_validate_input(hass, peblar) + + # Return info that you want to store in the config entry. + return {"title": "peblar"} + + +class peblarConfigFlow(ConfigFlow, domain=COMPONENT_DOMAIN): + """Handle a config flow for peblar.""" + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + ) + + errors = {} + + try: + await self.async_set_unique_id(user_input["ip_address"]) + if self.source != SOURCE_REAUTH: + self._abort_if_unique_id_configured() + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], data=user_input) + reauth_entry = self._get_reauth_entry() + if user_input["ip_address"] == reauth_entry.data["ip_address"]: + return self.async_update_reload_and_abort(reauth_entry, data=user_input) + errors["base"] = "reauth_invalid" + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) diff --git a/config/custom_components/peblar/const.py b/config/custom_components/peblar/const.py new file mode 100644 index 00000000000000..5c5a1c095c06a2 --- /dev/null +++ b/config/custom_components/peblar/const.py @@ -0,0 +1,45 @@ +"""Constants for the Eneco peblar integration.""" + +DOMAIN = "peblar" +UPDATE_INTERVAL = 30 + + +CHARGER_CURRENT_VERSION_KEY = "FirmwareVersion" +CHARGER_PART_NUMBER_KEY = "ProductPn" +CHARGER_SERIAL_NUMBER_KEY = "ProductSn" +CHARGER_SOFTWARE_KEY = "FirmwareVersion" +CHARGER_MAX_CHARGING_CURRENT_KEY = "ChargeCurrentLimit" +CHARGER_CHARGING_CURRENT_ACTUAL_KEY = "ChargeCurrentLimitActual" +CHARGER_TOTAL_ENERGY_KEY = "EnergyTotal" +CHARGER_SESSION_ENERGY_KEY = "EnergySession" +CHARGER_CHARGE_POWER_KEY = "PowerTotal" + +# CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" +# CHARGER_ADDED_ENERGY_KEY = "added_energy" +# CHARGER_ADDED_RANGE_KEY = "added_range" +# CHARGER_CHARGING_POWER_KEY = "charging_power" +# CHARGER_CHARGING_SPEED_KEY = "charging_speed" +# CHARGER_CHARGING_TIME_KEY = "charging_time" +# CHARGER_COST_KEY = "cost" +# CHARGER_CURRENT_MODE_KEY = "current_mode" +# CHARGER_CURRENT_VERSION_KEY = "currentVersion" +# CHARGER_CURRENCY_KEY = "currency" +# CHARGER_DATA_KEY = "config_data" +# CHARGER_DEPOT_PRICE_KEY = "depot_price" +# CHARGER_ENERGY_PRICE_KEY = "energy_price" +# CHARGER_FEATURES_KEY = "features" +# CHARGER_SERIAL_NUMBER_KEY = "serial_number" +# CHARGER_PART_NUMBER_KEY = "part_number" +# CHARGER_PLAN_KEY = "plan" +# CHARGER_POWER_BOOST_KEY = "POWER_BOOST" +# CHARGER_SOFTWARE_KEY = "software" +# CHARGER_MAX_AVAILABLE_POWER_KEY = "max_available_power" +# CHARGER_MAX_CHARGING_CURRENT_KEY = "max_charging_current" +# CHARGER_MAX_ICP_CURRENT_KEY = "icp_max_current" +# CHARGER_PAUSE_RESUME_KEY = "paused" +# CHARGER_LOCKED_UNLOCKED_KEY = "locked" +# CHARGER_NAME_KEY = "name" +# CHARGER_STATE_OF_CHARGE_KEY = "state_of_charge" +# CHARGER_STATUS_ID_KEY = "status_id" +# CHARGER_STATUS_DESCRIPTION_KEY = "status_description" +# CHARGER_CONNECTIONS = "connections" diff --git a/config/custom_components/peblar/coordinator.py b/config/custom_components/peblar/coordinator.py new file mode 100644 index 00000000000000..92a7e166142f48 --- /dev/null +++ b/config/custom_components/peblar/coordinator.py @@ -0,0 +1,82 @@ +"""DataUpdateCoordinator for the peblar integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +import requests +from .peblar import Peblar + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CHARGER_MAX_CHARGING_CURRENT_KEY, DOMAIN, UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +def _validate(peblar: Peblar) -> None: + """Authenticate using Peblar API.""" + try: + peblar.authenticate() + except requests.exceptions.HTTPError as peblar_connection_error: + if peblar_connection_error.response.status_code == 401: + raise InvalidAuth from peblar_connection_error + raise ConnectionError from peblar_connection_error + + +async def async_validate_input(hass: HomeAssistant, peblar: Peblar) -> None: + """Get new sensor data for Peblar component.""" + await hass.async_add_executor_job(_validate, peblar) + + +class PeblarCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Peblar Coordinator class.""" + + def __init__(self, peblar: Peblar, hass: HomeAssistant) -> None: + """Initialize.""" + self._peblar = peblar + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + def authenticate(self) -> None: + """Authenticate using Peblar API.""" + self._peblar.authenticate() + + def _get_data(self) -> dict[str, Any]: + """Get new sensor data for Peblar component.""" + data: dict[str, Any] = self._peblar.getChargerData() + data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_MAX_CHARGING_CURRENT_KEY] + return data + + async def _async_update_data(self) -> dict[str, Any]: + """Get new sensor data for Peblar component.""" + return await self.hass.async_add_executor_job(self._get_data) + + def _set_charging_current(self, charging_current: float) -> None: + """Set maximum charging current for Peblar.""" + try: + self._peblar.setMaxChargingCurrent(charging_current) + except requests.exceptions.HTTPError as peblar_connection_error: + if peblar_connection_error.response.status_code == 403: + raise InvalidAuth from peblar_connection_error + raise + + async def async_set_charging_current(self, charging_current: float) -> None: + """Set maximum charging current for Peblar.""" + await self.hass.async_add_executor_job( + self._set_charging_current, charging_current + ) + await self.async_request_refresh() + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/config/custom_components/peblar/entity.py b/config/custom_components/peblar/entity.py new file mode 100644 index 00000000000000..594b6b2713fe9d --- /dev/null +++ b/config/custom_components/peblar/entity.py @@ -0,0 +1,36 @@ +"""Base entity for the peblar integration.""" + +from __future__ import annotations + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + CHARGER_CURRENT_VERSION_KEY, + CHARGER_PART_NUMBER_KEY, + CHARGER_SERIAL_NUMBER_KEY, + CHARGER_SOFTWARE_KEY, + DOMAIN, +) +from .coordinator import PeblarCoordinator + + +class PeblarEntity(CoordinatorEntity[PeblarCoordinator]): + """Defines a base Peblar entity.""" + + _attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this Peblar device.""" + return DeviceInfo( + identifiers={ + ( + DOMAIN, + self.coordinator.data[CHARGER_SERIAL_NUMBER_KEY], + ) + }, + name=f"Peblar", + manufacturer="Peblar", + model_id=self.coordinator.data[CHARGER_PART_NUMBER_KEY], + ) diff --git a/config/custom_components/peblar/icons.json b/config/custom_components/peblar/icons.json new file mode 100644 index 00000000000000..359e05cb44122a --- /dev/null +++ b/config/custom_components/peblar/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "charging_speed": { + "default": "mdi:speedometer" + }, + "added_range": { + "default": "mdi:map-marker-distance" + }, + "cost": { + "default": "mdi:ev-station" + }, + "current_mode": { + "default": "mdi:ev-station" + }, + "depot_price": { + "default": "mdi:ev-station" + }, + "energy_price": { + "default": "mdi:ev-station" + }, + "status_description": { + "default": "mdi:ev-station" + } + } + } +} diff --git a/config/custom_components/peblar/manifest.json b/config/custom_components/peblar/manifest.json new file mode 100644 index 00000000000000..f278cbc2949d13 --- /dev/null +++ b/config/custom_components/peblar/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "peblar", + "name": "Peblar", + "codeowners": ["@thimo1996"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/Eneco_Peblar", + "iot_class": "cloud_polling", + "requirements": [], + "version": "0.1.0" +} diff --git a/config/custom_components/peblar/number.py b/config/custom_components/peblar/number.py new file mode 100644 index 00000000000000..b630fe0741f1f6 --- /dev/null +++ b/config/custom_components/peblar/number.py @@ -0,0 +1,106 @@ +"""Home Assistant component for accessing the Peblar Portal API. + +The number component allows control of charging current. +""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import cast + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_SERIAL_NUMBER_KEY, + DOMAIN, +) +from .coordinator import InvalidAuth, PeblarCoordinator +from .entity import PeblarEntity + + +@dataclass(frozen=True, kw_only=True) +class PeblarNumberEntityDescription(NumberEntityDescription): + """Describes Peblar number entity.""" + + max_value_fn: Callable[[PeblarCoordinator], float] + min_value_fn: Callable[[PeblarCoordinator], float] + set_value_fn: Callable[[PeblarCoordinator], Callable[[float], Awaitable[None]]] + + +NUMBER_TYPES: dict[str, PeblarNumberEntityDescription] = { + CHARGER_MAX_CHARGING_CURRENT_KEY: PeblarNumberEntityDescription( + key=CHARGER_MAX_CHARGING_CURRENT_KEY, + translation_key=CHARGER_MAX_CHARGING_CURRENT_KEY, + max_value_fn=lambda _: 20000.0, + min_value_fn=lambda _: 0.0, + set_value_fn=lambda coordinator: coordinator.async_set_charging_current, + native_step=1, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Create Peblar number entities in HASS.""" + coordinator: PeblarCoordinator = hass.data[DOMAIN][entry.entry_id] + # Check if the user has sufficient rights to change values, if so, add number component: + try: + await coordinator.async_set_charging_current( + coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] + ) + except InvalidAuth: + return + except ConnectionError as exc: + raise PlatformNotReady from exc + + async_add_entities( + PeblarNumber(coordinator, entry, description) + for ent in coordinator.data + if (description := NUMBER_TYPES.get(ent)) + ) + + +class PeblarNumber(PeblarEntity, NumberEntity): + """Representation of the Peblar.""" + + entity_description: PeblarNumberEntityDescription + + def __init__( + self, + coordinator: PeblarCoordinator, + entry: ConfigEntry, + description: PeblarNumberEntityDescription, + ) -> None: + """Initialize a Peblar number entity.""" + super().__init__(coordinator) + self.entity_description = description + self._coordinator = coordinator + self._attr_unique_id = ( + f"{description.key}-{coordinator.data[CHARGER_SERIAL_NUMBER_KEY]}" + ) + + @property + def native_max_value(self) -> float: + """Return the maximum available value.""" + return self.entity_description.max_value_fn(self.coordinator) + + @property + def native_min_value(self) -> float: + """Return the minimum available value.""" + return self.entity_description.min_value_fn(self.coordinator) + + @property + def native_value(self) -> float | None: + """Return the value of the entity.""" + return cast(float | None, self._coordinator.data[self.entity_description.key]) + + async def async_set_native_value(self, value: float) -> None: + """Set the value of the entity.""" + await self.entity_description.set_value_fn(self.coordinator)(value) diff --git a/config/custom_components/peblar/peblar.py b/config/custom_components/peblar/peblar.py new file mode 100644 index 00000000000000..5557d1b99280be --- /dev/null +++ b/config/custom_components/peblar/peblar.py @@ -0,0 +1,81 @@ +""" + +Peblar class + +""" + +import requests +import json + + +class Peblar: + def __init__(self, token, address, requestGetTimeout=None): + self.token = token + self.address = address + self._requestGetTimeout = requestGetTimeout + self.baseUrl = "http://" + self.address + "/api/wlac/v1/" + self.headers = { + "Content-type": "application/json", + "Authorization": f"{self.token}", + } + + @property + def requestGetTimeout(self): + return self._requestGetTimeout + + def authenticate(self): + try: + response = requests.get( + f"{self.baseUrl}system", + headers=self.headers, + timeout=self._requestGetTimeout, + ) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + raise (err) + + def getChargerData(self): + try: + response = requests.get( + f"{self.baseUrl}system", + headers=self.headers, + timeout=self._requestGetTimeout, + ) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + raise (err) + result1 = json.loads(response.text) + try: + response = requests.get( + f"{self.baseUrl}meter", + headers=self.headers, + timeout=self._requestGetTimeout, + ) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + raise (err) + result2 = json.loads(response.text) + try: + response = requests.get( + f"{self.baseUrl}evinterface", + headers=self.headers, + timeout=self._requestGetTimeout, + ) + response.raise_for_status() + except requests.exceptions.HTTPError as err: + raise (err) + result3 = json.loads(response.text) + return result1 | result2 | result3 + + def setMaxChargingCurrent(self, newMaxChargingCurrentValue): + try: + response = requests.patch( + f"{self.baseUrl}evinterface", + headers=self.headers, + data=f'{{ "ChargeCurrentLimit": {newMaxChargingCurrentValue}}}', + timeout=self._requestGetTimeout, + ) + pass + except requests.exceptions.HTTPError as err: + raise (err) + return json.loads(response.text) diff --git a/config/custom_components/peblar/sensor.py b/config/custom_components/peblar/sensor.py new file mode 100644 index 00000000000000..e7c445a3d7a72b --- /dev/null +++ b/config/custom_components/peblar/sensor.py @@ -0,0 +1,124 @@ +"""Home Assistant component for accessing the Peblar Portal API. The sensor component creates multiple sensors regarding peblar performance.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import ( + CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_SERIAL_NUMBER_KEY, + CHARGER_TOTAL_ENERGY_KEY, + CHARGER_SESSION_ENERGY_KEY, + CHARGER_CHARGE_POWER_KEY, + DOMAIN, +) +from .coordinator import PeblarCoordinator +from .entity import PeblarEntity + +UPDATE_INTERVAL = 30 + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class PeblarSensorEntityDescription(SensorEntityDescription): + """Describes Peblar sensor entity.""" + + precision: int | None = None + + +SENSOR_TYPES: dict[str, PeblarSensorEntityDescription] = { + CHARGER_MAX_CHARGING_CURRENT_KEY: PeblarSensorEntityDescription( + key=CHARGER_MAX_CHARGING_CURRENT_KEY, + translation_key=CHARGER_MAX_CHARGING_CURRENT_KEY, + native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + CHARGER_TOTAL_ENERGY_KEY: PeblarSensorEntityDescription( + key=CHARGER_TOTAL_ENERGY_KEY, + translation_key=CHARGER_TOTAL_ENERGY_KEY, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, + ), + CHARGER_SESSION_ENERGY_KEY: PeblarSensorEntityDescription( + key=CHARGER_SESSION_ENERGY_KEY, + translation_key=CHARGER_SESSION_ENERGY_KEY, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.MEASUREMENT, + ), + CHARGER_CHARGE_POWER_KEY: PeblarSensorEntityDescription( + key=CHARGER_CHARGE_POWER_KEY, + translation_key=CHARGER_CHARGE_POWER_KEY, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Create Peblar sensor entities in HASS.""" + coordinator: PeblarCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + PeblarSensor(coordinator, description) + for ent in coordinator.data + if (description := SENSOR_TYPES.get(ent)) + ) + + +class PeblarSensor(PeblarEntity, SensorEntity): + """Representation of the Peblar portal.""" + + entity_description: PeblarSensorEntityDescription + + def __init__( + self, + coordinator: PeblarCoordinator, + description: PeblarSensorEntityDescription, + ) -> None: + """Initialize a Peblar sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{description.key}-{coordinator.data[CHARGER_SERIAL_NUMBER_KEY]}" + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor. Round the value when it, and the precision property are not None.""" + if ( + sensor_round := self.entity_description.precision + ) is not None and self.coordinator.data[ + self.entity_description.key + ] is not None: + return cast( + StateType, + round(self.coordinator.data[self.entity_description.key], sensor_round), + ) + return cast(StateType, self.coordinator.data[self.entity_description.key]) diff --git a/config/custom_components/peblar/strings.json b/config/custom_components/peblar/strings.json new file mode 100644 index 00000000000000..f4378b328d8cca --- /dev/null +++ b/config/custom_components/peblar/strings.json @@ -0,0 +1,96 @@ +{ + "config": { + "step": { + "user": { + "data": { + "station": "Station Serial Number", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_invalid": "Re-authentication failed; Serial Number does not match original" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "entity": { + "lock": { + "lock": { + "name": "[%key:component::lock::title%]" + } + }, + "number": { + "maximum_charging_current": { + "name": "Maximum charging current" + }, + "energy_price": { + "name": "Energy price" + }, + "maximum_icp_current": { + "name": "Maximum ICP current" + } + }, + "sensor": { + "charging_power": { + "name": "Charging power" + }, + "max_available_power": { + "name": "Max available power" + }, + "charging_speed": { + "name": "Charging speed" + }, + "added_range": { + "name": "Added range" + }, + "added_energy": { + "name": "Added energy" + }, + "added_discharged_energy": { + "name": "Discharged energy" + }, + "cost": { + "name": "Cost" + }, + "state_of_charge": { + "name": "State of charge" + }, + "current_mode": { + "name": "Current mode" + }, + "depot_price": { + "name": "Depot price" + }, + "energy_price": { + "name": "Energy price" + }, + "status_description": { + "name": "Status description" + }, + "max_charging_current": { + "name": "Max charging current" + }, + "icp_max_current": { + "name": "Max ICP current" + } + }, + "switch": { + "pause_resume": { + "name": "Pause/resume" + } + } + } +} diff --git a/config/custom_components/peblar/translations/en.json b/config/custom_components/peblar/translations/en.json new file mode 100644 index 00000000000000..214d39117fa9d6 --- /dev/null +++ b/config/custom_components/peblar/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "reauth_invalid": "Re-authentication failed; Token not valid", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "ip_address": "ip address", + "access_token": "access token" + } + } + } + }, + "entity": { + "number": { + "ChargeCurrentLimit": { + "name": "Maximum charging current" + } + }, + "sensor": { + "EnergyTotal": { + "name": "Total energy" + }, + "EnergySession": { + "name": "Session energy" + }, + "PowerTotal": { + "name": "Charging power" + } + } + } +} \ No newline at end of file