-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
metadata for automatic pub of changed values
- Loading branch information
Showing
3 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
from dataclasses import asdict, fields, is_dataclass | ||
from enum import Enum | ||
import logging | ||
import threading | ||
from typing import Dict, List, Tuple | ||
|
||
from control.data import Data | ||
from helpermodules.pub import Pub | ||
|
||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
# In den Metadaten wird unter dem Key der Topic-Suffix ab "openWB/ev/2/" angegeben. Der Topic-Prefix ("openWB/ev/2/") | ||
# wird automatisch ermittelt. | ||
# Der Key mutable_by_algorithm ist True, wenn die Werte im Algorithmus geändert werden können. Nur diese Werte werden | ||
# automatisiert gepublished. Werte, die an anderer Stelle gepublished werden, wie zB Zählerstände, werden mit False | ||
# gekennzeichnet. Um eine Dokumentation der Topics zu erhalten, sollten die Metadaten dennoch ausgefüllt werden. | ||
# Metadaten werden nur für Felder erzeugt, die gepublished werden sollen, dh bei ganzen Klassen für das Feld der | ||
# jeweiligen Klasse. Wenn Werte aus einer instanziierten Klasse gepublished werden sollen, erhält die übergeordnete | ||
# Klasse keine Metadaten (siehe Beispiel unten). | ||
# Damit die geänderten Werte automatisiert gepublished werden können, muss jede Klasse eine bestimmte Form haben: | ||
# | ||
# @dataclass | ||
# class SampleClass: | ||
# parameter1: bool = False | ||
# parameter2: int = 5 | ||
|
||
|
||
# def sample_class() -> SampleClass: | ||
# return SampleClass() | ||
|
||
|
||
# @dataclass | ||
# class SampleNested: | ||
# parameter1: bool = field(default=False, metadata={"topic": "get/nested1", "mutable_by_algorithm": True}) | ||
# parameter2: int = field(default=0, metadata={"topic": "get/nested2", "mutable_by_algorithm": True}) | ||
|
||
|
||
# def sample_nested() -> SampleNested: | ||
# return SampleNested() | ||
|
||
|
||
# @dataclass | ||
# class SampleData: | ||
# # Wenn eine ganze Klasse als Dictionary gepublished werden soll, wie zB bei Konfigurationen, werden Metadaten für | ||
# diese Klasse eingetragen. Die Felder der Konfigurationsklasse bekommen keine Metadaten, da diese nicht einzeln | ||
# gepublished werden. | ||
# sample_field_class: SampleClass = field( | ||
# default_factory=sample_class, metadata={"topic": "get/field_class", "mutable_by_algorithm": True}) | ||
# sample_field_int: int = field(default=0, metadata={"topic": "get/field_int", "mutable_by_algorithm": True}) | ||
# sample_field_immutable: float = field( | ||
# default=0, metadata={"topic": "get/field_immutable", "mutable_by_algorithm": False}) | ||
# sample_field_list: List = field(default_factory=currents_list_factory, metadata={ | ||
# "topic": "get/field_list", "mutable_by_algorithm": True}) | ||
# # Bei verschachtelten Klassen, wo der zu publishende Wert auf einer tieferen Ebene liegt, werden nur für den zu | ||
# publishenden Wert Metadaten erzeugt. | ||
# sample_field_nested: SampleNested = field(default_factory=sample_nested) | ||
|
||
|
||
# class Sample: | ||
# def __init__(self) -> None: | ||
# self.data = SampleData() | ||
|
||
|
||
class ChangedValuesHandler: | ||
def __init__(self, event_module_update_completed: threading.Event) -> None: | ||
self.prev_data: Data = Data(event_module_update_completed) | ||
|
||
def store_inital_values(self): | ||
# speichern der Daten zum Zyklus-Beginn, um später die geänderten Werte zu ermitteln | ||
self.prev_data.copy_data() | ||
|
||
def pub_changed_values(self): | ||
# publishen der geänderten Werte | ||
pass | ||
# for key, value in data.data.bat_data.items(): | ||
# self._update_value(f"openWB/set/bat/{value.num}/", self.prev_data.bat_data[key], value) | ||
|
||
def _update_value(self, topic_prefix, data_inst_previous, data_inst): | ||
for f in fields(data_inst): | ||
try: | ||
changed = False | ||
value = getattr(data_inst, f.name) | ||
if isinstance(value, Enum): | ||
value = value.value | ||
previous_value = getattr(data_inst_previous, f.name) | ||
if isinstance(previous_value, Enum): | ||
previous_value = previous_value.value | ||
if hasattr(f, "metadata"): | ||
if f.metadata.get("mutable_by_algorithm"): | ||
if isinstance(value, (str, int, float, Dict, List, Tuple)): | ||
if previous_value != value: | ||
changed = True | ||
elif isinstance(value, (bool, type(None))): | ||
if previous_value is not value: | ||
changed = True | ||
else: | ||
dict_prev = dict(asdict(previous_value)) | ||
dict_current = dict(asdict(value)) | ||
if dict_prev != dict_current: | ||
changed = True | ||
value = asdict(value) | ||
previous_value = asdict(previous_value) | ||
|
||
if changed: | ||
topic = f"{topic_prefix}{f.metadata['topic']}" | ||
Pub().pub(topic, value) | ||
log.debug(f"Topic {topic}, Payload {value}, vorherige Payload: {previous_value}") | ||
continue | ||
if is_dataclass(value): | ||
self._update_value(topic_prefix, previous_value, value) | ||
except Exception as e: | ||
log.exception(e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
from dataclasses import asdict, dataclass, field | ||
from typing import Dict, List, Optional, Tuple | ||
from unittest.mock import Mock | ||
|
||
import pytest | ||
from control.chargemode import Chargemode | ||
from control.chargepoint.chargepoint_state import ChargepointState | ||
|
||
from dataclass_utils.factories import currents_list_factory | ||
from helpermodules.changed_values_handler import ChangedValuesHandler | ||
|
||
NONE_TYPE = type(None) | ||
|
||
|
||
@dataclass | ||
class SampleClass: | ||
parameter1: bool = False | ||
parameter2: int = 5 | ||
|
||
|
||
def sample_class() -> SampleClass: | ||
return SampleClass() | ||
|
||
|
||
@dataclass | ||
class SampleNested: | ||
parameter1: bool = field(default=False, metadata={"topic": "get/nested1", "mutable_by_algorithm": True}) | ||
parameter2: int = field(default=0, metadata={"topic": "get/nested2", "mutable_by_algorithm": True}) | ||
|
||
|
||
def sample_nested() -> SampleNested: | ||
return SampleNested() | ||
|
||
|
||
def sample_dict_factory() -> Dict: | ||
return {"key": "value"} | ||
|
||
|
||
def sample_tuple_factory() -> Tuple: | ||
return (1, 2, 3) | ||
|
||
|
||
@dataclass | ||
class SampleData: | ||
sample_field_bool: bool = field(default=False, metadata={"topic": "get/field_bool", "mutable_by_algorithm": True}) | ||
sample_field_class: SampleClass = field( | ||
default_factory=sample_class, metadata={"topic": "get/field_class", "mutable_by_algorithm": True}) | ||
sample_field_dict: Dict = field(default_factory=sample_dict_factory, metadata={ | ||
"topic": "get/field_dict", "mutable_by_algorithm": True}) | ||
sample_field_enum: ChargepointState = field(default=ChargepointState.CHARGING_ALLOWED, metadata={ | ||
"topic": "get/field_enum", "mutable_by_algorithm": True}) | ||
sample_field_float: float = field(default=0, metadata={"topic": "get/field_float", "mutable_by_algorithm": True}) | ||
sample_field_int: int = field(default=0, metadata={"topic": "get/field_int", "mutable_by_algorithm": True}) | ||
sample_field_immutable: float = field( | ||
default=0, metadata={"topic": "get/field_immutable", "mutable_by_algorithm": False}) | ||
sample_field_list: List = field(default_factory=currents_list_factory, metadata={ | ||
"topic": "get/field_list", "mutable_by_algorithm": True}) | ||
sample_field_nested: SampleNested = field(default_factory=sample_nested) | ||
sample_field_none: Optional[str] = field( | ||
default="Hi", metadata={"topic": "get/field_none", "mutable_by_algorithm": True}) | ||
sample_field_str: str = field(default="Hi", metadata={"topic": "get/field_str", "mutable_by_algorithm": True}) | ||
sample_field_tuple: Tuple = field(default_factory=sample_tuple_factory, metadata={ | ||
"topic": "get/field_tuple", "mutable_by_algorithm": True}) | ||
|
||
|
||
@dataclass | ||
class Params: | ||
name: str | ||
sample_data: SampleData | ||
expected_pub_call: Tuple = () | ||
expected_calls: int = 1 | ||
|
||
|
||
cases = [ | ||
Params(name="change bool", sample_data=SampleData(sample_field_bool=True), | ||
expected_pub_call=("openWB/get/field_bool", True)), | ||
Params(name="change class", sample_data=SampleData(sample_field_class=SampleClass(parameter1=True)), | ||
expected_pub_call=("openWB/get/field_class", asdict(SampleClass(parameter1=True)))), | ||
Params(name="change dict", sample_data=SampleData(sample_field_dict={"key": "another_value"}), | ||
expected_pub_call=("openWB/get/field_dict", {"key": "another_value"})), | ||
Params(name="change enum", sample_data=SampleData(sample_field_enum=ChargepointState.NO_CHARGING_ALLOWED), | ||
expected_pub_call=("openWB/get/field_enum", ChargepointState.NO_CHARGING_ALLOWED.value)), | ||
Params(name="change float", sample_data=SampleData(sample_field_float=2.5), | ||
expected_pub_call=("openWB/get/field_float", 2.5)), | ||
Params(name="change int", sample_data=SampleData(sample_field_int=2), | ||
expected_pub_call=("openWB/get/field_int", 2)), | ||
Params(name="immutable", sample_data=SampleData(sample_field_immutable=2), expected_calls=0), | ||
Params(name="change list", sample_data=SampleData(sample_field_list=[ | ||
10, 0, 0]), expected_pub_call=("openWB/get/field_list", [10, 0, 0])), | ||
Params(name="change nested", sample_data=SampleData(sample_field_nested=SampleNested( | ||
parameter1=True)), expected_pub_call=("openWB/get/nested1", True)), | ||
Params(name="change none", sample_data=SampleData(sample_field_none=None), | ||
expected_pub_call=("openWB/get/field_none", None)), | ||
Params(name="change str", sample_data=SampleData(sample_field_str="Hello"), | ||
expected_pub_call=("openWB/get/field_str", "Hello")), | ||
Params(name="change tuple", sample_data=SampleData(sample_field_tuple=(1, 2, 4)), | ||
expected_pub_call=("openWB/get/field_tuple", (1, 2, 4))), | ||
|
||
] | ||
|
||
|
||
@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases]) | ||
def test_update_value(params: Params, mock_pub: Mock): | ||
# setup | ||
handler = ChangedValuesHandler() | ||
|
||
# execution | ||
handler._update_value("openWB/", SampleData(), params.sample_data) | ||
|
||
# evaluation | ||
assert len(mock_pub.method_calls) == params.expected_calls | ||
if params.expected_calls > 0: | ||
assert mock_pub.method_calls[0].args == params.expected_pub_call |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters