Skip to content

Commit

Permalink
Merge pull request #2091 from benderl/sonnenbatterie
Browse files Browse the repository at this point in the history
Sonnenbatterie
  • Loading branch information
benderl authored Dec 19, 2024
2 parents 7799925 + 9e0456e commit 3308fb5
Show file tree
Hide file tree
Showing 244 changed files with 485 additions and 286 deletions.
54 changes: 51 additions & 3 deletions packages/modules/devices/sonnen/sonnenbatterie/bat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import logging
from typing import Dict, Union
from typing import Dict, Optional, Union

from dataclass_utils import dataclass_from_dict
from modules.common import req
Expand All @@ -20,10 +20,12 @@ def __init__(self,
device_id: int,
device_address: str,
device_variant: int,
api_v2_token: Optional[str],
component_config: Union[Dict, SonnenbatterieBatSetup]) -> None:
self.__device_id = device_id
self.__device_address = device_address
self.__device_variant = device_variant
self.__api_v2_token = api_v2_token
self.component_config = dataclass_from_dict(SonnenbatterieBatSetup, component_config)
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher")
self.store = get_bat_value_store(self.component_config.id)
Expand All @@ -45,9 +47,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:
Expand Down Expand Up @@ -101,6 +105,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,
Expand Down Expand Up @@ -133,5 +163,23 @@ 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 3 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)


component_descriptor = ComponentDescriptor(configuration_factory=SonnenbatterieBatSetup)
19 changes: 17 additions & 2 deletions packages/modules/devices/sonnen/sonnenbatterie/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/modules/devices/sonnen/sonnenbatterie/counter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import logging
from typing import Dict, Union
from typing import Dict, Optional, Union

from dataclass_utils import dataclass_from_dict
from modules.common import req
Expand All @@ -20,18 +20,22 @@ def __init__(self,
device_id: int,
device_address: str,
device_variant: int,
api_v2_token: Optional[str],
component_config: Union[Dict, SonnenbatterieCounterSetup]) -> None:
self.__device_id = device_id
self.__device_address = device_address
self.__device_variant = device_variant
self.__api_v2_token = api_v2_token
self.component_config = dataclass_from_dict(SonnenbatterieCounterSetup, component_config)
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:
Expand Down
115 changes: 115 additions & 0 deletions packages/modules/devices/sonnen/sonnenbatterie/counter_consumption.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 14 additions & 2 deletions packages/modules/devices/sonnen/sonnenbatterie/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -21,25 +23,35 @@ def create_bat_component(component_config: SonnenbatterieBatSetup):
return SonnenbatterieBat(device_config.id,
device_config.configuration.ip_address,
device_config.configuration.variant,
device_config.configuration.api_v2_token,
component_config)

def create_counter_component(component_config: SonnenbatterieCounterSetup):
def create_evu_counter_component(component_config: SonnenbatterieCounterSetup):
return SonnenbatterieCounter(device_config.id,
device_config.configuration.ip_address,
device_config.configuration.variant,
device_config.configuration.api_v2_token,
component_config)

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(device_config.id,
device_config.configuration.ip_address,
device_config.configuration.variant,
device_config.configuration.api_v2_token,
component_config)

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())
Expand Down
10 changes: 7 additions & 3 deletions packages/modules/devices/sonnen/sonnenbatterie/inverter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
import logging
from typing import Dict, Union
from typing import Dict, Optional, Union

from dataclass_utils import dataclass_from_dict
from modules.common import req
Expand All @@ -19,19 +19,23 @@ class SonnenbatterieInverter(AbstractInverter):
def __init__(self,
device_id: int,
device_address: str,
device_variant: int,
device_variant: Optional[int],
api_v2_token: str,
component_config: Union[Dict, SonnenbatterieInverterSetup]) -> None:
self.__device_id = device_id
self.__device_address = device_address
self.__device_variant = device_variant
self.__api_v2_token = api_v2_token
self.component_config = dataclass_from_dict(SonnenbatterieInverterSetup, component_config)
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:
Expand Down
Loading

0 comments on commit 3308fb5

Please sign in to comment.