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..4840ff3 100644
--- a/README.md
+++ b/README.md
@@ -3,36 +3,233 @@ 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/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 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.
+
+> [!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 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:
+
+### `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..eb9a50d 100644
--- a/custom_components/sonnenbatterie/__init__.py
+++ b/custom_components/sonnenbatterie/__init__.py
@@ -2,34 +2,307 @@
import json
+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,
+)
+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."""
# 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.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()
+ inverter_power = int((await sb.get_batterysystem())['battery_system']['system']['inverter_capacity'])
+ await sb.logout()
+
+ # noinspection PyPep8Naming
+ SCHEMA_CHARGE_BATTERY = vol.Schema(
+ {
+ **cv.ENTITY_SERVICE_FIELDS,
+ vol.Required(CONF_CHARGE_WATT): vol.Range(min=0, max=inverter_power),
+ }
+ )
+
+ # 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
-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
+ # 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
+ 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))
+ 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 async_reload_entry(hass, entry):
- """Reload config entry."""
- await async_unload_entry(hass, entry)
- await async_setup_entry(hass, entry)
+ async def set_battery_reserve(call: ServiceCall) -> ServiceResponse:
+ value = call.data.get(CONF_SERVICE_VALUE)
+ 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,
+ }
+
+ async def set_config_item(call: ServiceCall) -> ServiceResponse:
+ item = call.data.get(CONF_SERVICE_ITEM)
+ value = call.data.get(CONF_SERVICE_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'))
+ 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,
+ }
+
+ 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
+
+ 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:
+ 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,
+ }
+
+ # 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,
+ "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,
+ )
+
+ 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
+# 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):
@@ -41,4 +314,5 @@ async def async_reload_entry(hass, entry):
async def async_unload_entry(hass, entry):
"""Handle removal of an 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 6f7a3fd..7838bcd 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
@@ -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): cv.positive_int,
+ 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.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()
@@ -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})",
)
@@ -133,9 +142,12 @@ 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)
+ 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/const.py b/custom_components/sonnenbatterie/const.py
index a679449..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"
@@ -10,6 +12,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..cdf4532 100644
--- a/custom_components/sonnenbatterie/coordinator.py
+++ b/custom_components/sonnenbatterie/coordinator.py
@@ -9,20 +9,17 @@
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
-from sonnenbatterie import sonnenbatterie
+from sonnenbatterie import AsyncSonnenBatterie
from .const import DOMAIN, LOGGER, logging
-_LOGGER = logging.getLogger(__name__)
-
-
class SonnenBatterieCoordinator(DataUpdateCoordinator):
"""The SonnenBatterieCoordinator class."""
def __init__(
self,
hass: HomeAssistant,
- sb_inst: sonnenbatterie,
+ sb_inst: AsyncSonnenBatterie,
update_interval_seconds: int,
ip_address,
debug_mode,
@@ -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.
@@ -45,7 +42,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,31 +82,29 @@ 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:
- e = traceback.format_exc()
- LOGGER.error(e)
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():
- LOGGER.warning(f"Unable to connecto to Sonnenbatteries at {self.ip_address} for {elapsed} seconds. Please check! [{ex}]")
+ 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()
@@ -141,9 +136,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..50bd714 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.2"],
+ "version": "2025.01.01"
}
diff --git a/custom_components/sonnenbatterie/sensor.py b/custom_components/sonnenbatterie/sensor.py
index feae883..994a801 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,
@@ -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."""
@@ -42,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)
@@ -50,11 +47,11 @@ 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
- 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(
@@ -82,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
new file mode 100644
index 0000000..1ff6d88
--- /dev/null
+++ b/custom_components/sonnenbatterie/services.yaml
@@ -0,0 +1,100 @@
+set_operating_mode:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: sonnenbatterie
+ mode:
+ required: true
+ example: "timeofuse"
+ default: "automatic"
+ selector:
+ text:
+charge_battery:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: sonnenbatterie
+ power:
+ required: true
+ example: "1000"
+ selector:
+ number:
+discharge_battery:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: sonnenbatterie
+ power:
+ required: true
+ example: "1000"
+ selector:
+ number:
+set_battery_reserve:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: sonnenbatterie
+ value:
+ required: true
+ selector:
+ number:
+ min: 0
+ max: 100
+set_config_item:
+ fields:
+ device_id:
+ required: true
+ selector:
+ device:
+ integration: sonnenbatterie
+ item:
+ required: true
+ example: "EM_USOC"
+ selector:
+ text:
+ value:
+ required: true
+ example: "15"
+ selector:
+ 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
+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 a5da7e0..a357219 100644
--- a/custom_components/sonnenbatterie/translations/de.json
+++ b/custom_components/sonnenbatterie/translations/de.json
@@ -149,5 +149,141 @@
"name": "Spannungswandler UPV 2"
}
}
+ },
+ "services": {
+ "set_operating_mode": {
+ "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",
+ "example": "automatic"
+ }
+ }
+ },
+ "charge_battery": {
+ "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",
+ "example": "1000"
+ }
+ }
+ },
+ "discharge_battery": {
+ "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",
+ "example": "1234"
+ }
+ }
+ },
+ "set_battery_reserve": {
+ "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",
+ "example": "10"
+ }
+ }
+ },
+ "set_config_item": {
+ "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",
+ "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": {
+ "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",
+ "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",
+ "fields": {
+ "device_id": {
+ "description": "HomeAssistant ID des Geräts",
+ "name": "Device ID",
+ "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 5081da7..a90eeac 100644
--- a/custom_components/sonnenbatterie/translations/en.json
+++ b/custom_components/sonnenbatterie/translations/en.json
@@ -149,5 +149,141 @@
"name": "Inverter UPV 2"
}
}
- }
+ },
+ "services": {
+ "set_operating_mode": {
+ "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",
+ "example": "automatic"
+ }
+ }
+ },
+ "charge_battery": {
+ "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",
+ "example": "1000"
+ }
+ }
+ },
+ "discharge_battery": {
+ "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",
+ "example": "1234"
+ }
+ }
+ },
+ "set_battery_reserve": {
+ "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",
+ "example": "10"
+ }
+ }
+ },
+ "set_config_item": {
+ "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",
+ "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": {
+ "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",
+ "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",
+ "fields": {
+ "device_id": {
+ "description": "HomeAssistant Id of the target device",
+ "name": "Device Id",
+ "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"
+ }
+ }
+ }
+}
}