From 08ca4eb43ca99f5452256bc3b80da22205563e5a Mon Sep 17 00:00:00 2001 From: stefan Date: Fri, 27 Dec 2024 13:24:50 +0100 Subject: [PATCH 01/15] Rename HACS github action Signed-off-by: stefan --- .github/workflows/validate.yaml | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index c38f573..75e2956 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -1,4 +1,4 @@ -name: Validate +name: Validate with HACS on: push: diff --git a/README.md b/README.md index 40a1264..883eb83 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Homeassistant integration to show many stats of Sonnenbatterie that should work with current versions of Sonnenbatterie. [![Validate with hassfest](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml) +[![Validate with HACS](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml) ## Tested working with * eco 8.03 9010 ND From 9f36b777d22b1f645fe179c8a1801d72ab4deffd Mon Sep 17 00:00:00 2001 From: stefan Date: Fri, 27 Dec 2024 13:32:43 +0100 Subject: [PATCH 02/15] Update github action links Signed-off-by: stefan --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 883eb83..3403a42 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Homeassistant integration to show many stats of Sonnenbatterie that should work with current versions of Sonnenbatterie. [![Validate with hassfest](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml) -[![Validate with HACS](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml) +[![Validate with HACS](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml) ## Tested working with * eco 8.03 9010 ND @@ -13,7 +13,7 @@ that should work with current versions of Sonnenbatterie. ## Won't work with older Batteries * ex. model 9.2 eco from 2014 not working -## Important: ### +## Important: Set the update interval in the Integration Settings. Default is 30 seconds, don't go below 10 seconds otherwise you might encounter an exploding recorder database. From f1cde4f471118297da482097b3c13ddfbfd7dd3a Mon Sep 17 00:00:00 2001 From: stefan Date: Sun, 29 Dec 2024 23:59:05 +0100 Subject: [PATCH 03/15] This is the first version of the integration to provide user facing actions. Currently supported are: - `set_operating_mode(mode=)` - `charge_battery(power=)` - `discharge_battery(power=)` - `set_battery_reserve(value=)` - `set_config_item(item=, value=)` - `set_tou_schedule(schedule=)` - `get_tou_schedule()` Signed-off-by: stefan --- README.md | 228 ++++++++++++++++-- custom_components/sonnenbatterie/__init__.py | 202 +++++++++++++++- .../sonnenbatterie/config_flow.py | 10 +- custom_components/sonnenbatterie/const.py | 19 ++ .../sonnenbatterie/coordinator.py | 22 +- .../sonnenbatterie/manifest.json | 4 +- custom_components/sonnenbatterie/sensor.py | 4 +- 7 files changed, 448 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3403a42..17c634c 100644 --- a/README.md +++ b/README.md @@ -5,35 +5,231 @@ that should work with current versions of Sonnenbatterie. [![Validate with hassfest](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/hassfest.yaml) [![Validate with HACS](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml/badge.svg)](https://github.com/weltmeyer/ha_sonnenbatterie/actions/workflows/validate.yaml) +## Installation +Easiest way to install is to add this repository via [HACS](https://hacs.xyz). + ## Tested working with * eco 8.03 9010 ND * eco 8.0 DE 9010 ND * sonnenBatterie 10 performance -## Won't work with older Batteries +### Won't work with older Batteries * ex. model 9.2 eco from 2014 not working -## Important: -Set the update interval in the Integration Settings. Default is 30 seconds, don't -go below 10 seconds otherwise you might encounter an exploding recorder database. +## Sensors +The main foxus of the integration is to provide a comprehensive set of sensors +for your SonnenBatterie. Right after installation the most relevant sensors +are already activated. + +> [!TIP] +> If you want to dive deeper, just head over to your Sonnenbatterie device +> settings, click on "Entities" and enable the ones you're interested in. + + +## Actions +Since version 2024.12.03 this integration also supports actions you can use to +set some variables that influence the behaviour of your SonnenBatterie. + +Currently supported actions are: + +### `set_operating_mode(mode=)` +- Sets the operating mode of your SonnenBatterie. +- Supported values for `` are: + - `"manual"` + - `"automatic"` + - `"timeofuse"` + +##### Code snippet +``` yaml +action: sonnenbatterie.set_operating_mode +data: + mode: "automatic" +``` + +##### Response +An `int` representing the mode that has been set: +- 1: `manual` +- 2: `automatic` +- 10: `timeofuse` + +### `charge_battery(power=)` +> [!IMPORTANT] +> Requires the SonnenBatterie to be in `manual` or `auto`mode to have any +> effect. +> +> **Disables power delivery from the battery to local consumers!** + +- Sets your battery to charge with `` watts +- Disables discharging to support local consumers while charging +- Supported values for `` are: + - min. power = 0 (0 = disable functionality) + - max. power = value of your battery's `inverter_max_power` value. + + The integration tries to determine the upper limit automatically + and caps the input if a higher value than supported by the battery + is given + +##### Code snippet +``` yaml +action: sonnenbatterie.charge_battery +data: + power: 0 +``` + +##### Response +A `bool` value, either `True` if setting the value was successful or `False` +otherwise. + +### `discharge_battery(power=)` +> [!IMPORTANT] +> Requires the SonnenBatterie to be in `manual` or `auto`mode to have any +> effect. +> +> **Enables power delivery from the battery to local consumers and may result +> in sending power to the network if local demand is lower than the value +> given!** + +- Sets your battery to discharge with `` watts +- Disables charging of the battery while active +- Supported values for `` are: + - min. power = 0 (0 = disable functionality) + - max. power = value of your battery's `inverter_max_power` value. + + The integration tries to determine the upper limit automatically + and caps the input if a higher value than supported by the battery + is given + +##### Code snippet +``` yaml +action: sonnenbatterie.discharge_battery +data: + power: 0 +``` -### Problems and/or Unused/Unavailable sensors -Depending on the software on and the oparting mode of your Sonnenbatterie some -values may not be available. The integration does it's best to detect the absence -of these values. If a value isn't returned by your Sonnenbatterie you will see -entries like the following in your log: +##### Response +A `bool` value, either `True` if setting the value was successful or `False` +otherwise. -If you feel that your Sonnenbatterie **should** provide one or more of those -you can enable the "debug_mode" from +### `set_battery_reserve(value=)` + +- Sets the percentage of energy that should be left in the battery +- `` can be in the range from 0 - 100 + +##### Code snippet +``` yaml +action: sonnenbatterie.set_battery_reserve +data: + value: 10 +``` + +##### Response +An integer representing the current value of "battery reserve" + +### `set_config_item(item=, value=)` +- Allows to set some selected configuration variables of the SonnenBattery. +- Currently supported `` values: + - "EM_OperatingMode" + - allowed values: + - `manual` + - `automatic` + - `timeofuse` + - _prefer [`set_operating_mode`](.#set_operatingmode)) over this_ + - "EM_ToU_Schedule" + - set a scheulde for charging in ToU mode + - accepts JSON array as string of the format + ``` json + [ { "start": "10:00", + "stop": "11:00", + "threshold_p_mac": 10000 + }, + ... + ] + ``` + - time ranges **must not** overlap + - since there are only times, the schedules stay active if not deleted by + sending an empty array (`"[]"`) + - _prefer [`set_tou_schedule`](.#set_tou_schedule) over this_ + - "EM_USOC" + - set the battery reserve in percent (0 - 100) + - accepts *a string* representing the value, like `"15"` for 15% reserve + - _prefer [`set_battery_reserve`](.#set_battery_reserve) over this_ + +##### Code snippet +``` yaml +action: sonnenbatterie.set_config_item +data: + item: "EM_USOC" + value: "10" +``` +##### Response +``` json +{'EM_USOC': '10'} +``` + +### `set_tou_schedule(schedule=)` + +> [!IMPORTANT] +> The SonnenBatterie must be in `timeofuse` operating mode for any +> submitted schedule to take effekt. + +- Sets the shedule entries for the "Time of Use" operating mode +- The value for the schedule is a JSON array **in string format** +- The format is: + ``` json + [ { "start": "10:00", + "stop": "11:00", + "threshold_p_mac": 10000 + }, + ... + ] + ``` +- time ranges **must not** overlap +- since there are only times, the schedules stay active if not deleted by + sending an empty array (`"[]"`) + +##### Code snippet +``` yaml +action: sonnenbatterie.set_tou_schedule_string +data: + schedule: '[{"start":"10:00", "stop":"10:00", "threshold_p_max": 20000}]' +``` + +##### Result +``` json +{ + "schedule": '[{"start": "10:00", "stop": "10:00", "threshold_p_max": 20000}]' +} +``` + +### `get_tou_schedule()` +- Retrieves the current schedule as stored in your SonnenBatterie + +##### Code snippet +``` yaml +action: sonnenbatterie.get_tou_schedule +data: {} +``` + +##### Result +``` yaml +schedule: "[{\"start\":\"10:00\", \"stop\":\"10:00\", \"threshold_p_max\": 20000}]" +``` + +## Problems and/or unused/unavailable sensors +Depending on the software on and the operating mode of your Sonnenbatterie some +sonsors may not be available. The integration does its best to collect as many +values as possible. + +If you feel that your Sonnenbatterie doesn't provide a sensor you think it +should, you can enable a "Debug Mode" from _Settings -> Devices & Services -> Integrations -> Sonnenbatterie -> (...) -> Reconfigure_ -Just enable the "Debug mode" and watch the logs of your HomeAssistant instance. -You'll get the full data that's returned by your Sonnenbatterie in the logs. -Please put those logs along with the setting you want monitored into a new issue. +Then restart HomeAssistant and watch the logs. +You'll get the full data that's returned by your Sonnenbatterie there. +Please put those logs along with the setting you want monitored into +[a new issue](https://github.com/weltmeyer/ha_sonnenbatterie/issues). -## Install -Easiest way to install is to add this repository via [HACS](https://hacs.xyz). ## Screenshots :) ![image](https://user-images.githubusercontent.com/1668465/78452159-ed2d7d80-7689-11ea-9e30-3a66ecc2372a.png) diff --git a/custom_components/sonnenbatterie/__init__.py b/custom_components/sonnenbatterie/__init__.py index a5b1a96..54e6273 100644 --- a/custom_components/sonnenbatterie/__init__.py +++ b/custom_components/sonnenbatterie/__init__.py @@ -1,10 +1,22 @@ """The Sonnenbatterie integration.""" import json +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( Platform ) +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, SupportsResponse, +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError +from sonnenbatterie import AsyncSonnenBatterie +from timeofuse import TimeofUseSchedule from .const import * @@ -15,22 +27,204 @@ # hass.data.setdefault(DOMAIN, {}) # return True +SB_OPERATING_MODES = { + "manual": 1, + "automatic": 2, + "expansion": 6, + "timeofuse": 10 +} + + +SCHEMA_SET_BATTERY_RESERVE = vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_SERVICE_VALUE): vol.Range(min=0, max=100) + } +) + +SCHEMA_SET_CONFIG_ITEM = vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_SERVICE_ITEM, default=""): vol.In(CONF_CONFIG_ITEMS), + vol.Required(CONF_SERVICE_VALUE, default=""): str + } +) + +SCHEMA_SET_OPERATING_MODE = vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_SERVICE_MODE, default="automatic"): vol.In(CONF_OPERATING_MODES), + } +) + +SCHEMA_SET_TOU_SCHEDULE_STRING = vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_SERVICE_SCHEDULE): cv.string_with_no_html, + } +) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + LOGGER.info(f"setup_entry: {json.dumps(dict(config_entry.data))}\n{json.dumps(dict(config_entry.options))}") + ip_address = config_entry.data["ip_address"] + password = config_entry.data["password"] + username = config_entry.data["username"] + + sb = AsyncSonnenBatterie(username, password, ip_address) + await sb.login() + inverter_power = int((await sb.get_batterysystem())['battery_system']['system']['inverter_capacity']) + + # noinspection PyPep8Naming + SCHEMA_CHARGE_BATTERY = vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(CONF_CHARGE_WATT): vol.Range(min=0, max=inverter_power), + } + ) -async def async_setup_entry(hass, config_entry): - LOGGER.debug("setup_entry: " + json.dumps(dict(config_entry.data))) await hass.config_entries.async_forward_entry_setups(config_entry, [ Platform.SENSOR ]) # rustydust_241227: this doesn't seem to be needed # config_entry.add_update_listener(update_listener) config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) - return True + # service definitions + async def charge_battery(call: ServiceCall) -> ServiceResponse: + power = int(call.data.get(CONF_CHARGE_WATT)) + # Make sure we have an sb2 object + await sb.login() + response = await sb.sb2.charge_battery(power) + return { + "charge": response, + } + + async def discharge_battery(call: ServiceCall) -> ServiceResponse: + power = int(call.data.get(CONF_CHARGE_WATT)) + await sb.login() + response = await sb.sb2.discharge_battery(power) + return { + "discharge": response, + } + + async def set_battery_reserve(call: ServiceCall) -> ServiceResponse: + value = call.data.get(CONF_SERVICE_VALUE) + await sb.login() + response = int((await sb.sb2.set_battery_reserve(value))["EM_USOC"]) + return { + "battery_reserve": response, + } + + async def set_config_item(call: ServiceCall) -> ServiceResponse: + item = call.data.get(CONF_SERVICE_ITEM) + value = call.data.get(CONF_SERVICE_VALUE) + await sb.login() + response = await sb.sb2.set_config_item(item, value) + return { + "response": response, + } + + async def set_operating_mode(call: ServiceCall) -> ServiceResponse: + mode = SB_OPERATING_MODES.get(call.data.get('mode')) + await sb.login() + response = await sb.set_operating_mode(mode) + LOGGER.info("set_operating_mode: " + json.dumps(response)) + return { + "mode": response, + } + + async def set_tou_schedule(call: ServiceCall) -> ServiceResponse: + schedule = call.data.get(CONF_SERVICE_SCHEDULE) + try: + json_schedule = json.loads(schedule) + except ValueError as e: + raise HomeAssistantError(f"Schedule is not a valid JSON string: '{schedule}'") from e + + tou = TimeofUseSchedule() + try: + tou.load_tou_schedule_from_json(json_schedule) + except ValueError as e: + raise HomeAssistantError(f"Schedule is not a valid schedule: '{schedule}'") from e + except TypeError as t: + raise HomeAssistantError(f"Schedule is not a valid schedule: '{schedule}'") from t + + await sb.login() + response = await sb.sb2.set_tou_schedule_string(schedule) + return { + "schedule": response["EM_ToU_Schedule"], + } + + async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: + await sb.login() + response = await sb.sb2.get_tou_schedule_string() + LOGGER.info("get_tou_schedule_string: " + response) + return { + "schedule": response, + } + + # service registration + hass.services.async_register( + DOMAIN, + "charge_battery", + charge_battery, + schema=SCHEMA_CHARGE_BATTERY, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "discharge_battery", + discharge_battery, + schema=SCHEMA_CHARGE_BATTERY, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "set_battery_reserve", + set_battery_reserve, + schema=SCHEMA_SET_BATTERY_RESERVE, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "set_config_item", + set_config_item, + schema=SCHEMA_SET_CONFIG_ITEM, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "set_operating_mode", + set_operating_mode, + schema=SCHEMA_SET_OPERATING_MODE, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "set_tou_schedule", + set_tou_schedule, + schema=SCHEMA_SET_TOU_SCHEDULE_STRING, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "get_tou_schedule", + get_tou_schedule, + supports_response=SupportsResponse.ONLY, + ) + + # Done setting up the entry + return True + async def async_reload_entry(hass, entry): """Reload config entry.""" await async_unload_entry(hass, entry) await async_setup_entry(hass, entry) - # rustydust_241227: this doesn't seem to be needed # async def update_listener(hass, entry): # LOGGER.warning("Update listener" + json.dumps(dict(entry.options))) diff --git a/custom_components/sonnenbatterie/config_flow.py b/custom_components/sonnenbatterie/config_flow.py index 6f7a3fd..93d0515 100644 --- a/custom_components/sonnenbatterie/config_flow.py +++ b/custom_components/sonnenbatterie/config_flow.py @@ -1,5 +1,5 @@ # pylint: disable=no-name-in-module -from sonnenbatterie import sonnenbatterie +from sonnenbatterie import AsyncSonnenBatterie # pylint: enable=no-name-in-module import traceback @@ -28,7 +28,7 @@ class SonnenbatterieFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_IP_ADDRESS, default="127.0.0.1"): str, vol.Required(CONF_USERNAME): vol.In(["User", "Installer"]), vol.Required(CONF_PASSWORD, default="sonnenUser3552"): str, - vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.positive_int, + vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.Range(min=10, max=3600), vol.Optional(ATTR_SONNEN_DEBUG, default=DEFAULT_SONNEN_DEBUG): cv.boolean, } ) @@ -133,9 +133,9 @@ async def async_step_reconfigure(self, user_input): @staticmethod - def _internal_setup(_username, _password, _ipaddress): - sb_test = sonnenbatterie(_username, _password, _ipaddress) - return sb_test.get_systemdata().get("DE_Ticket_Number", "Unknown") + async def _internal_setup(_username, _password, _ipaddress): + sb_test = AsyncSonnenBatterie(_username, _password, _ipaddress) + return await sb_test.get_systemdata().get("DE_Ticket_Number", "Unknown") @callback def _show_form(self, errors=None): diff --git a/custom_components/sonnenbatterie/const.py b/custom_components/sonnenbatterie/const.py index a679449..f2338d3 100644 --- a/custom_components/sonnenbatterie/const.py +++ b/custom_components/sonnenbatterie/const.py @@ -10,6 +10,25 @@ LOGGER = logging.getLogger(__package__) +""" Limited to those that can be changed safely """ +CONF_CONFIG_ITEMS = [ + "EM_OperatingMode", + "EM_ToU_Schedule", + "EM_USOC" +] + +CONF_OPERATING_MODES = [ + "manual", + "automatic", + "timeofuse" +] + +CONF_CHARGE_WATT = "power" +CONF_SERVICE_ITEM = "item" +CONF_SERVICE_MODE = "mode" +CONF_SERVICE_SCHEDULE = "schedule" +CONF_SERVICE_VALUE = "value" + # rustydust_241227: doesn't seem to be used anywhere # def flatten_obj(prefix, seperator, obj): # result = {} diff --git a/custom_components/sonnenbatterie/coordinator.py b/custom_components/sonnenbatterie/coordinator.py index f606e69..8548fab 100644 --- a/custom_components/sonnenbatterie/coordinator.py +++ b/custom_components/sonnenbatterie/coordinator.py @@ -9,7 +9,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from sonnenbatterie import sonnenbatterie +from sonnenbatterie import AsyncSonnenBatterie from .const import DOMAIN, LOGGER, logging @@ -22,7 +22,7 @@ class SonnenBatterieCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - sb_inst: sonnenbatterie, + sb_inst: AsyncSonnenBatterie, update_interval_seconds: int, ip_address, debug_mode, @@ -45,7 +45,7 @@ def __init__( self.stopped = False - self.sbInst: sonnenbatterie = sb_inst + self.sbInst: AsyncSonnenBatterie = sb_inst self.meterSensors = {} self.update_interval_seconds = update_interval_seconds self.ip_address = ip_address @@ -85,22 +85,22 @@ async def _async_update_data(self): try: # ignore errors here, may be transient result = await self.hass.async_add_executor_job(self.sbInst.get_battery) - self.latestData["battery"] = result + self.latestData["battery"] = await result result = await self.hass.async_add_executor_job(self.sbInst.get_batterysystem) - self.latestData["battery_system"] = result + self.latestData["battery_system"] = await result result = await self.hass.async_add_executor_job(self.sbInst.get_inverter) - self.latestData["inverter"] = result + self.latestData["inverter"] = await result result = await self.hass.async_add_executor_job(self.sbInst.get_powermeter) - self.latestData["powermeter"] = result + self.latestData["powermeter"] = await result result = await self.hass.async_add_executor_job(self.sbInst.get_status) - self.latestData["status"] = result + self.latestData["status"] = await result result = await self.hass.async_add_executor_job(self.sbInst.get_systemdata) - self.latestData["system_data"] = result + self.latestData["system_data"] = await result except Exception as ex: if self.debug: @@ -141,9 +141,7 @@ async def _async_update_data(self): """ some manually calculated values """ batt_module_capacity = int( - self.latestData["battery_system"]["battery_system"]["system"][ - "storage_capacity_per_module" - ] + self.latestData["battery_system"]["battery_system"]["system"]["storage_capacity_per_module"] ) batt_module_count = int(self.latestData["battery_system"]["modules"]) diff --git a/custom_components/sonnenbatterie/manifest.json b/custom_components/sonnenbatterie/manifest.json index b5edf52..7379faf 100644 --- a/custom_components/sonnenbatterie/manifest.json +++ b/custom_components/sonnenbatterie/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/weltmeyer/ha_sonnenbatterie", "iot_class": "local_polling", "issue_tracker": "https://github.com/weltmeyer/ha_sonnenbatterie/issues", - "requirements": ["requests","sonnenbatterie>=0.3.0"], - "version": "2024.12.02" + "requirements": ["requests","sonnenbatterie>=0.5.0"], + "version": "2024.12.03" } diff --git a/custom_components/sonnenbatterie/sensor.py b/custom_components/sonnenbatterie/sensor.py index feae883..a8f8988 100644 --- a/custom_components/sonnenbatterie/sensor.py +++ b/custom_components/sonnenbatterie/sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import StateType from .coordinator import SonnenBatterieCoordinator -from sonnenbatterie import sonnenbatterie +from sonnenbatterie import AsyncSonnenBatterie from .const import ( ATTR_SONNEN_DEBUG, DEFAULT_SCAN_INTERVAL, @@ -50,7 +50,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): debug_mode = config_entry.data.get(ATTR_SONNEN_DEBUG) sonnen_inst = await hass.async_add_executor_job( - sonnenbatterie, username, password, ip_address + AsyncSonnenBatterie, username, password, ip_address ) update_interval_seconds = update_interval_seconds or DEFAULT_SCAN_INTERVAL From 26b245484c45ca019e530d3e30c5b8bfea7dad3e Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 00:10:39 +0100 Subject: [PATCH 04/15] Typo fix Signed-off-by: stefan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17c634c..1d74071 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Easiest way to install is to add this repository via [HACS](https://hacs.xyz). * ex. model 9.2 eco from 2014 not working ## Sensors -The main foxus of the integration is to provide a comprehensive set of sensors +The main focus of the integration is to provide a comprehensive set of sensors for your SonnenBatterie. Right after installation the most relevant sensors are already activated. From 73105c8ef8c0285bcea1c42141d2764a4483be4d Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 00:32:19 +0100 Subject: [PATCH 05/15] Add missing services.yaml Signed-off-by: stefan --- .../sonnenbatterie/services.yaml | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 custom_components/sonnenbatterie/services.yaml diff --git a/custom_components/sonnenbatterie/services.yaml b/custom_components/sonnenbatterie/services.yaml new file mode 100644 index 0000000..c28d9c6 --- /dev/null +++ b/custom_components/sonnenbatterie/services.yaml @@ -0,0 +1,53 @@ +set_operating_mode: + fields: + mode: + required: true + example: "timeofuse" + default: "automatic" + selector: + text: +charge_battery: + fields: + power: + required: true + example: "1000" + selector: + number: + min: 0 +discharge_battery: + fields: + power: + required: true + example: "1000" + selector: + number: + min: 0 +set_battery_reserve: + fields: + value: + required: true + selector: + number: + min: 0 + max: 100 +set_config_item: + fields: + item: + required: true + example: "EM_USOC" + selector: + text: + value: + required: true + example: "15" + selector: + text: +set_tou_schedule: + fields: + schedule: + required: true + example: "[{\"start\":\"10:00\", \"stop\":\"11:00\", \"threshold_p_max\": 20000 }]" + selector: + text: +get_tou_schedule: + fields: From e16dedc7a7e559b5780f47b8f6e9dc80676b7d47 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 00:33:49 +0100 Subject: [PATCH 06/15] Remove empty fields from `get_tou_schedule\' Signed-off-by: stefan --- custom_components/sonnenbatterie/services.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/sonnenbatterie/services.yaml b/custom_components/sonnenbatterie/services.yaml index c28d9c6..72ba62d 100644 --- a/custom_components/sonnenbatterie/services.yaml +++ b/custom_components/sonnenbatterie/services.yaml @@ -50,4 +50,3 @@ set_tou_schedule: selector: text: get_tou_schedule: - fields: From 7e588b179b1ac3c7b655a3aba5ee9b30899bc55e Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 00:36:26 +0100 Subject: [PATCH 07/15] Removed `min: 0` from open ended number fields Signed-off-by: stefan --- custom_components/sonnenbatterie/services.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/sonnenbatterie/services.yaml b/custom_components/sonnenbatterie/services.yaml index 72ba62d..d542abf 100644 --- a/custom_components/sonnenbatterie/services.yaml +++ b/custom_components/sonnenbatterie/services.yaml @@ -13,7 +13,6 @@ charge_battery: example: "1000" selector: number: - min: 0 discharge_battery: fields: power: @@ -21,7 +20,6 @@ discharge_battery: example: "1000" selector: number: - min: 0 set_battery_reserve: fields: value: From 000bd841f55b0f122c8b3338a40b077d28e39562 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 02:46:13 +0100 Subject: [PATCH 08/15] Add translations for actions Signed-off-by: stefan --- .../sonnenbatterie/translations/de.json | 77 +++++++++++++++++++ .../sonnenbatterie/translations/en.json | 77 +++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/custom_components/sonnenbatterie/translations/de.json b/custom_components/sonnenbatterie/translations/de.json index a5da7e0..c0da15e 100644 --- a/custom_components/sonnenbatterie/translations/de.json +++ b/custom_components/sonnenbatterie/translations/de.json @@ -149,5 +149,82 @@ "name": "Spannungswandler UPV 2" } } + }, + "services": { + "set_operating_mode": { + "name": "Setze Sonnenbatterie-Betriebsmodus", + "description": "Setzt den Betriebsmodus der Sonnenbatterie ('manual', 'automatic', 'timeofuse')", + "fields": { + "mode": { + "description": "Der zu setzende Betriebsmodus", + "name": "Betriebsmodus", + "example": "automatic" + } + } + }, + "charge_battery": { + "name": "Sonnenbatterie laden", + "description": "Erzwingt das Laden der Sonnenbatterie mit der angegebenen Leistung", + "fields": { + "power": { + "name": "Lade-Leistung in Watt", + "description": "Leistung, mit der die Sonnenbatterie geladen werden soll", + "example": "1000" + } + } + }, + "discharge_battery": { + "name": "Sonnenbatterie entladen", + "description": "Erzwingt die Abgabe von Leistung der Sonnenbatterie an Verbraucher", + "fields": { + "power": { + "name": "Entlade-Leistung in Watt", + "description": "Leistung, die aus der Sonnebatterie an Verbraucher abgegehen wird", + "example": "1234" + } + } + }, + "set_battery_reserve": { + "name": "Reservekapazität setzen", + "description": "Setzt die von der Sonnebatterie zurückgehaltenene Kapazität in Prozent", + "fields": { + "value": { + "name": "Zurückgehaltene Kapazität", + "description": "Kapazität in Prozent, die die Sonnenbatterie auf jeden Fall reservieren soll", + "example": "10" + } + } + }, + "set_config_item": { + "name": "Setzen eines Konfigurationswerts der Sonnenbatterie", + "description": "Erlaubt das Setzen einzelner Konfigurationswerte und Betriebsparameter der Sonnenbatterie", + "fields": { + "item": { + "name": "Zu setzender Parameter", + "description": "Der Name des zu setzenden Parameters der Sonnenbatterie", + "example": "EM_USOC" + }, + "value": { + "name": "Wert des zu setzenden Parameters", + "description": "Der zu setzende Wert, kann je nach Parameter ein Text, eine Zahl oder ein JSON-String sein", + "example": "10 (int) oder \"[]\" (leerer JSON-String)" + } + } + }, + "set_tou_schedule": { + "name": "Ladefenster festlegen", + "description": "Erlaubt das Setzen von Ladenfenstern im zeitgesteuerten Betriebsmodus", + "fields": { + "schedule": { + "name": "Zeitfenster (JSON-Array)", + "description": "Ein oder mehrere Zeitfenster-Angaben als JSON-Array im String-Format", + "example": "\"[{\"start\":\"10:00\", \"stop\":\"11:00\", \"threshold_p_max\": 20000}]\"" + } + } + }, + "get_tou_schedule": { + "name": "Auslesen der Lade-Zeitfenster", + "description": "Liefert die aktuell in der Sonnebatterie hinterlegten Zeitfenster für das Laden im zeitgesteuerten Modus" + } } } diff --git a/custom_components/sonnenbatterie/translations/en.json b/custom_components/sonnenbatterie/translations/en.json index 5081da7..90f7182 100644 --- a/custom_components/sonnenbatterie/translations/en.json +++ b/custom_components/sonnenbatterie/translations/en.json @@ -149,5 +149,82 @@ "name": "Inverter UPV 2" } } + }, + "services": { + "set_operating_mode": { + "name": "Set operating mode", + "description": "Sets the operating mode of the Sonnenbatterie", + "fields": { + "mode": { + "description": "Operating mode to set ('manual', 'automatic', 'timeofuse')", + "name": "Operating mode", + "example": "automatic" + } + } + }, + "charge_battery": { + "name": "Charge battery", + "description": "Forces the charging of the Sonnenbatterie with the specified power in W", + "fields": { + "power": { + "name": "Charging power", + "description": "Power to charge the Sonnenbatterie with", + "example": "1000" + } + } + }, + "discharge_battery": { + "name": "Discharge battery", + "description": "Forces the discharge of specified power from the Sonnenbatterie", + "fields": { + "power": { + "name": "Discharging power", + "description": "Power to discharge the Sonnenbatterie with", + "example": "1234" + } + } + }, + "set_battery_reserve": { + "name": "Set backup capacity", + "description": "Sets the backup capacity the Sonnenbatterie keeps back in percent of the total", + "fields": { + "value": { + "name": "Kept back capacity", + "description": "Precentage of the total capacity that should be kept back", + "example": "10" + } + } + }, + "set_config_item": { + "name": "Set a config parameter", + "description": "Allows changing of some of the Sonnenbatterie's operating paramters", + "fields": { + "item": { + "name": "Parameter name", + "description": "Name of the parameter that should be set", + "example": "EM_USOC" + }, + "value": { + "name": "Parameter value", + "description": "The value the parameter should be set to. Can be an integer or a string, depending on the parameter", + "example": "10 (int) or \"[]\" (empty JSON string)" + } + } + }, + "set_tou_schedule": { + "name": "Set charging schedule(s)", + "description": "Allows to set charging schedules that are honored when the Sonnenbattery is in Time-of-Use mode", + "fields": { + "schedule": { + "name": "Charging window(s)", + "description": "One or more charging windows as string containing a JSON array", + "example": "\"[{\"start\":\"10:00\", \"stop\":\"11:00\", \"threshold_p_max\": 20000}]\"" + } + } + }, + "get_tou_schedule": { + "name": "Get charging schedule", + "description": "Returns the charging schedules currently stored in the Sonnenbatterie's configuration" + } } } From 386f0d6f1e04bff5468069be23caabf06c300226 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 02:57:23 +0100 Subject: [PATCH 09/15] Remove another LOGGER I overlooked ... Signed-off-by: stefan --- custom_components/sonnenbatterie/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/sonnenbatterie/__init__.py b/custom_components/sonnenbatterie/__init__.py index 54e6273..49f587b 100644 --- a/custom_components/sonnenbatterie/__init__.py +++ b/custom_components/sonnenbatterie/__init__.py @@ -127,7 +127,6 @@ async def set_operating_mode(call: ServiceCall) -> ServiceResponse: mode = SB_OPERATING_MODES.get(call.data.get('mode')) await sb.login() response = await sb.set_operating_mode(mode) - LOGGER.info("set_operating_mode: " + json.dumps(response)) return { "mode": response, } @@ -156,7 +155,6 @@ async def set_tou_schedule(call: ServiceCall) -> ServiceResponse: async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: await sb.login() response = await sb.sb2.get_tou_schedule_string() - LOGGER.info("get_tou_schedule_string: " + response) return { "schedule": response, } From 6830fd54160f3e5308ed72ccaf6da000fcc6eae5 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 13:34:50 +0100 Subject: [PATCH 10/15] Async is a fickly beast ... Signed-off-by: stefan --- custom_components/sonnenbatterie/__init__.py | 16 ++++++++----- .../sonnenbatterie/config_flow.py | 24 ++++++++++++++----- .../sonnenbatterie/coordinator.py | 6 ++--- custom_components/sonnenbatterie/sensor.py | 3 --- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/custom_components/sonnenbatterie/__init__.py b/custom_components/sonnenbatterie/__init__.py index 49f587b..eecc7d3 100644 --- a/custom_components/sonnenbatterie/__init__.py +++ b/custom_components/sonnenbatterie/__init__.py @@ -65,7 +65,7 @@ ) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - LOGGER.info(f"setup_entry: {json.dumps(dict(config_entry.data))}\n{json.dumps(dict(config_entry.options))}") + LOGGER.info(f"setup_entry: {config_entry.data}\n{config_entry.options}") ip_address = config_entry.data["ip_address"] password = config_entry.data["password"] username = config_entry.data["username"] @@ -73,6 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sb = AsyncSonnenBatterie(username, password, ip_address) await sb.login() inverter_power = int((await sb.get_batterysystem())['battery_system']['system']['inverter_capacity']) + await sb.logout() # noinspection PyPep8Naming SCHEMA_CHARGE_BATTERY = vol.Schema( @@ -85,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await hass.config_entries.async_forward_entry_setups(config_entry, [ Platform.SENSOR ]) # rustydust_241227: this doesn't seem to be needed # config_entry.add_update_listener(update_listener) - config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + # config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) # service definitions @@ -152,6 +153,7 @@ async def set_tou_schedule(call: ServiceCall) -> ServiceResponse: "schedule": response["EM_ToU_Schedule"], } + # noinspection PyUnusedLocal async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: await sb.login() response = await sb.sb2.get_tou_schedule_string() @@ -218,10 +220,11 @@ async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: # Done setting up the entry return True -async def async_reload_entry(hass, entry): - """Reload config entry.""" - await async_unload_entry(hass, entry) - await async_setup_entry(hass, entry) +# rustydust_241230: no longer needed +# async def async_reload_entry(hass, entry): +# """Reload config entry.""" +# await async_unload_entry(hass, entry) +# await async_setup_entry(hass, entry) # rustydust_241227: this doesn't seem to be needed # async def update_listener(hass, entry): @@ -233,4 +236,5 @@ async def async_reload_entry(hass, entry): async def async_unload_entry(hass, entry): """Handle removal of an entry.""" + LOGGER.info(f"Unloading config entry: {entry}") return await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR) diff --git a/custom_components/sonnenbatterie/config_flow.py b/custom_components/sonnenbatterie/config_flow.py index 93d0515..91fea07 100644 --- a/custom_components/sonnenbatterie/config_flow.py +++ b/custom_components/sonnenbatterie/config_flow.py @@ -25,10 +25,10 @@ class SonnenbatterieFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONFIG_SCHEMA_USER = vol.Schema( { - vol.Required(CONF_IP_ADDRESS, default="127.0.0.1"): str, + vol.Required(CONF_IP_ADDRESS, default="192.168.0.1"): str, vol.Required(CONF_USERNAME): vol.In(["User", "Installer"]), vol.Required(CONF_PASSWORD, default="sonnenUser3552"): str, - vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.Range(min=10, max=3600), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, vol.Optional(ATTR_SONNEN_DEBUG, default=DEFAULT_SONNEN_DEBUG): cv.boolean, } ) @@ -52,15 +52,21 @@ async def async_step_user(self, user_input=None): LOGGER.error(f"Unable to connect to sonnenbatterie: {e}") return self._show_form({"base": "connection_error"}) + # async is a fickly beast ... + sb_serial = await my_serial + unique_id = f"{DOMAIN}-{sb_serial}" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( - title=f"{user_input[CONF_IP_ADDRESS]} ({my_serial})", + title=f"{user_input[CONF_IP_ADDRESS]} ({sb_serial})", data={ CONF_USERNAME: username, CONF_PASSWORD: password, CONF_IP_ADDRESS: ipaddress, CONF_SCAN_INTERVAL: user_input[CONF_SCAN_INTERVAL], ATTR_SONNEN_DEBUG: user_input[ATTR_SONNEN_DEBUG], - CONF_SERIAL_NUMBER: my_serial, + CONF_SERIAL_NUMBER: sb_serial, }, ) @@ -98,6 +104,7 @@ async def async_step_reconfigure(self, user_input): ) if user_input is not None: + LOGGER.info(f"Reconfiguring {entry}") if entry.data.get(CONF_SERIAL_NUMBER): await self.async_set_unique_id(entry.data[CONF_SERIAL_NUMBER]) self._abort_if_unique_id_configured() @@ -119,10 +126,12 @@ async def async_step_reconfigure(self, user_input): errors={"base": "connection_error"}, ) + # async is a fickly beast ... + sb_serial = await my_serial return self.async_update_reload_and_abort( self._get_reconfigure_entry(), data_updates=user_input, - title=f"{user_input[CONF_IP_ADDRESS]} ({my_serial})", + title=f"{user_input[CONF_IP_ADDRESS]} ({sb_serial})", ) @@ -135,7 +144,10 @@ async def async_step_reconfigure(self, user_input): @staticmethod async def _internal_setup(_username, _password, _ipaddress): sb_test = AsyncSonnenBatterie(_username, _password, _ipaddress) - return await sb_test.get_systemdata().get("DE_Ticket_Number", "Unknown") + await sb_test.login() + result = (await sb_test.get_systemdata()).get("DE_Ticket_Number", "Unknown") + await sb_test.logout() + return result @callback def _show_form(self, errors=None): diff --git a/custom_components/sonnenbatterie/coordinator.py b/custom_components/sonnenbatterie/coordinator.py index 8548fab..96a5337 100644 --- a/custom_components/sonnenbatterie/coordinator.py +++ b/custom_components/sonnenbatterie/coordinator.py @@ -103,13 +103,11 @@ async def _async_update_data(self): self.latestData["system_data"] = await result except Exception as ex: - if self.debug: - e = traceback.format_exc() - LOGGER.error(e) + LOGGER.info(traceback.format_exc()) if self._last_error is not None: elapsed = time() - self._last_error if elapsed > timedelta(seconds=180).total_seconds(): - LOGGER.warning(f"Unable to connecto to Sonnenbatteries at {self.ip_address} for {elapsed} seconds. Please check! [{ex}]") + LOGGER.error(f"Unable to connecto to Sonnenbatteries at {self.ip_address} for {elapsed} seconds. Please check! [{ex}]") else: self._last_error = time() diff --git a/custom_components/sonnenbatterie/sensor.py b/custom_components/sonnenbatterie/sensor.py index a8f8988..29d0290 100644 --- a/custom_components/sonnenbatterie/sensor.py +++ b/custom_components/sonnenbatterie/sensor.py @@ -29,9 +29,6 @@ generate_powermeter_sensors, ) -_LOGGER = logging.getLogger(__name__) - - # rustydust_241227: this doesn't seem to be used anywhere # async def async_unload_entry(hass, entry): # """Unload a config entry.""" From 0c3b50002cc5c4abe952e8e1fe8894ecdd20cfb6 Mon Sep 17 00:00:00 2001 From: stefan Date: Mon, 30 Dec 2024 18:57:57 +0100 Subject: [PATCH 11/15] Update requirement of `python_sonnenbatterie` to >= 0.5.1 Signed-off-by: stefan --- custom_components/sonnenbatterie/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/sonnenbatterie/manifest.json b/custom_components/sonnenbatterie/manifest.json index 7379faf..a8f3962 100644 --- a/custom_components/sonnenbatterie/manifest.json +++ b/custom_components/sonnenbatterie/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/weltmeyer/ha_sonnenbatterie", "iot_class": "local_polling", "issue_tracker": "https://github.com/weltmeyer/ha_sonnenbatterie/issues", - "requirements": ["requests","sonnenbatterie>=0.5.0"], + "requirements": ["requests","sonnenbatterie>=0.5.1"], "version": "2024.12.03" } From 44a3b0616926ebb14ac0286b54292511e4c05c1d Mon Sep 17 00:00:00 2001 From: stefan Date: Tue, 31 Dec 2024 03:02:34 +0100 Subject: [PATCH 12/15] - No more timeouts when retrieving values - Properly support device selection on actions (device_id is now mandatory!) - Support multiple SonnenBatterie instances (I even tested this ;)) Signed-off-by: stefan --- custom_components/sonnenbatterie/__init__.py | 93 ++++++++++++++----- .../sonnenbatterie/config_flow.py | 2 +- custom_components/sonnenbatterie/const.py | 2 + .../sonnenbatterie/coordinator.py | 9 +- .../sonnenbatterie/manifest.json | 2 +- custom_components/sonnenbatterie/sensor.py | 6 +- .../sonnenbatterie/services.yaml | 36 +++++++ 7 files changed, 115 insertions(+), 35 deletions(-) diff --git a/custom_components/sonnenbatterie/__init__.py b/custom_components/sonnenbatterie/__init__.py index eecc7d3..6d23a87 100644 --- a/custom_components/sonnenbatterie/__init__.py +++ b/custom_components/sonnenbatterie/__init__.py @@ -1,26 +1,32 @@ """The Sonnenbatterie integration.""" import json -import voluptuous as vol +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - Platform + Platform, + ATTR_DEVICE_ID, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, ) from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse, ) - -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import ( + async_get as dr_async_get, +) +from homeassistant.util.read_only_dict import ReadOnlyDict from sonnenbatterie import AsyncSonnenBatterie from timeofuse import TimeofUseSchedule from .const import * - # rustydust_241227: this doesn't seem to be needed - kept until we're sure ;) # async def async_setup(hass, config): # """Set up a skeleton component.""" @@ -65,10 +71,10 @@ ) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - LOGGER.info(f"setup_entry: {config_entry.data}\n{config_entry.options}") - ip_address = config_entry.data["ip_address"] - password = config_entry.data["password"] - username = config_entry.data["username"] + LOGGER.debug(f"setup_entry: {config_entry.data}\n{config_entry.entry_id}") + ip_address = config_entry.data[CONF_IP_ADDRESS] + password = config_entry.data[CONF_PASSWORD] + username = config_entry.data[CONF_USERNAME] sb = AsyncSonnenBatterie(username, password, ip_address) await sb.login() @@ -83,34 +89,65 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b } ) + # Set up base data in hass object + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = {} + hass.data[DOMAIN][config_entry.entry_id][CONF_IP_ADDRESS] = ip_address + hass.data[DOMAIN][config_entry.entry_id][CONF_USERNAME] = username + hass.data[DOMAIN][config_entry.entry_id][CONF_PASSWORD] = password + await hass.config_entries.async_forward_entry_setups(config_entry, [ Platform.SENSOR ]) # rustydust_241227: this doesn't seem to be needed # config_entry.add_update_listener(update_listener) # config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + def _get_sb_connection(call_data: ReadOnlyDict) -> AsyncSonnenBatterie: + LOGGER.debug(f"_get_sb_connection: {call_data}") + if ATTR_DEVICE_ID in call_data: + # no idea why, but sometimes it's a list and other times a str + if isinstance(call_data[ATTR_DEVICE_ID], list): + device_id = call_data[ATTR_DEVICE_ID][0] + else: + device_id = call_data[ATTR_DEVICE_ID] + device_registry = dr_async_get(hass) + if not (device_entry := device_registry.async_get(device_id)): + raise HomeAssistantError(f"No device found for device_id: {device_id}") + if not (sb_config := hass.data[DOMAIN][device_entry.primary_config_entry]): + raise HomeAssistantError(f"Unable to find config for device_id: {device_id} ({device_entry.name})") + if not (sb_config.get(CONF_USERNAME) and sb_config.get(CONF_PASSWORD) and sb_config.get(CONF_IP_ADDRESS)): + raise HomeAssistantError(f"Invalid config for device_id: {device_id} ({sb_config}). Please report an issue at {SONNENBATTERIE_ISSUE_URL}.") + return AsyncSonnenBatterie(sb_config.get(CONF_USERNAME), sb_config.get(CONF_PASSWORD), sb_config.get(CONF_IP_ADDRESS)) + else: + return sb # service definitions async def charge_battery(call: ServiceCall) -> ServiceResponse: power = int(call.data.get(CONF_CHARGE_WATT)) # Make sure we have an sb2 object - await sb.login() - response = await sb.sb2.charge_battery(power) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.charge_battery(power) + await sb_conn.logout() return { "charge": response, } async def discharge_battery(call: ServiceCall) -> ServiceResponse: power = int(call.data.get(CONF_CHARGE_WATT)) - await sb.login() - response = await sb.sb2.discharge_battery(power) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.discharge_battery(power) + await sb_conn.logout() return { "discharge": response, } async def set_battery_reserve(call: ServiceCall) -> ServiceResponse: value = call.data.get(CONF_SERVICE_VALUE) - await sb.login() - response = int((await sb.sb2.set_battery_reserve(value))["EM_USOC"]) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = int((await sb_conn.sb2.set_battery_reserve(value))["EM_USOC"]) + await sb_conn.logout() return { "battery_reserve": response, } @@ -118,16 +155,20 @@ async def set_battery_reserve(call: ServiceCall) -> ServiceResponse: async def set_config_item(call: ServiceCall) -> ServiceResponse: item = call.data.get(CONF_SERVICE_ITEM) value = call.data.get(CONF_SERVICE_VALUE) - await sb.login() - response = await sb.sb2.set_config_item(item, value) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.set_config_item(item, value) + await sb_conn.logout() return { "response": response, } async def set_operating_mode(call: ServiceCall) -> ServiceResponse: mode = SB_OPERATING_MODES.get(call.data.get('mode')) - await sb.login() - response = await sb.set_operating_mode(mode) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.set_operating_mode(mode) + await sb_conn.logout() return { "mode": response, } @@ -147,16 +188,20 @@ async def set_tou_schedule(call: ServiceCall) -> ServiceResponse: except TypeError as t: raise HomeAssistantError(f"Schedule is not a valid schedule: '{schedule}'") from t - await sb.login() - response = await sb.sb2.set_tou_schedule_string(schedule) + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.set_tou_schedule_string(schedule) + await sb_conn.logout() return { "schedule": response["EM_ToU_Schedule"], } # noinspection PyUnusedLocal async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: - await sb.login() - response = await sb.sb2.get_tou_schedule_string() + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.get_tou_schedule_string() + await sb_conn.logout() return { "schedule": response, } @@ -236,5 +281,5 @@ async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: async def async_unload_entry(hass, entry): """Handle removal of an entry.""" - LOGGER.info(f"Unloading config entry: {entry}") + LOGGER.debug(f"Unloading config entry: {entry}") return await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR) diff --git a/custom_components/sonnenbatterie/config_flow.py b/custom_components/sonnenbatterie/config_flow.py index 91fea07..7838bcd 100644 --- a/custom_components/sonnenbatterie/config_flow.py +++ b/custom_components/sonnenbatterie/config_flow.py @@ -104,7 +104,7 @@ async def async_step_reconfigure(self, user_input): ) if user_input is not None: - LOGGER.info(f"Reconfiguring {entry}") + LOGGER.debug(f"Reconfiguring {entry}") if entry.data.get(CONF_SERIAL_NUMBER): await self.async_set_unique_id(entry.data[CONF_SERIAL_NUMBER]) self._abort_if_unique_id_configured() diff --git a/custom_components/sonnenbatterie/const.py b/custom_components/sonnenbatterie/const.py index f2338d3..81f4d68 100644 --- a/custom_components/sonnenbatterie/const.py +++ b/custom_components/sonnenbatterie/const.py @@ -1,5 +1,7 @@ import logging +SONNENBATTERIE_ISSUE_URL = "https://github.com/weltmeyer/ha_sonnenbatterie/issues" + CONF_SERIAL_NUMBER = "serial_number" ATTR_SONNEN_DEBUG = "sonnenbatterie_debug" diff --git a/custom_components/sonnenbatterie/coordinator.py b/custom_components/sonnenbatterie/coordinator.py index 96a5337..cdf4532 100644 --- a/custom_components/sonnenbatterie/coordinator.py +++ b/custom_components/sonnenbatterie/coordinator.py @@ -13,9 +13,6 @@ from .const import DOMAIN, LOGGER, logging -_LOGGER = logging.getLogger(__name__) - - class SonnenBatterieCoordinator(DataUpdateCoordinator): """The SonnenBatterieCoordinator class.""" @@ -31,7 +28,7 @@ def __init__( """Initialize my coordinator.""" super().__init__( hass, - _LOGGER, + LOGGER, # Name of the data. For logging purposes. name=f"sonnenbatterie-{device_id}", # Polling interval. Will only be polled if there are subscribers. @@ -103,10 +100,10 @@ async def _async_update_data(self): self.latestData["system_data"] = await result except Exception as ex: - LOGGER.info(traceback.format_exc()) if self._last_error is not None: + LOGGER.info(traceback.format_exc() + " ... might be maintenance window") elapsed = time() - self._last_error - if elapsed > timedelta(seconds=180).total_seconds(): + if elapsed > 180: LOGGER.error(f"Unable to connecto to Sonnenbatteries at {self.ip_address} for {elapsed} seconds. Please check! [{ex}]") else: self._last_error = time() diff --git a/custom_components/sonnenbatterie/manifest.json b/custom_components/sonnenbatterie/manifest.json index a8f3962..3daa6e5 100644 --- a/custom_components/sonnenbatterie/manifest.json +++ b/custom_components/sonnenbatterie/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/weltmeyer/ha_sonnenbatterie", "iot_class": "local_polling", "issue_tracker": "https://github.com/weltmeyer/ha_sonnenbatterie/issues", - "requirements": ["requests","sonnenbatterie>=0.5.1"], + "requirements": ["requests","sonnenbatterie>=0.5.2"], "version": "2024.12.03" } diff --git a/custom_components/sonnenbatterie/sensor.py b/custom_components/sonnenbatterie/sensor.py index 29d0290..994a801 100644 --- a/custom_components/sonnenbatterie/sensor.py +++ b/custom_components/sonnenbatterie/sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the sensor platform.""" - LOGGER.info("SETUP_ENTRY") + LOGGER.debug("SETUP_ENTRY") username = config_entry.data.get(CONF_USERNAME) password = config_entry.data.get(CONF_PASSWORD) ip_address = config_entry.data.get(CONF_IP_ADDRESS) @@ -51,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) update_interval_seconds = update_interval_seconds or DEFAULT_SCAN_INTERVAL - LOGGER.info("{0} - UPDATEINTERVAL: {1}".format(DOMAIN, update_interval_seconds)) + LOGGER.debug(f"{DOMAIN} - UPDATEINTERVAL: {update_interval_seconds}") """ The Coordinator is called from HA for updates from API """ coordinator = SonnenBatterieCoordinator( @@ -79,7 +79,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for description in generate_powermeter_sensors(_coordinator=coordinator) ) - LOGGER.info("Init done") + LOGGER.debug("Init done") return True diff --git a/custom_components/sonnenbatterie/services.yaml b/custom_components/sonnenbatterie/services.yaml index d542abf..99cb357 100644 --- a/custom_components/sonnenbatterie/services.yaml +++ b/custom_components/sonnenbatterie/services.yaml @@ -1,5 +1,10 @@ set_operating_mode: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie mode: required: true example: "timeofuse" @@ -8,6 +13,11 @@ set_operating_mode: text: charge_battery: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie power: required: true example: "1000" @@ -15,6 +25,11 @@ charge_battery: number: discharge_battery: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie power: required: true example: "1000" @@ -22,6 +37,11 @@ discharge_battery: number: set_battery_reserve: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie value: required: true selector: @@ -30,6 +50,11 @@ set_battery_reserve: max: 100 set_config_item: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie item: required: true example: "EM_USOC" @@ -42,9 +67,20 @@ set_config_item: text: set_tou_schedule: fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie schedule: required: true example: "[{\"start\":\"10:00\", \"stop\":\"11:00\", \"threshold_p_max\": 20000 }]" selector: text: get_tou_schedule: + fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie From 9859776155ba5c59ab396d704a521291b1e42e2f Mon Sep 17 00:00:00 2001 From: stefan Date: Tue, 31 Dec 2024 03:11:19 +0100 Subject: [PATCH 13/15] It's always the translations ... Signed-off-by: stefan --- .../sonnenbatterie/translations/de.json | 39 ++++++++++++++++++- .../sonnenbatterie/translations/en.json | 39 ++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/custom_components/sonnenbatterie/translations/de.json b/custom_components/sonnenbatterie/translations/de.json index c0da15e..01d21e3 100644 --- a/custom_components/sonnenbatterie/translations/de.json +++ b/custom_components/sonnenbatterie/translations/de.json @@ -155,6 +155,11 @@ "name": "Setze Sonnenbatterie-Betriebsmodus", "description": "Setzt den Betriebsmodus der Sonnenbatterie ('manual', 'automatic', 'timeofuse')", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "mode": { "description": "Der zu setzende Betriebsmodus", "name": "Betriebsmodus", @@ -166,6 +171,11 @@ "name": "Sonnenbatterie laden", "description": "Erzwingt das Laden der Sonnenbatterie mit der angegebenen Leistung", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "power": { "name": "Lade-Leistung in Watt", "description": "Leistung, mit der die Sonnenbatterie geladen werden soll", @@ -177,6 +187,11 @@ "name": "Sonnenbatterie entladen", "description": "Erzwingt die Abgabe von Leistung der Sonnenbatterie an Verbraucher", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "power": { "name": "Entlade-Leistung in Watt", "description": "Leistung, die aus der Sonnebatterie an Verbraucher abgegehen wird", @@ -188,6 +203,11 @@ "name": "Reservekapazität setzen", "description": "Setzt die von der Sonnebatterie zurückgehaltenene Kapazität in Prozent", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "value": { "name": "Zurückgehaltene Kapazität", "description": "Kapazität in Prozent, die die Sonnenbatterie auf jeden Fall reservieren soll", @@ -199,6 +219,11 @@ "name": "Setzen eines Konfigurationswerts der Sonnenbatterie", "description": "Erlaubt das Setzen einzelner Konfigurationswerte und Betriebsparameter der Sonnenbatterie", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "item": { "name": "Zu setzender Parameter", "description": "Der Name des zu setzenden Parameters der Sonnenbatterie", @@ -215,6 +240,11 @@ "name": "Ladefenster festlegen", "description": "Erlaubt das Setzen von Ladenfenstern im zeitgesteuerten Betriebsmodus", "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + }, "schedule": { "name": "Zeitfenster (JSON-Array)", "description": "Ein oder mehrere Zeitfenster-Angaben als JSON-Array im String-Format", @@ -224,7 +254,14 @@ }, "get_tou_schedule": { "name": "Auslesen der Lade-Zeitfenster", - "description": "Liefert die aktuell in der Sonnebatterie hinterlegten Zeitfenster für das Laden im zeitgesteuerten Modus" + "description": "Liefert die aktuell in der Sonnebatterie hinterlegten Zeitfenster für das Laden im zeitgesteuerten Modus", + "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + } + } } } } diff --git a/custom_components/sonnenbatterie/translations/en.json b/custom_components/sonnenbatterie/translations/en.json index 90f7182..dc9d107 100644 --- a/custom_components/sonnenbatterie/translations/en.json +++ b/custom_components/sonnenbatterie/translations/en.json @@ -155,6 +155,11 @@ "name": "Set operating mode", "description": "Sets the operating mode of the Sonnenbatterie", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "mode": { "description": "Operating mode to set ('manual', 'automatic', 'timeofuse')", "name": "Operating mode", @@ -166,6 +171,11 @@ "name": "Charge battery", "description": "Forces the charging of the Sonnenbatterie with the specified power in W", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "power": { "name": "Charging power", "description": "Power to charge the Sonnenbatterie with", @@ -177,6 +187,11 @@ "name": "Discharge battery", "description": "Forces the discharge of specified power from the Sonnenbatterie", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "power": { "name": "Discharging power", "description": "Power to discharge the Sonnenbatterie with", @@ -188,6 +203,11 @@ "name": "Set backup capacity", "description": "Sets the backup capacity the Sonnenbatterie keeps back in percent of the total", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "value": { "name": "Kept back capacity", "description": "Precentage of the total capacity that should be kept back", @@ -199,6 +219,11 @@ "name": "Set a config parameter", "description": "Allows changing of some of the Sonnenbatterie's operating paramters", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "item": { "name": "Parameter name", "description": "Name of the parameter that should be set", @@ -215,6 +240,11 @@ "name": "Set charging schedule(s)", "description": "Allows to set charging schedules that are honored when the Sonnenbattery is in Time-of-Use mode", "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + }, "schedule": { "name": "Charging window(s)", "description": "One or more charging windows as string containing a JSON array", @@ -224,7 +254,14 @@ }, "get_tou_schedule": { "name": "Get charging schedule", - "description": "Returns the charging schedules currently stored in the Sonnenbatterie's configuration" + "description": "Returns the charging schedules currently stored in the Sonnenbatterie's configuration", + "fields": { + "device_id": { + "description": "HomeAssistant Id of the target device", + "name": "Device Id", + "example": "1234567890" + } + } } } } From 0d8c4ff622912fa7868a5981cc09d9b7eec98a82 Mon Sep 17 00:00:00 2001 From: stefan Date: Tue, 31 Dec 2024 15:35:12 +0100 Subject: [PATCH 14/15] Update requirements Signed-off-by: stefan --- README.md | 2 +- custom_components/sonnenbatterie/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d74071..4840ff3 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ are already activated. ## Actions -Since version 2024.12.03 this integration also supports actions you can use to +Since version 2025.01.01 this integration also supports actions you can use to set some variables that influence the behaviour of your SonnenBatterie. Currently supported actions are: diff --git a/custom_components/sonnenbatterie/manifest.json b/custom_components/sonnenbatterie/manifest.json index 3daa6e5..50bd714 100644 --- a/custom_components/sonnenbatterie/manifest.json +++ b/custom_components/sonnenbatterie/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/weltmeyer/ha_sonnenbatterie/issues", "requirements": ["requests","sonnenbatterie>=0.5.2"], - "version": "2024.12.03" + "version": "2025.01.01" } From e22c4daa728540903b6043040fe02717e7eb2c8a Mon Sep 17 00:00:00 2001 From: Stefan Rubner Date: Tue, 31 Dec 2024 16:43:06 +0100 Subject: [PATCH 15/15] Add convenience actions: - `get_battery_reserve` - `get_operating_mode` > [!WARNING] > Untested since I'm on the road Signed-off-by: Stefan Rubner --- custom_components/sonnenbatterie/__init__.py | 33 +++++++++++++++++++ .../sonnenbatterie/services.yaml | 14 ++++++++ .../sonnenbatterie/translations/de.json | 22 +++++++++++++ .../sonnenbatterie/translations/en.json | 24 +++++++++++++- 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/custom_components/sonnenbatterie/__init__.py b/custom_components/sonnenbatterie/__init__.py index 6d23a87..eb9a50d 100644 --- a/custom_components/sonnenbatterie/__init__.py +++ b/custom_components/sonnenbatterie/__init__.py @@ -206,6 +206,25 @@ async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: "schedule": response, } + # noinspection PyUnusedLocal + async def get_battery_reserve(call: ServiceCall) -> ServiceResponse: + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.get_battery_reserve() + await sb_conn.logout() + return { + "backup_reserve": response, + } + + async def get_operating_mode(call: ServiceCall) -> ServiceResponse: + sb_conn = _get_sb_connection(call.data) + await sb_conn.login() + response = await sb_conn.sb2.get_operating_mode() + await sb_conn.logout() + return { + "operating_mode": response, + } + # service registration hass.services.async_register( DOMAIN, @@ -262,6 +281,20 @@ async def get_tou_schedule(call: ServiceCall) -> ServiceResponse: supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + "get_battery_reserve", + get_battery_reserve, + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + "get_operating_mode", + get_operating_mode, + supports_response=SupportsResponse.ONLY, + ) + # Done setting up the entry return True diff --git a/custom_components/sonnenbatterie/services.yaml b/custom_components/sonnenbatterie/services.yaml index 99cb357..1ff6d88 100644 --- a/custom_components/sonnenbatterie/services.yaml +++ b/custom_components/sonnenbatterie/services.yaml @@ -84,3 +84,17 @@ get_tou_schedule: selector: device: integration: sonnenbatterie +get_battery_reserve: + fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie +get_operating_mode: + fields: + device_id: + required: true + selector: + device: + integration: sonnenbatterie diff --git a/custom_components/sonnenbatterie/translations/de.json b/custom_components/sonnenbatterie/translations/de.json index 01d21e3..a357219 100644 --- a/custom_components/sonnenbatterie/translations/de.json +++ b/custom_components/sonnenbatterie/translations/de.json @@ -262,6 +262,28 @@ "example": "1234567890" } } + }, + "get_battery_reserve": { + "name": "Auslesen der Batterie-Reserve", + "description": "Liefert die von der Sonnebatterie als Reserve zurückgehaltene Kapazität in Prozent", + "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + } + } + }, + "get_operating_mode": { + "name": "Betriebsmodus auslesen", + "description": "Liefert den aktuellen Betriebsmodus der Sonnebatterie in numerischer Form", + "fields": { + "device_id": { + "description": "HomeAssistant ID des Geräts", + "name": "Device ID", + "example": "1234567890" + } + } } } } diff --git a/custom_components/sonnenbatterie/translations/en.json b/custom_components/sonnenbatterie/translations/en.json index dc9d107..a90eeac 100644 --- a/custom_components/sonnenbatterie/translations/en.json +++ b/custom_components/sonnenbatterie/translations/en.json @@ -262,6 +262,28 @@ "example": "1234567890" } } + }, + "get_battery_reserve": { + "name": "Get battery reserve", + "description": "Returns the percentage of capacity the Sonnebatterie keeps back.", + "fields": { + "device_id": { + "description": "HomeAssistant ID of the target device", + "name": "Device ID", + "example": "1234567890" + } + } + }, + "get_operating_mode": { + "name": "Get operating mode", + "description": "Returns the current operating mode of the SonnenBatterie in numeric form", + "fields": { + "device_id": { + "description": "HomeAssistant ID of the target device", + "name": "Device ID", + "example": "1234567890" + } + } } - } +} }