diff --git a/packages/modules/devices/sonnen/sonnenbatterie/bat.py b/packages/modules/devices/sonnen/sonnenbatterie/bat.py index c22ce2dd4e..aa075eac4b 100644 --- a/packages/modules/devices/sonnen/sonnenbatterie/bat.py +++ b/packages/modules/devices/sonnen/sonnenbatterie/bat.py @@ -1,20 +1,22 @@ #!/usr/bin/env python3 +from typing import Any, TypedDict, Dict, Optional import logging -from typing import Any, TypedDict -from modules.common import req -from modules.common.abstract_device import AbstractBat -from modules.common.component_state import BatState -from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.simcount import SimCounter -from modules.common.store import get_bat_value_store from modules.devices.sonnen.sonnenbatterie.config import SonnenbatterieBatSetup +from modules.common.store import get_bat_value_store +from modules.common.simcount import SimCounter +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.component_type import ComponentDescriptor +from modules.common.component_state import BatState +from modules.common.abstract_device import AbstractBat +from modules.common import req + log = logging.getLogger(__name__) class KwargsDict(TypedDict): + api_v2_token: str device_id: int device_address: str device_variant: int @@ -29,6 +31,7 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__device_address: str = self.kwargs['device_address'] self.__device_variant: int = self.kwargs['device_variant'] + self.__api_v2_token: Optional[str] = self.kwargs.get('api_v2_token') self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") self.store = get_bat_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) @@ -49,9 +52,11 @@ def __update_variant_0(self) -> BatState: soc=battery_soc ) - def __read_variant_1(self, api: str = "v1"): + def __read_variant_1(self, api: str = "v1", target: str = "status") -> Dict: return req.get_http_session().get( - "http://" + self.__device_address + "/api/" + api + "/status", timeout=5 + f"http://{self.__device_address}/api/{api}/{target}", + timeout=5, + headers={"Auth-Token": self.__api_v2_token} if api == "v2" else None ).json() def __update_variant_1(self, api: str = "v1") -> BatState: @@ -105,6 +110,32 @@ def __update_variant_1(self, api: str = "v1") -> BatState: exported=exported ) + def __get_json_api_v2_configurations(self) -> Dict: + if self.__device_variant != 3: + raise ValueError("JSON API v2 wird nur für Variante 3 unterstützt!") + return self.__read_variant_1("v2", "configurations") + + def __set_json_api_v2_configurations(self, configuration: Dict) -> None: + if self.__device_variant != 3: + raise ValueError("JSON API v2 wird nur für Variante 3 unterstützt!") + req.get_http_session().put( + f"http://{self.__device_address}/api/v2/configurations", + json=configuration, + headers={"Auth-Token": self.__api_v2_token} + ) + + def __set_json_api_v2_setpoint(self, power_limit: int) -> None: + if self.__device_variant != 3: + raise ValueError("JSON API v2 wird nur für Variante 3 unterstützt!") + command = "charge" + if power_limit < 0: + command = "discharge" + power_limit = -power_limit + req.get_http_session().post( + f"http://{self.__device_address}/api/v2/setpoint/{command}/{power_limit}", + headers={"Auth-Token": self.__api_v2_token, "Content-Type": "application/json"} + ) + def __read_variant_2_element(self, element: str) -> str: response = req.get_http_session().get( 'http://' + self.__device_address + ':7979/rest/devices/battery/' + element, @@ -137,5 +168,27 @@ def update(self) -> None: raise ValueError("Unbekannte Variante: " + str(self.__device_variant)) self.store.set(state) + def set_power_limit(self, power_limit: Optional[int]) -> None: + if self.__device_variant != 3: + raise ValueError("Leistungsvorgabe wird nur für Variante 'JSON-API v2' unterstützt!") + operating_mode = self.__get_json_api_v2_configurations()["EM_OperatingMode"] + log.debug(f"Betriebsmodus: aktuell: {operating_mode}") + if power_limit is None: + # Keine Leistungsvorgabe, Betriebsmodus "Eigenverbrauch" aktivieren + if operating_mode == "1": + log.debug("Keine Leistungsvorgabe, aktiviere normale Steuerung durch den Speicher") + self.__set_json_api_v2_configurations({"EM_OperatingMode": "2"}) + else: + # Leistungsvorgabe, Betriebsmodus "Manuell" aktivieren + if operating_mode == "2": + log.debug(f"Leistungsvorgabe: {power_limit}, aktiviere manuelle Steuerung durch openWB") + self.__set_json_api_v2_configurations({"EM_OperatingMode": "1"}) + log.debug(f"Setze Leistungsvorgabe auf: {power_limit}") + self.__set_json_api_v2_setpoint(power_limit) + + def power_limit_controllable(self) -> bool: + # Leistungsvorgabe ist nur für Variante 3 (JSON-API v2) möglich + return self.__device_variant == 3 + component_descriptor = ComponentDescriptor(configuration_factory=SonnenbatterieBatSetup) diff --git a/packages/modules/devices/sonnen/sonnenbatterie/config.py b/packages/modules/devices/sonnen/sonnenbatterie/config.py index ea0df8814e..bf9054ed23 100644 --- a/packages/modules/devices/sonnen/sonnenbatterie/config.py +++ b/packages/modules/devices/sonnen/sonnenbatterie/config.py @@ -5,9 +5,10 @@ class SonnenBatterieConfiguration: - def __init__(self, variant: int = 0, ip_address: Optional[str] = None): + def __init__(self, variant: int = 0, ip_address: Optional[str] = None, api_v2_token: Optional[str] = None): self.variant = variant self.ip_address = ip_address + self.api_v2_token = api_v2_token class SonnenBatterie: @@ -44,13 +45,27 @@ def __init__(self): class SonnenbatterieCounterSetup(ComponentSetup[SonnenbatterieCounterConfiguration]): def __init__(self, - name: str = "SonnenBatterie Zähler", + name: str = "SonnenBatterie EVU-Zähler", type: str = "counter", id: int = 0, configuration: SonnenbatterieCounterConfiguration = None) -> None: super().__init__(name, type, id, configuration or SonnenbatterieCounterConfiguration()) +class SonnenbatterieConsumptionCounterConfiguration: + def __init__(self): + pass + + +class SonnenbatterieConsumptionCounterSetup(ComponentSetup[SonnenbatterieCounterConfiguration]): + def __init__(self, + name: str = "SonnenBatterie Verbrauchs-Zähler", + type: str = "counter_consumption", + id: int = 0, + configuration: SonnenbatterieConsumptionCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or SonnenbatterieConsumptionCounterConfiguration()) + + class SonnenbatterieInverterConfiguration: def __init__(self): pass diff --git a/packages/modules/devices/sonnen/sonnenbatterie/counter.py b/packages/modules/devices/sonnen/sonnenbatterie/counter.py index 5a0180ca96..a91d6358f3 100644 --- a/packages/modules/devices/sonnen/sonnenbatterie/counter.py +++ b/packages/modules/devices/sonnen/sonnenbatterie/counter.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import logging -from typing import TypedDict, Any +from typing import Optional, TypedDict, Any from modules.common import req from modules.common.abstract_device import AbstractCounter @@ -15,6 +15,7 @@ class KwargsDict(TypedDict): + api_v2_token: str device_id: int device_address: str device_variant: int @@ -29,13 +30,16 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__device_address: str = self.kwargs['device_address'] self.__device_variant: int = self.kwargs['device_variant'] + self.__api_v2_token: Optional[str] = self.kwargs.get('api_v2_token') self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") self.store = get_counter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def __read_variant_1(self, api: str = "v1"): return req.get_http_session().get( - "http://" + self.__device_address + "/api/" + api + "/status", timeout=5 + "http://" + self.__device_address + "/api/" + api + "/status", + timeout=5, + headers={"Auth-Token": self.__api_v2_token} if api == "v2" else None ).json() def __update_variant_1(self, api: str = "v1") -> CounterState: diff --git a/packages/modules/devices/sonnen/sonnenbatterie/counter_consumption.py b/packages/modules/devices/sonnen/sonnenbatterie/counter_consumption.py new file mode 100644 index 0000000000..3b46066f35 --- /dev/null +++ b/packages/modules/devices/sonnen/sonnenbatterie/counter_consumption.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +import logging +from typing import Dict, Optional, Union + +from dataclass_utils import dataclass_from_dict +from modules.common import req +from modules.common.abstract_device import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store import get_counter_value_store +from modules.devices.sonnen.sonnenbatterie.config import SonnenbatterieConsumptionCounterSetup + +log = logging.getLogger(__name__) + + +class SonnenbatterieConsumptionCounter(AbstractCounter): + def __init__(self, + device_address: str, + device_variant: int, + api_v2_token: Optional[str], + component_config: Union[Dict, SonnenbatterieConsumptionCounterSetup]) -> None: + self.__device_address = device_address + self.__device_variant = device_variant + self.__api_v2_token = api_v2_token + self.component_config = dataclass_from_dict(SonnenbatterieConsumptionCounterSetup, component_config) + self.store = get_counter_value_store(self.component_config.id) + self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) + + def __read_variant_3(self): + result = req.get_http_session().get( + "http://" + self.__device_address + "/api/v2/powermeter", + timeout=5, + headers={"Auth-Token": self.__api_v2_token} + ).json() + for channel in result: + if channel["direction"] == "consumption": + return channel + raise ValueError("No consumption channel found") + + def __update_variant_3(self) -> CounterState: + # Auslesen einer Sonnenbatterie 8 oder 10 über die integrierte JSON-API v2 des Batteriesystems + ''' + example data: + [ + { + "a_l1": 0, + "a_l2": 0, + "a_l3": 0, + "channel": 1, + "deviceid": 4, + "direction": "production", + "error": -1, + "kwh_exported": 0, + "kwh_imported": 0, + "v_l1_l2": 0, + "v_l1_n": 0, + "v_l2_l3": 0, + "v_l2_n": 0, + "v_l3_l1": 0, + "v_l3_n": 0, + "va_total": 0, + "var_total": 0, + "w_l1": 0, + "w_l2": 0, + "w_l3": 0, + "w_total": 0 + }, + { + "a_l1": 0, + "a_l2": 0, + "a_l3": 0, + "channel": 2, + "deviceid": 4, + "direction": "consumption", + "error": -1, + "kwh_exported": 0, + "kwh_imported": 0, + "v_l1_l2": 0, + "v_l1_n": 0, + "v_l2_l3": 0, + "v_l2_n": 0, + "v_l3_l1": 0, + "v_l3_n": 0, + "va_total": 0, + "var_total": 0, + "w_l1": 0, + "w_l2": 0, + "w_l3": 0, + "w_total": 0 + } + ] + ''' + counter_state = self.__read_variant_3() + return CounterState( + power=counter_state["w_total"], + powers=[counter_state[f"w_l{phase}"] for phase in range(1, 4)], + currents=[counter_state[f"a_l{phase}"] for phase in range(1, 4)], + voltages=[counter_state[f"v_l{phase}_n"] for phase in range(1, 4)], + imported=counter_state["kwh_imported"], + exported=counter_state["kwh_exported"] + ) + + def update(self) -> None: + log.debug("Variante: " + str(self.__device_variant)) + if self.__device_variant in [0, 1, 2]: + log.debug("Diese Variante bietet keine Verbrauchsdaten!") + elif self.__device_variant == 3: + state = self.__update_variant_3() + else: + raise ValueError("Unbekannte Variante: " + str(self.__device_variant)) + self.store.set(state) + + +component_descriptor = ComponentDescriptor(configuration_factory=SonnenbatterieConsumptionCounterSetup) diff --git a/packages/modules/devices/sonnen/sonnenbatterie/device.py b/packages/modules/devices/sonnen/sonnenbatterie/device.py index 34eab8948c..0d0064ae8b 100644 --- a/packages/modules/devices/sonnen/sonnenbatterie/device.py +++ b/packages/modules/devices/sonnen/sonnenbatterie/device.py @@ -8,8 +8,10 @@ from modules.devices.sonnen.sonnenbatterie.bat import SonnenbatterieBat from modules.devices.sonnen.sonnenbatterie.config import (SonnenBatterie, SonnenbatterieBatSetup, SonnenbatterieCounterSetup, + SonnenbatterieConsumptionCounterSetup, SonnenbatterieInverterSetup) from modules.devices.sonnen.sonnenbatterie.counter import SonnenbatterieCounter +from modules.devices.sonnen.sonnenbatterie.counter_consumption import SonnenbatterieConsumptionCounter from modules.devices.sonnen.sonnenbatterie.inverter import SonnenbatterieInverter @@ -21,25 +23,35 @@ def create_bat_component(component_config: SonnenbatterieBatSetup): return SonnenbatterieBat(component_config, device_id=device_config.id, device_address=device_config.configuration.ip_address, - device_variant=device_config.configuration.variant) + device_variant=device_config.configuration.variant, + device_api_v2_token=device_config.configuration.api_v2_token) - def create_counter_component(component_config: SonnenbatterieCounterSetup): + def create_evu_counter_component(component_config: SonnenbatterieCounterSetup): return SonnenbatterieCounter(component_config, device_id=device_config.id, device_address=device_config.configuration.ip_address, - device_variant=device_config.configuration.variant) + device_variant=device_config.configuration.variant, + device_api_v2_token=device_config.configuration.api_v2_token) + + def create_consumption_counter_component(component_config: SonnenbatterieConsumptionCounterSetup): + return SonnenbatterieConsumptionCounter(device_config.configuration.ip_address, + device_config.configuration.variant, + device_config.configuration.api_v2_token, + component_config) def create_inverter_component(component_config: SonnenbatterieInverterSetup): return SonnenbatterieInverter(component_config, device_id=device_config.id, device_address=device_config.configuration.ip_address, - device_variant=device_config.configuration.variant) + device_variant=device_config.configuration.variant, + device_api_v2_token=device_config.configuration.api_v2_token) return ConfigurableDevice( device_config=device_config, component_factory=ComponentFactoryByType( bat=create_bat_component, - counter=create_counter_component, + counter=create_evu_counter_component, + counter_consumption=create_consumption_counter_component, inverter=create_inverter_component, ), component_updater=IndependentComponentUpdater(lambda component: component.update()) diff --git a/packages/modules/devices/sonnen/sonnenbatterie/inverter.py b/packages/modules/devices/sonnen/sonnenbatterie/inverter.py index fdf71f4290..f2f584a98e 100644 --- a/packages/modules/devices/sonnen/sonnenbatterie/inverter.py +++ b/packages/modules/devices/sonnen/sonnenbatterie/inverter.py @@ -1,20 +1,21 @@ #!/usr/bin/env python3 +from typing import Any, Optional, TypedDict import logging -from typing import Any, TypedDict - -from modules.common import req -from modules.common.abstract_device import AbstractInverter -from modules.common.component_state import InverterState -from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.simcount import SimCounter -from modules.common.store import get_inverter_value_store from modules.devices.sonnen.sonnenbatterie.config import SonnenbatterieInverterSetup +from modules.common.store import get_inverter_value_store +from modules.common.simcount import SimCounter +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.component_type import ComponentDescriptor +from modules.common.component_state import InverterState +from modules.common.abstract_device import AbstractInverter +from modules.common import req + log = logging.getLogger(__name__) class KwargsDict(TypedDict): + api_v2_token: str device_id: int device_address: str device_variant: int @@ -29,13 +30,16 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__device_address: str = self.kwargs['device_address'] self.__device_variant: int = self.kwargs['device_variant'] + self.__api_v2_token: Optional[str] = self.kwargs.get('api_v2_token') self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") self.store = get_inverter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def __read_variant_1(self, api: str = "v1"): return req.get_http_session().get( - "http://" + self.__device_address + "/api/" + api + "/status", timeout=5 + "http://" + self.__device_address + "/api/" + api + "/status", + timeout=5, + headers={"Auth-Token": self.__api_v2_token} if api == "v2" else None ).json() def __update_variant_1(self, api: str = "v1") -> InverterState: