Skip to content
Merged
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
73 changes: 63 additions & 10 deletions packages/modules/devices/sonnen/sonnenbatterie/bat.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
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 TypedDict, Any
from typing import Optional, TypedDict, Any

from modules.common import req
from modules.common.abstract_device import AbstractCounter
Expand All @@ -15,6 +15,7 @@


class KwargsDict(TypedDict):
api_v2_token: str
device_id: int
device_address: str
device_variant: int
Expand All @@ -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:
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)
22 changes: 17 additions & 5 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(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())
Expand Down
Loading