Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sonnenbatterie power control #2092

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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