From 186136a9a1e376dd4276b3151bed6cafa986d6eb Mon Sep 17 00:00:00 2001 From: bullitt168 Date: Sun, 30 Jun 2024 20:02:17 +0000 Subject: [PATCH 1/2] added support for multiple wallboxes --- custom_components/e3dc_rscp/binary_sensor.py | 149 +++++++------ custom_components/e3dc_rscp/button.py | 37 +-- custom_components/e3dc_rscp/const.py | 1 + custom_components/e3dc_rscp/coordinator.py | 108 +++++---- custom_components/e3dc_rscp/e3dc_proxy.py | 4 +- custom_components/e3dc_rscp/sensor.py | 210 ++++++++++-------- custom_components/e3dc_rscp/strings.json | 54 ++++- custom_components/e3dc_rscp/switch.py | 72 +++--- .../e3dc_rscp/translations/en.json | 44 ++-- 9 files changed, 399 insertions(+), 280 deletions(-) diff --git a/custom_components/e3dc_rscp/binary_sensor.py b/custom_components/e3dc_rscp/binary_sensor.py index 4fac741..98d33f4 100644 --- a/custom_components/e3dc_rscp/binary_sensor.py +++ b/custom_components/e3dc_rscp/binary_sensor.py @@ -55,67 +55,6 @@ class E3DCBinarySensorEntityDescription(BinarySensorEntityDescription): on_icon="mdi:electric-switch-closed", off_icon="mdi:electric-switch", ), - E3DCBinarySensorEntityDescription( - key="wallbox-sun-mode", - translation_key="wallbox-sun-mode", - on_icon="mdi:weather-sunny", - off_icon="mdi:weather-sunny-off", - device_class=None, - ), - E3DCBinarySensorEntityDescription( - key="wallbox-plug-lock", - translation_key="wallbox-plug-lock", - on_icon="mdi:lock-open", - off_icon="mdi:lock", - device_class=BinarySensorDeviceClass.LOCK, - entity_registry_enabled_default=False, # Disabled per default as only Wallbox easy connect provides this state - - ), - E3DCBinarySensorEntityDescription( - key="wallbox-plug", - translation_key="wallbox-plug", - on_icon="mdi:power-plug", - off_icon="mdi:power-plug-off", - device_class=BinarySensorDeviceClass.PLUG, - ), - E3DCBinarySensorEntityDescription( - key="wallbox-schuko", - translation_key="wallbox-schuko", - on_icon="mdi:power-plug-outline", - off_icon="mdi:power-plug-off-outline", - device_class=BinarySensorDeviceClass.POWER, - entity_registry_enabled_default=False, # Disabled per default as only Wallbox multi connect I provides this feature - ), - E3DCBinarySensorEntityDescription( - key="wallbox-charging", - translation_key="wallbox-charging", - on_icon="mdi:car-electric", - off_icon="mdi:car-electric-outline", - device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - ), - E3DCBinarySensorEntityDescription( - key="wallbox-charging-canceled", - translation_key="wallbox-charging-canceled", - on_icon="mdi:cancel", - off_icon="mdi:check-circle-outline", - device_class=None, - ), - E3DCBinarySensorEntityDescription( - key="wallbox-battery-to-car", - translation_key="wallbox-battery-to-car", - on_icon="mdi:battery-charging", - off_icon="mdi:battery-off", - device_class=None, - entity_registry_enabled_default=False, - ), - E3DCBinarySensorEntityDescription( - key="wallbox-key-state", - translation_key="wallbox-key-state", - on_icon="mdi:key-variant", - off_icon="mdi:key-remove", - device_class=BinarySensorDeviceClass.LOCK, - entity_registry_enabled_default=False, - ), ) @@ -128,8 +67,94 @@ async def async_setup_entry( entities: list[E3DCBinarySensor] = [ E3DCBinarySensor(coordinator, description, entry.unique_id) for description in SENSOR_DESCRIPTIONS - if coordinator.wallbox_installed or not description.key.startswith("wallbox-") ] + + for wallbox in coordinator.wallboxes: + + wallbox_sun_mode_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-sun-mode", + translation_key="wallbox-sun-mode", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:weather-sunny", + off_icon="mdi:weather-sunny-off", + device_class=None, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_sun_mode_description, entry.unique_id)) + + wallbox_plug_lock_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-plug-lock", + translation_key="wallbox-plug-lock", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:lock-open", + off_icon="mdi:lock", + device_class=BinarySensorDeviceClass.LOCK, + entity_registry_enabled_default=False, # Disabled per default as only Wallbox easy connect provides this state + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_plug_lock_description, entry.unique_id)) + + wallbox_plug_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-plug", + translation_key="wallbox-plug", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:power-plug", + off_icon="mdi:power-plug-off", + device_class=BinarySensorDeviceClass.PLUG, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_plug_description, entry.unique_id)) + + wallbox_schuko_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-schuko", + translation_key="wallbox-schuko", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:power-plug-outline", + off_icon="mdi:power-plug-off-outline", + device_class=BinarySensorDeviceClass.POWER, + entity_registry_enabled_default=False, # Disabled per default as only Wallbox multi connect I provides this feature + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_schuko_description, entry.unique_id)) + + wallbox_charging_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-charging", + translation_key="wallbox-charging", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:car-electric", + off_icon="mdi:car-electric-outline", + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_charging_description, entry.unique_id)) + + wallbox_charging_canceled_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-charging-canceled", + translation_key="wallbox-charging-canceled", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:cancel", + off_icon="mdi:check-circle-outline", + device_class=None, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_charging_canceled_description, entry.unique_id)) + + wallbox_battery_to_car_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-battery-to-car", + translation_key="wallbox-battery-to-car", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:battery-charging", + off_icon="mdi:battery-off", + device_class=None, + entity_registry_enabled_default=False, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_battery_to_car_description, entry.unique_id)) + + wallbox_key_state_description = E3DCBinarySensorEntityDescription( + key=wallbox["key"] + "-key-state", + translation_key="wallbox-key-state", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + on_icon="mdi:key-variant", + off_icon="mdi:key-remove", + device_class=BinarySensorDeviceClass.LOCK, + entity_registry_enabled_default=False, + ) + entities.append(E3DCBinarySensor(coordinator, wallbox_key_state_description, entry.unique_id)) + async_add_entities(entities) diff --git a/custom_components/e3dc_rscp/button.py b/custom_components/e3dc_rscp/button.py index f16871c..1385ae1 100644 --- a/custom_components/e3dc_rscp/button.py +++ b/custom_components/e3dc_rscp/button.py @@ -31,20 +31,7 @@ class E3DCButtonEntityDescription(ButtonEntityDescription): ) = None -BUTTONS: Final[tuple[E3DCButtonEntityDescription, ...]] = ( - E3DCButtonEntityDescription( - key="wallbox-toggle-wallbox-phases", - translation_key="wallbox-toggle-wallbox-phases", - icon="mdi:sine-wave", - async_press_action=lambda coordinator: coordinator.async_toggle_wallbox_phases(), - ), - E3DCButtonEntityDescription( - key="wallbox-toggle_wallbox-charging", - translation_key="wallbox-toggle-wallbox-charging", - icon="mdi:car-electric", - async_press_action=lambda coordinator: coordinator.async_toggle_wallbox_charging(), - ), -) +BUTTONS: Final[tuple[E3DCButtonEntityDescription, ...]] = () # None yet async def async_setup_entry( @@ -57,9 +44,29 @@ async def async_setup_entry( entities: list[E3DCButton] = [ E3DCButton(coordinator, description, entry.unique_id) for description in BUTTONS - if coordinator.wallbox_installed or not description.key.startswith("wallbox-") ] + for wallbox in coordinator.wallboxes: + + wallbox_toggle_wallbox_phases_description = E3DCButtonEntityDescription( + key=wallbox["key"] + "-toggle-wallbox-phases", + translation_key="wallbox-toggle-wallbox-phases", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:sine-wave", + async_press_action=lambda coordinator: coordinator.async_toggle_wallbox_phases(), + ) + entities.append(E3DCButton(coordinator, wallbox_toggle_wallbox_phases_description, entry.unique_id)) + + wallbox_toggle_wallbox_charging_description = E3DCButtonEntityDescription( + key=wallbox["key"] + "-toggle-wallbox-charging", + translation_key="wallbox-toggle-wallbox-charging", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:car-electric", + async_press_action=lambda coordinator: coordinator.async_toggle_wallbox_charging(), + ) + entities.append(E3DCButton(coordinator, wallbox_toggle_wallbox_charging_description, entry.unique_id)) + + async_add_entities(entities) diff --git a/custom_components/e3dc_rscp/const.py b/custom_components/e3dc_rscp/const.py index 85cbc5a..c367fe7 100644 --- a/custom_components/e3dc_rscp/const.py +++ b/custom_components/e3dc_rscp/const.py @@ -12,6 +12,7 @@ SERVICE_MANUAL_CHARGE = "manual_charge" SERVICE_SET_WALLBOX_MAX_CHARGE_CURRENT = "set_wallbox_max_charge_current" MAX_CHARGE_CURRENT = 32 # Maximum allowed wallbox charging current in Amperes +MAX_WALLBOXES_POSSIBLE = 8 # 8 is the maximum according to RSCP Specification PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, diff --git a/custom_components/e3dc_rscp/coordinator.py b/custom_components/e3dc_rscp/coordinator.py index 7861c78..587a9d3 100644 --- a/custom_components/e3dc_rscp/coordinator.py +++ b/custom_components/e3dc_rscp/coordinator.py @@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.components.sensor import SensorStateClass -from .const import DOMAIN, MAX_CHARGE_CURRENT +from .const import DOMAIN, MAX_CHARGE_CURRENT, MAX_WALLBOXES_POSSIBLE from .e3dc_proxy import E3DCProxy @@ -38,7 +38,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._sw_version: str = "" self._update_guard_powersettings: bool = False self._update_guard_wallboxsettings: bool = False - self._wallbox_installed: bool = False + self._wallboxes: list[dict[str, str | int]] = [] self._timezone_offset: int = 0 self._next_stat_update: float = 0 @@ -83,35 +83,48 @@ async def async_connect(self): await self._load_timezone_settings() - @property - def wallbox_installed(self) -> bool: - """Get the wallbox installed status.""" - return self._wallbox_installed - - @wallbox_installed.setter - def wallbox_installed(self, value: bool) -> None: - """Set the wallbox installed status.""" - self._wallbox_installed = value async def async_identify_wallboxes(self): """Identify availability of Wallboxes if get_wallbox_data() returns meaningful data.""" _LOGGER.debug("async_identify_wallboxes") # TODO: Find a more robust way to identify if a Wallbox is installed - try: - request_data: dict[str, Any] = await self.hass.async_add_executor_job( - self.proxy.get_wallbox_data - ) - except HomeAssistantError as ex: - _LOGGER.warning("Failed to load wallboxes, not updating data: %s", ex) - return + for wbIndex in range(0, MAX_WALLBOXES_POSSIBLE-1): + try: + request_data: dict[str, Any] = await self.hass.async_add_executor_job( + self.proxy.get_wallbox_data, wbIndex + ) + except HomeAssistantError as ex: + _LOGGER.warning("Failed to load wallbox with index %s, not updating data: %s", wbIndex, ex) + return + + if request_data["appSoftware"] is not None: + _LOGGER.debug("Wallbox with index %s has been found", wbIndex) + wallbox = { + "index": wbIndex, + "key": f"wallbox-{wbIndex + 1}", + "name": f"Wallbox {wbIndex + 1}" + } + self.wallboxes.append(wallbox) + else: + _LOGGER.debug("No Wallbox with index %s has been found", wbIndex) - if request_data["appSoftware"] is not None: - _LOGGER.debug("Wallbox has been found") - self.wallbox_installed = True - else: - _LOGGER.debug("No Wallbox has been found") - self.wallbox_installed = False + # Fix Naming if there's only one wallbox + if len(self.wallboxes) == 1: + self.wallboxes[0]["key"] = "wallbox" + self.wallboxes[0]["name"] = "Wallbox" + + # Getter for _wallboxes + @property + def wallboxes(self) -> list[dict[str, str | int]]: + """Get the list of wallboxes.""" + return self._wallboxes + + # Setter for _wallboxes + @wallboxes.setter + def wallboxes(self, value: list[dict[str, str | int]]) -> None: + """Set the list of wallboxes.""" + self._wallboxes = value async def _async_connect_additional_powermeters(self): """Identify the installed powermeters and reconnect to E3DC with this config.""" @@ -193,7 +206,7 @@ async def _async_update_data(self) -> dict[str, Any]: else: _LOGGER.debug("Not polling wallbox, they are updating right now") - if self.wallbox_installed is True: + if len(self.wallboxes) > 0: _LOGGER.debug("Polling wallbox") await self._load_and_process_wallbox_data() @@ -298,28 +311,31 @@ async def _load_and_process_powermeters_data(self) -> None: async def _load_and_process_wallbox_data(self) -> None: """Load and process wallbox data to existing data.""" - try: - request_data: dict[str, Any] = await self.hass.async_add_executor_job( - self.proxy.get_wallbox_data - ) - except HomeAssistantError as ex: - _LOGGER.warning("Failed to load wallboxes, not updating data: %s", ex) - return - for key, value in request_data.items(): - formatted_key = "wallbox-" + re.sub(r'(? dict[str, Any]: return self.e3dc.get_powermeters(keepAlive=True) @e3dc_call - def get_wallbox_data(self) -> dict[str, Any]: + def get_wallbox_data(self, wbIndex: int = 0) -> dict[str, Any]: """Poll current wallbox readings.""" - return self.e3dc.get_wallbox_data(keepAlive=True) + return self.e3dc.get_wallbox_data(wbIndex=wbIndex, keepAlive=True) @e3dc_call def get_powermeters_data(self) -> dict[str, Any]: diff --git a/custom_components/e3dc_rscp/sensor.py b/custom_components/e3dc_rscp/sensor.py index 9ad0b14..56eedb5 100644 --- a/custom_components/e3dc_rscp/sensor.py +++ b/custom_components/e3dc_rscp/sensor.py @@ -372,94 +372,6 @@ device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( - key="wallbox-app-software", - translation_key="wallbox-app-software", - icon="mdi:information-outline", - device_class=None, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC - ), - SensorEntityDescription( - key="wallbox-consumption-net", - translation_key="wallbox-consumption-net", - icon="mdi:transmission-tower-import", - native_unit_of_measurement=UnitOfPower.WATT, - suggested_unit_of_measurement=UnitOfPower.KILO_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="wallbox-consumption-sun", - translation_key="wallbox-consumption-sun", - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfPower.WATT, - suggested_unit_of_measurement=UnitOfPower.KILO_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="wallbox-energy-all", - translation_key="wallbox-energy-all", - icon="mdi:counter", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - suggested_display_precision=2, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="wallbox-energy-net", - translation_key="wallbox-energy-net", - icon="mdi:counter", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - suggested_display_precision=2, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="wallbox-energy-sun", - translation_key="wallbox-energy-sun", - icon="mdi:counter", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - suggested_display_precision=2, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="wallbox-index", - translation_key="wallbox-index", - icon="mdi:numeric", - device_class=None, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC - ), - SensorEntityDescription( - key="wallbox-max-charge-current", - translation_key="wallbox-max-charge-current", - icon="mdi:current-ac", - native_unit_of_measurement="A", - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="wallbox-phases", - translation_key="wallbox-phases", - icon="mdi:sine-wave", - device_class=None, - ), - SensorEntityDescription( - key="wallbox-soc", - translation_key="wallbox-soc", - icon="mdi:battery-charging", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=False, - ), ) @@ -472,7 +384,6 @@ async def async_setup_entry( entities: list[E3DCSensor] = [ E3DCSensor(coordinator, description, entry.unique_id) for description in SENSOR_DESCRIPTIONS - if coordinator.wallbox_installed or not description.key.startswith("wallbox-") ] # Add Sensor descriptions for additional powermeters, skipp root PM @@ -508,6 +419,127 @@ async def async_setup_entry( ) entities.append(E3DCSensor(coordinator, power_description, entry.unique_id)) + for wallbox in coordinator.wallboxes: + + wallbox_app_software_description = SensorEntityDescription( + key=wallbox["key"] + "-app-software", + translation_key="wallbox-app-software", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:information-outline", + device_class=None, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC + ) + entities.append(E3DCSensor(coordinator, wallbox_app_software_description, entry.unique_id)) + + wallbox_consumption_net_description = SensorEntityDescription( + key=wallbox["key"] + "-consumption-net", + translation_key="wallbox-consumption-net", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:transmission-tower-import", + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ) + entities.append(E3DCSensor(coordinator, wallbox_consumption_net_description, entry.unique_id)) + + wallbox_consumption_sun_description = SensorEntityDescription( + key=wallbox["key"] + "-consumption-sun", + translation_key="wallbox-consumption-sun", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ) + entities.append(E3DCSensor(coordinator, wallbox_consumption_sun_description, entry.unique_id)) + + wallbox_energy_all_description = SensorEntityDescription( + key=wallbox["key"] + "-energy-all", + translation_key="wallbox-energy-all", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:counter", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + entities.append(E3DCSensor(coordinator, wallbox_energy_all_description, entry.unique_id)) + + wallbox_energy_net_description = SensorEntityDescription( + key=wallbox["key"] + "-energy-net", + translation_key="wallbox-energy-net", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:counter", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + entities.append(E3DCSensor(coordinator, wallbox_energy_net_description, entry.unique_id)) + + wallbox_energy_sun_description = SensorEntityDescription( + key=wallbox["key"] + "-energy-sun", + translation_key="wallbox-energy-sun", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:counter", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + entities.append(E3DCSensor(coordinator, wallbox_energy_sun_description, entry.unique_id)) + + wallbox_index_description = SensorEntityDescription( + key=wallbox["key"] + "-index", + translation_key="wallbox-index", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:numeric", + device_class=None, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC + ) + entities.append(E3DCSensor(coordinator, wallbox_index_description, entry.unique_id)) + + wallbox_max_charge_current_description = SensorEntityDescription( + key=wallbox["key"] + "-max-charge-current", + translation_key="wallbox-max-charge-current", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:current-ac", + native_unit_of_measurement="A", + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ) + entities.append(E3DCSensor(coordinator, wallbox_max_charge_current_description, entry.unique_id)) + + wallbox_phases_description = SensorEntityDescription( + key=wallbox["key"] + "-phases", + translation_key="wallbox-phases", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:sine-wave", + device_class=None, + ) + entities.append(E3DCSensor(coordinator, wallbox_phases_description, entry.unique_id)) + + wallbox_soc_description = SensorEntityDescription( + key=wallbox["key"] + "-soc", + translation_key="wallbox-soc", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + icon="mdi:battery-charging", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ) + entities.append(E3DCSensor(coordinator, wallbox_soc_description, entry.unique_id)) + + async_add_entities(entities) diff --git a/custom_components/e3dc_rscp/strings.json b/custom_components/e3dc_rscp/strings.json index 4653faf..beb163b 100644 --- a/custom_components/e3dc_rscp/strings.json +++ b/custom_components/e3dc_rscp/strings.json @@ -40,28 +40,28 @@ "name": "Manual charge" }, "wallbox-battery-to-car": { - "name": "Wallbox battery to car" + "name": "{wallbox_name} battery to car" }, "wallbox-charging": { - "name": "Wallbox charging" + "name": "{wallbox_name} charging" }, "wallbox-charging-canceled": { - "name": "Wallbox charging canceled" + "name": "{wallbox_name} charging canceled" }, "wallbox-sun-mode": { - "name": "Wallbox sun mode" + "name": "{wallbox_name} sun mode" }, "wallbox-plug-lock": { - "name": "Wallbox plug lock" + "name": "{wallbox_name} plug lock" }, "wallbox-plug": { - "name": "Wallbox plug" + "name": "{wallbox_name} plug" }, "wallbox-schuko": { - "name": "Wallbox schuko" + "name": "{wallbox_name} schuko" }, "wallbox-key-state": { - "name": "Wallbox key state" + "name": "{wallbox_name} key state" } }, "sensor": { @@ -217,6 +217,36 @@ }, "farm-additional-total": { "name": "Farm additional powermeter - total" + }, + "wallbox-app-software": { + "name": "{wallbox_name} app software" + }, + "wallbox-consumption-net": { + "name": "{wallbox_name} consumption net" + }, + "wallbox-consumption-sun": { + "name": "{wallbox_name} consumption sun" + }, + "wallbox-energy-all": { + "name": "{wallbox_name} energy all" + }, + "wallbox-energy-net": { + "name": "{wallbox_name} energy net" + }, + "wallbox-energy-sun": { + "name": "{wallbox_name} energy sun" + }, + "wallbox-index": { + "name": "{wallbox_name} index" + }, + "wallbox-max-charge-current": { + "name": "{wallbox_name} max charge current" + }, + "wallbox-phases": { + "name": "{wallbox_name} phases" + }, + "wallbox-soc": { + "name": "{wallbox_name} state of charge" } }, "switch": { @@ -227,18 +257,18 @@ "name": "SmartPower powersaving" }, "wallbox-sun-mode": { - "name": "Wallbox sun mode" + "name": "{wallbox_name} sun mode" }, "wallbox-schuko": { - "name": "Wallbox schuko" + "name": "{wallbox_name} schuko" } }, "button": { "wallbox-toggle-wallbox-charging": { - "name": "Wallbox charging" + "name": "{wallbox_name} charging" }, "wallbox-toggle-wallbox-phases": { - "name": "Wallbox phases" + "name": "{wallbox_name} phases" } } }, diff --git a/custom_components/e3dc_rscp/switch.py b/custom_components/e3dc_rscp/switch.py index b91ad8b..0a004e8 100644 --- a/custom_components/e3dc_rscp/switch.py +++ b/custom_components/e3dc_rscp/switch.py @@ -66,37 +66,7 @@ class E3DCSwitchEntityDescription(SwitchEntityDescription): False ), ), - # REGULAR SWITCHES - E3DCSwitchEntityDescription( - # TODO: Figure out how the icons match the on/off state - key="wallbox-sun-mode", - translation_key="wallbox-sun-mode", - name="Wallbox Sun Mode", - on_icon="mdi:weather-sunny", - off_icon="mdi:weather-sunny-off", - device_class=SwitchDeviceClass.SWITCH, - async_turn_on_action=lambda coordinator: coordinator.async_set_wallbox_sun_mode( - True - ), - async_turn_off_action=lambda coordinator: coordinator.async_set_wallbox_sun_mode( - False - ), - ), - E3DCSwitchEntityDescription( - key="wallbox-schuko", - translation_key="wallbox-schuko", - name="Wallbox Schuko", - on_icon="mdi:power-plug", - off_icon="mdi:power-plug-off", - device_class=SwitchDeviceClass.OUTLET, - async_turn_on_action=lambda coordinator: coordinator.async_set_wallbox_schuko( - True - ), - async_turn_off_action=lambda coordinator: coordinator.async_set_wallbox_schuko( - False - ), - entity_registry_enabled_default=False, # Disabled per default as only Wallbox multi connect I provides this feature - ), + # REGULAR SWITCHES (None yet) ) @@ -109,8 +79,46 @@ async def async_setup_entry( entities: list[E3DCSwitch] = [ E3DCSwitch(coordinator, description, entry.unique_id) for description in SWITCHES - if coordinator.wallbox_installed or not description.key.startswith("wallbox-") ] + + for wallbox in coordinator.wallboxes: + + wallbox_sun_mode_description = E3DCSwitchEntityDescription( + # TODO: Figure out how the icons match the on/off state + key=wallbox["key"] + "-sun-mode", + translation_key="wallbox-sun-mode", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + name="Wallbox Sun Mode", + on_icon="mdi:weather-sunny", + off_icon="mdi:weather-sunny-off", + device_class=SwitchDeviceClass.SWITCH, + async_turn_on_action=lambda coordinator: coordinator.async_set_wallbox_sun_mode( + True + ), + async_turn_off_action=lambda coordinator: coordinator.async_set_wallbox_sun_mode( + False + ), + ) + entities.append(E3DCSwitch(coordinator, wallbox_sun_mode_description, entry.unique_id)) + + wallbox_schuko_description = E3DCSwitchEntityDescription( + key=wallbox["key"] + "-schuko", + translation_key="wallbox-schuko", + translation_placeholders = {"wallbox_name": wallbox["name"]}, + name="Wallbox Schuko", + on_icon="mdi:power-plug", + off_icon="mdi:power-plug-off", + device_class=SwitchDeviceClass.OUTLET, + async_turn_on_action=lambda coordinator: coordinator.async_set_wallbox_schuko( + True + ), + async_turn_off_action=lambda coordinator: coordinator.async_set_wallbox_schuko( + False + ), + entity_registry_enabled_default=False, # Disabled per default as only Wallbox multi connect I provides this feature + ) + entities.append(E3DCSwitch(coordinator, wallbox_schuko_description, entry.unique_id)) + async_add_entities(entities) diff --git a/custom_components/e3dc_rscp/translations/en.json b/custom_components/e3dc_rscp/translations/en.json index df13048..46d9104 100644 --- a/custom_components/e3dc_rscp/translations/en.json +++ b/custom_components/e3dc_rscp/translations/en.json @@ -40,28 +40,28 @@ "name": "Manual charge" }, "wallbox-battery-to-car": { - "name": "Wallbox battery to car" + "name": "{wallbox_name} battery to car" }, "wallbox-charging-canceled": { - "name": "Wallbox charging canceled" + "name": "{wallbox_name} charging canceled" }, "wallbox-sun-mode": { - "name": "Wallbox sun mode" + "name": "{wallbox_name} sun mode" }, "wallbox-key-state": { - "name": "Wallbox key state" + "name": "{wallbox_name} key state" }, "wallbox-plug-lock": { - "name": "Wallbox plug lock" + "name": "{wallbox_name} plug lock" }, "wallbox-plug": { - "name": "Wallbox plug" + "name": "{wallbox_name} plug" }, "wallbox-schuko": { - "name": "Wallbox schuko" + "name": "{wallbox_name} schuko" }, "wallbox-charging": { - "name": "Wallbox charging" + "name": "{wallbox_name} charging" } }, "sensor": { @@ -219,34 +219,34 @@ "name": "Farm additional powermeter - total" }, "wallbox-app-software": { - "name": "Wallbox app software" + "name": "{wallbox_name} app software" }, "wallbox-consumption-net": { - "name": "Wallbox consumption net" + "name": "{wallbox_name} consumption net" }, "wallbox-consumption-sun": { - "name": "Wallbox consumption sun" + "name": "{wallbox_name} consumption sun" }, "wallbox-energy-all": { - "name": "Wallbox energy all" + "name": "{wallbox_name} energy all" }, "wallbox-energy-net": { - "name": "Wallbox energy net" + "name": "{wallbox_name} energy net" }, "wallbox-energy-sun": { - "name": "Wallbox energy sun" + "name": "{wallbox_name} energy sun" }, "wallbox-index": { - "name": "Wallbox index" + "name": "{wallbox_name} index" }, "wallbox-max-charge-current": { - "name": "Wallbox max charge current" + "name": "{wallbox_name} max charge current" }, "wallbox-phases": { - "name": "Wallbox phases" + "name": "{wallbox_name} phases" }, "wallbox-soc": { - "name": "Wallbox state of charge" + "name": "{wallbox_name} state of charge" } }, "switch": { @@ -257,18 +257,18 @@ "name": "SmartPower powersaving" }, "wallbox-sun-mode": { - "name": "Wallbox sun mode" + "name": "{wallbox_name} sun mode" }, "wallbox-schuko": { - "name": "Wallbox schuko" + "name": "{wallbox_name} schuko" } }, "button": { "wallbox-toggle-wallbox-charging": { - "name": "Wallbox charging" + "name": "{wallbox_name} charging" }, "wallbox-toggle-wallbox-phases": { - "name": "Wallbox phases" + "name": "{wallbox_name} phases" } } }, From bb132a9539c9f2333eeeb65cc155ef6693a6e47e Mon Sep 17 00:00:00 2001 From: bullitt168 Date: Sun, 30 Jun 2024 21:21:32 +0000 Subject: [PATCH 2/2] Added multi-wallbox capability to service --- custom_components/e3dc_rscp/coordinator.py | 25 ++++++++------ custom_components/e3dc_rscp/e3dc_proxy.py | 34 +++++++++---------- custom_components/e3dc_rscp/services.py | 16 +++++++-- custom_components/e3dc_rscp/services.yaml | 9 +++++ custom_components/e3dc_rscp/strings.json | 4 +++ .../e3dc_rscp/translations/en.json | 4 +++ 6 files changed, 62 insertions(+), 30 deletions(-) diff --git a/custom_components/e3dc_rscp/coordinator.py b/custom_components/e3dc_rscp/coordinator.py index 587a9d3..51fb52b 100644 --- a/custom_components/e3dc_rscp/coordinator.py +++ b/custom_components/e3dc_rscp/coordinator.py @@ -89,25 +89,25 @@ async def async_identify_wallboxes(self): _LOGGER.debug("async_identify_wallboxes") # TODO: Find a more robust way to identify if a Wallbox is installed - for wbIndex in range(0, MAX_WALLBOXES_POSSIBLE-1): + for wallbox_index in range(0, MAX_WALLBOXES_POSSIBLE-1): try: request_data: dict[str, Any] = await self.hass.async_add_executor_job( - self.proxy.get_wallbox_data, wbIndex + self.proxy.get_wallbox_data, wallbox_index ) except HomeAssistantError as ex: - _LOGGER.warning("Failed to load wallbox with index %s, not updating data: %s", wbIndex, ex) + _LOGGER.warning("Failed to load wallbox with index %s, not updating data: %s", wallbox_index, ex) return if request_data["appSoftware"] is not None: - _LOGGER.debug("Wallbox with index %s has been found", wbIndex) + _LOGGER.debug("Wallbox with index %s has been found", wallbox_index) wallbox = { - "index": wbIndex, - "key": f"wallbox-{wbIndex + 1}", - "name": f"Wallbox {wbIndex + 1}" + "index": wallbox_index, + "key": f"wallbox-{wallbox_index + 1}", + "name": f"Wallbox {wallbox_index + 1}" } self.wallboxes.append(wallbox) else: - _LOGGER.debug("No Wallbox with index %s has been found", wbIndex) + _LOGGER.debug("No Wallbox with index %s has been found", wallbox_index) # Fix Naming if there's only one wallbox if len(self.wallboxes) == 1: @@ -503,7 +503,7 @@ async def async_clear_power_limits(self) -> None: _LOGGER.debug("Successfully cleared the power limits") - async def async_set_wallbox_max_charge_current(self, current: int | None) -> None: + async def async_set_wallbox_max_charge_current(self, current: int | None, wallbox_index: int | None) -> None: """Set the wallbox max charge current.""" # TODO: Add more refined way to deal with maximum charge current, right now it's hard coded to 32A. The max current is dependant on the local installations, many WBs are throttled at 16A, not 32A due to power grid restrictions. @@ -514,6 +514,11 @@ async def async_set_wallbox_max_charge_current(self, current: int | None) -> Non "async_set_wallbox_max_charge_current must be called with a positive current value." ) + if wallbox_index < 0 or wallbox_index >= MAX_WALLBOXES_POSSIBLE: + raise ValueError( + "async_set_wallbox_max_charge_current must be called with a valid wallbox id." + ) + if current > MAX_CHARGE_CURRENT: _LOGGER.warning("Limiting current to %s", MAX_CHARGE_CURRENT) current = MAX_CHARGE_CURRENT @@ -521,7 +526,7 @@ async def async_set_wallbox_max_charge_current(self, current: int | None) -> Non _LOGGER.debug("Setting wallbox max charge current to %s", current) await self.hass.async_add_executor_job( - self.proxy.set_wallbox_max_charge_current, current + self.proxy.set_wallbox_max_charge_current, current, wallbox_index ) _LOGGER.debug("Successfully set the wallbox max charge current to %s", current) diff --git a/custom_components/e3dc_rscp/e3dc_proxy.py b/custom_components/e3dc_rscp/e3dc_proxy.py index 984a35d..0a1bf4d 100644 --- a/custom_components/e3dc_rscp/e3dc_proxy.py +++ b/custom_components/e3dc_rscp/e3dc_proxy.py @@ -156,9 +156,9 @@ def get_powermeters(self) -> dict[str, Any]: return self.e3dc.get_powermeters(keepAlive=True) @e3dc_call - def get_wallbox_data(self, wbIndex: int = 0) -> dict[str, Any]: + def get_wallbox_data(self, wallbox_index: int = 0) -> dict[str, Any]: """Poll current wallbox readings.""" - return self.e3dc.get_wallbox_data(wbIndex=wbIndex, keepAlive=True) + return self.e3dc.get_wallbox_data(wbIndex=wallbox_index, keepAlive=True) @e3dc_call def get_powermeters_data(self) -> dict[str, Any]: @@ -232,78 +232,78 @@ def start_manual_charge(self, charge_amount_wh: int) -> None: _LOGGER.warning("Manual charging could not be activated") @e3dc_call - def set_wallbox_sun_mode(self, enabled: bool, wbIndex: int = 0): + def set_wallbox_sun_mode(self, enabled: bool, wallbox_index: int = 0): """Set wallbox charging mode to sun mode on/off. Args: enabled(bool): the desired state True = sun mode enabled, False = sun mode disabled - wbIndex (Optional[int]): index of the requested wallbox, + wallbox_index (Optional[int]): index of the requested wallbox, Returns: nothing """ - result: bool = self.e3dc.set_wallbox_sunmode(enabled, wbIndex, True) + result: bool = self.e3dc.set_wallbox_sunmode(enable=enabled, wbIndex=wallbox_index, keepAlive=True) if not result: raise HomeAssistantError("Failed to set wallbox to sun mode %s", enabled) @e3dc_call - def set_wallbox_schuko(self, enabled: bool, wbIndex: int = 0): + def set_wallbox_schuko(self, enabled: bool, wallbox_index: int = 0): """Set wallbox power outlet (schuko) to on/off. Args: enabled(bool): the desired state True = on, False = off - wbIndex (Optional[int]): index of the requested wallbox, + wallbox_index (Optional[int]): index of the requested wallbox, Returns: nothing """ - result: bool = self.e3dc.set_wallbox_schuko(enabled, wbIndex, True) + result: bool = self.e3dc.set_wallbox_schuko(enable=enabled, wbIndex=wallbox_index, keepAlive=True) if not result: raise HomeAssistantError("Failed to set wallbox schuko to %s", enabled) @e3dc_call - def toggle_wallbox_charging(self, wbIndex: int = 0): + def toggle_wallbox_charging(self, wallbox_index: int = 0): """Toggle charging of the wallbox. Args: - wbIndex (Optional[int]): index of the requested wallbox, + wallbox_index (Optional[int]): index of the requested wallbox, Returns: nothing """ - result: bool = self.e3dc.toggle_wallbox_charging(wbIndex, True) + result: bool = self.e3dc.toggle_wallbox_charging(wbIndex=wallbox_index, keepAlive=True) if not result: raise HomeAssistantError("Failed to toggle wallbox charging") @e3dc_call - def toggle_wallbox_phases(self, wbIndex: int = 0): + def toggle_wallbox_phases(self, wallbox_index: int = 0): """Toggle the phases of wallbox charging between 1 and 3 phases. Only works if "Phasen" in the portal/device is not set to Auto. Args: - wbIndex (Optional[int]): index of the requested wallbox, + wallbox_index (Optional[int]): index of the requested wallbox, Returns: nothing """ - result: bool = self.e3dc.toggle_wallbox_phases(wbIndex, True) + result: bool = self.e3dc.toggle_wallbox_phases(wbIndex=wallbox_index, keepAlive=True) if not result: raise HomeAssistantError("Failed to toggle wallbox phases") @e3dc_call def set_wallbox_max_charge_current( - self, max_charge_current: int, wbIndex: int = 0 + self, max_charge_current: int, wallbox_index: int = 0 ) -> bool: """Set the maximum charge current of the wallbox via RSCP protocol locally. Args: max_charge_current (int): maximum allowed charge current in A - wbIndex (Optional[int]): index of the requested wallbox + wallbox_index (Optional[int]): index of the requested wallbox Returns: True if success (wallbox has understood the request, but might have clipped the value) @@ -316,7 +316,7 @@ def set_wallbox_max_charge_current( max_charge_current = MAX_CHARGE_CURRENT return self.e3dc.set_wallbox_max_charge_current( - max_charge_current, wbIndex, keepAlive=True + max_charge_current=max_charge_current, wbIndex=wallbox_index, keepAlive=True ) @e3dc_call diff --git a/custom_components/e3dc_rscp/services.py b/custom_components/e3dc_rscp/services.py index 13cf36a..eb028b9 100644 --- a/custom_components/e3dc_rscp/services.py +++ b/custom_components/e3dc_rscp/services.py @@ -24,6 +24,7 @@ _device_map: dict[str, E3DCCoordinator] = {} ATTR_DEVICEID = "device_id" +ATTR_WALLBOX_INDEX = "wallbox_index" ATTR_MAX_CHARGE = "max_charge" ATTR_MAX_DISCHARGE = "max_discharge" ATTR_CHARGE_AMOUNT = "charge_amount" @@ -46,6 +47,7 @@ SCHEMA_SET_WALLBOX_MAX_CHARGE_CURRENT = vol.Schema( { vol.Required(ATTR_DEVICEID): str, + vol.Required(ATTR_WALLBOX_INDEX): vol.All(int, vol.Range(min=0)), vol.Optional(ATTR_MAX_CHARGE_CURRENT): vol.All(int, vol.Range(min=0)), } ) @@ -137,16 +139,24 @@ async def _async_set_wallbox_max_charge_current( hass: HomeAssistant, call: ServiceCall ) -> None: """Extract service information and relay to coordinator.""" - # TODO: Add option to select Wallbox + + _LOGGER.debug("begin of _async_set_wallbox_max_charge_current") + coordinator: E3DCCoordinator = _resolve_device_id( hass, call.data.get(ATTR_DEVICEID) ) + wallbox_index: int | None = call.data.get(ATTR_WALLBOX_INDEX) + if wallbox_index is None: + raise HomeAssistantError( + f"{SERVICE_SET_WALLBOX_MAX_CHARGE_CURRENT}: Need to set {ATTR_WALLBOX_INDEX}" + ) max_charge_current: int | None = call.data.get(ATTR_MAX_CHARGE_CURRENT) if max_charge_current is None: raise HomeAssistantError( - f"{SERVICE_SET_POWER_LIMITS}: Need to set {ATTR_MAX_CHARGE_CURRENT}" + f"{SERVICE_SET_WALLBOX_MAX_CHARGE_CURRENT}: Need to set {ATTR_MAX_CHARGE_CURRENT}" ) - await coordinator.async_set_wallbox_max_charge_current(current=max_charge_current) + _LOGGER.debug("calling coordinator.async_set_wallbox_max_charge_current") + await coordinator.async_set_wallbox_max_charge_current(current=max_charge_current, wallbox_index=wallbox_index) async def _async_set_power_limits(hass: HomeAssistant, call: ServiceCall) -> None: diff --git a/custom_components/e3dc_rscp/services.yaml b/custom_components/e3dc_rscp/services.yaml index 9f2198a..7cd5048 100644 --- a/custom_components/e3dc_rscp/services.yaml +++ b/custom_components/e3dc_rscp/services.yaml @@ -45,6 +45,15 @@ set_wallbox_max_charge_current: device: filter: integration: e3dc_rscp + wallbox_index: + required: true + example: "0" + selector: + number: + min: 0 + max: 7 + mode: box + step: 1 max_charge_current: required: false example: "16" diff --git a/custom_components/e3dc_rscp/strings.json b/custom_components/e3dc_rscp/strings.json index beb163b..2639e16 100644 --- a/custom_components/e3dc_rscp/strings.json +++ b/custom_components/e3dc_rscp/strings.json @@ -309,6 +309,10 @@ "name": "E3DC Device ID", "description": "E3DC Device ID, take it either from the YAML-Mode on the website of out of the URL of the device configuration page." }, + "wallbox_index": { + "name": "Wallbox Index", + "description": "Index of the Wallbox, You find it in the diagnostic entity of the wallbox \"Wallbox Index\" (deactivated by default)." + }, "max_charge_current": { "name": "Maximum Charging Current (A)", "description": "Maximum allowed Charging via Wallbox in Ampere." diff --git a/custom_components/e3dc_rscp/translations/en.json b/custom_components/e3dc_rscp/translations/en.json index 46d9104..af0acca 100644 --- a/custom_components/e3dc_rscp/translations/en.json +++ b/custom_components/e3dc_rscp/translations/en.json @@ -309,6 +309,10 @@ "name": "E3DC Device ID", "description": "E3DC Device ID, take it either from the YAML-Mode on the website of out of the URL of the device configuration page." }, + "wallbox_index": { + "name": "Wallbox Index", + "description": "Index of the Wallbox, You find it in the diagnostic entity of the wallbox \"Wallbox Index\" (deactivated by default)." + }, "max_charge_current": { "name": "Maximum Charging Current (A)", "description": "Maximum allowed Charging via Wallbox in Ampere."