diff --git a/README.md b/README.md index 3aa08f3..443ad03 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,14 @@ support this repo in the future with possible enhancements for the WEB-API__. - Added German Setup/GUI "translation" (not for the sensor's yet) +- Added support to read and update the spare capacity ("Notstromreserve") + - When you are using "SENEC Backup Power pro" and you are able to see and update the spare capacity at mein-senec.de, than you can read and update the spare capacity with this integration. + - Precondition: You are using the Web Api + - Since the functionallity is disabled by default, navigate to Settings -> Devices & Services, select the Senec Integration and click on the entities of the Web API. + Here you can activate the "Spare Capacity". + Once activated you can add the entity to your dashboard. When you click on the shown spare capacity on your dashboard a slider will be shown. With this slider you can change the spare capacity. The chance will automatically be synchronized with mein-senec.de + + ## Switching to this fork... Please find in all information you need to know diff --git a/custom_components/senec/__init__.py b/custom_components/senec/__init__.py index 57492c7..9da924b 100644 --- a/custom_components/senec/__init__.py +++ b/custom_components/senec/__init__.py @@ -37,7 +37,7 @@ SCAN_INTERVAL = timedelta(seconds=60) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["sensor", "binary_sensor", "switch"] +PLATFORMS = ["sensor", "binary_sensor", "switch", "number"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/custom_components/senec/const.py b/custom_components/senec/const.py index 28909c3..7d68cf1 100644 --- a/custom_components/senec/const.py +++ b/custom_components/senec/const.py @@ -9,6 +9,10 @@ SensorStateClass, ) from homeassistant.components.switch import SwitchEntityDescription +from homeassistant.components.number import ( + NumberEntityDescription, + NumberDeviceClass +) from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, PERCENTAGE, @@ -75,6 +79,21 @@ class ExtSensorEntityDescription(SensorEntityDescription): class ExtBinarySensorEntityDescription(BinarySensorEntityDescription): icon_off: str | None = None +"""Supported number implementations""" +WEB_NUMBER_SENYOR_TYPES = [ + NumberEntityDescription( + entity_registry_enabled_default=False, + key="spare_capacity", + name="Spare Capacity", + device_class = NumberDeviceClass.BATTERY, + mode = "slider", + native_max_value = 100, + native_min_value = 0, + native_step = 1, + native_unit_of_measurement = PERCENTAGE, + icon="mdi:battery-lock", + ), +] """Supported main unit switch types.""" MAIN_SWITCH_TYPES = [ @@ -386,7 +405,6 @@ class ExtBinarySensorEntityDescription(BinarySensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), - ExtSensorEntityDescription( key="solar_mpp1_potential", name="MPP1 Potential", @@ -459,7 +477,6 @@ class ExtBinarySensorEntityDescription(BinarySensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.MEASUREMENT, ), - ExtSensorEntityDescription( key="enfluri_net_freq", name="Enfluri Net Frequency", diff --git a/custom_components/senec/manifest.json b/custom_components/senec/manifest.json index f69abdc..4326c0e 100644 --- a/custom_components/senec/manifest.json +++ b/custom_components/senec/manifest.json @@ -4,7 +4,8 @@ "codeowners": [ "@marq24", "@mchwalisz", - "@mstuettgen" + "@mstuettgen", + "@io-debug" ], "config_flow": true, "dependencies": [], @@ -12,5 +13,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/marq24/ha-senec-v3/issues", "requirements": [], - "version": "3.0.6" + "version": "3.0.7" } diff --git a/custom_components/senec/number.py b/custom_components/senec/number.py new file mode 100644 index 0000000..c3423ae --- /dev/null +++ b/custom_components/senec/number.py @@ -0,0 +1,63 @@ +"""Platform for Senec numbers.""" +import logging + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify +from homeassistant.const import CONF_TYPE + +from . import SenecDataUpdateCoordinator, SenecEntity +from .const import ( + DOMAIN, + WEB_NUMBER_SENYOR_TYPES, + CONF_SYSTYPE_WEB +) + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities): + """Initialize sensor platform from config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + + #Take care that CONF_TYPE = CONF_SYSTEYPE_WEB, since the implementation works with the web API + if CONF_TYPE in config_entry.data and config_entry.data[CONF_TYPE] == CONF_SYSTYPE_WEB: + for description in WEB_NUMBER_SENYOR_TYPES: + entity = SenecNumber(coordinator, description) + entities.append(entity) + async_add_entities(entities) + +"""Implementation for the spare capacity of the senec device""" +class SenecNumber(SenecEntity, NumberEntity): + + def __init__( + self, + coordinator: SenecDataUpdateCoordinator, + description: NumberEntityDescription, + ): + """Initialize""" + super().__init__(coordinator=coordinator, description=description) + if (hasattr(self.entity_description, 'entity_registry_enabled_default')): + self._attr_entity_registry_enabled_default = self.entity_description.entity_registry_enabled_default + else: + self._attr_entity_registry_enabled_default = True + title = self.coordinator._config_entry.title + key = self.entity_description.key + name = self.entity_description.name + self.entity_id = f"number.{slugify(title)}_{key}" + self._attr_name = f"{title} {name}" + + @property + def state(self) -> int: + number = self.entity_description.key + value = getattr(self.coordinator.senec, number) + return int(value) + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + updated_spare_capacity = int(value) + api = self.coordinator.senec + await api.set_spare_capacity(updated_spare_capacity) + self.async_schedule_update_ha_state(force_refresh=True) + \ No newline at end of file diff --git a/custom_components/senec/pysenec_ha/__init__.py b/custom_components/senec/pysenec_ha/__init__.py index 3ed1356..9d48578 100644 --- a/custom_components/senec/pysenec_ha/__init__.py +++ b/custom_components/senec/pysenec_ha/__init__.py @@ -1371,6 +1371,13 @@ def __init__(self, user, pwd, websession, master_plant_number: int = 0): self._SENEC_API_URL_START = "https://mein-senec.de/endkunde/api/status/getstatus.php?type=" self._SENEC_API_URL_END = "&period=all&anlageNummer=%s" + # Calls for spare capacity - Base URL has to be followed by master plant number + self._SENEC_API_SPARE_CAPACITY_BASE_URL = "https://mein-senec.de/endkunde/api/senec/" + # Call the following URL (GET-Request) in order to get the spare capacity as int in the response body + self._SENEC_API_GET_SPARE_CAPACITY = "/emergencypower/reserve-in-percent" + # Call the following URL (Post Request) in order to set the spare capacity + self._SENEC_API_SET_SPARE_CAPACITY = "/emergencypower?reserve-in-percent=" + # can be used in all api calls, names come from senec website self._API_KEYS = [ "accuimport", # what comes OUT OF the accu @@ -1390,6 +1397,7 @@ def __init__(self, user, pwd, websession, master_plant_number: int = 0): self._energy_entities = {} self._power_entities = {} self._battery_entities = {} + self._spare_capacity = 0 #initialize the spare_capacity with 0 self._isAuthenticated = False def checkCookieJarType(self): @@ -1480,9 +1488,57 @@ async def update(self): self.checkCookieJarType() await self.update_now_kW_stats() await self.update_full_kWh_stats() + await self.update_spare_capacity() else: await self.authenticate(doUpdate=True, throw401=False) + """This function will update the spare capacity over the web api""" + async def update_spare_capacity(self): + _LOGGER.debug("***** update_spare_capacity(self) ********") + a_url = f"{self._SENEC_API_SPARE_CAPACITY_BASE_URL}{self._master_plant_number}{self._SENEC_API_GET_SPARE_CAPACITY}" + async with self.websession.get(a_url) as res: + try: + res.raise_for_status() + if res.status == 200: + self._spare_capacity = await res.text() + else: + self._isAuthenticated = False + await self.update() + + except ClientResponseError as exc: + if exc.status == 401: + self.purgeSenecCookies() + + self._isAuthenticated = False + await self.update() + """This function will set the spare capacity over the web api""" + async def set_spare_capacity(self,new_spare_capacity: int): + _LOGGER.debug("***** set_spare_capacity(self) ********") + a_url = f"{self._SENEC_API_SPARE_CAPACITY_BASE_URL}{self._master_plant_number}{self._SENEC_API_SET_SPARE_CAPACITY}{new_spare_capacity}" + #payload = f"reserve-in-percent={new_spare_capacity}" + async with self.websession.post(a_url, ssl=False) as res: + try: + res.raise_for_status() + if res.status == 200: + _LOGGER.debug("***** Set Spare Capacity successfully ********") + #res_body = await res.text() + #if res_body == "true": + #await self.update() + else: + self._isAuthenticated = False + await self.authenticate(doUpdate=False, throw401=False) + await self.set_spare_capacity(new_spare_capacity) + + except ClientResponseError as exc: + if exc.status == 401: + self.purgeSenecCookies() + + self._isAuthenticated = False + await self.authenticate(doUpdate=False, throw401=True) + await self.set_spare_capacity(new_spare_capacity) + + + async def update_now_kW_stats(self): _LOGGER.debug("***** update_now_kW_stats(self) ********") # grab NOW and TODAY stats @@ -1602,6 +1658,11 @@ async def update_get_systems(self, a_plant_number: int): self._isAuthenticated = False await self.authenticate(doUpdate=False, throw401=False) + @property + def spare_capacity(self) -> int: + if hasattr(self, '_spare_capacity'): + return int(self._spare_capacity) + @property def senec_num(self) -> str: if hasattr(self, '_dev_number'):