Skip to content

Commit

Permalink
metadata for automatic pub of changed values
Browse files Browse the repository at this point in the history
  • Loading branch information
LKuemmel committed Aug 10, 2023
1 parent 6cc9664 commit 3e2bf01
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
114 changes: 114 additions & 0 deletions packages/helpermodules/changed_values_handler.py
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)
113 changes: 113 additions & 0 deletions packages/helpermodules/changed_values_handler_test.py
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

Check failure on line 6 in packages/helpermodules/changed_values_handler_test.py

View workflow job for this annotation

GitHub Actions / build

'control.chargemode.Chargemode' imported but unused
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
4 changes: 4 additions & 0 deletions packages/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import threading
import traceback
from threading import Thread
from helpermodules.changed_values_handler import ChangedValuesHandler
from helpermodules.pub import Pub

from modules import loadvars
Expand Down Expand Up @@ -48,6 +49,7 @@ def handler_with_control_interval():
data.data.copy_data()
loadvars_.get_values()
data.data.copy_data()
changed_values_handler.store_inital_values()
self.heartbeat = True
if data.data.system_data["system"].data["perform_update"]:
data.data.system_data["system"].perform_update()
Expand All @@ -59,6 +61,7 @@ def handler_with_control_interval():
control.calc_current()
proc.process_algorithm_results()
data.data.graph_data.pub_graph_data()
changed_values_handler.pub_changed_values()
self.interval_counter = 1
else:
self.interval_counter = self.interval_counter + 1
Expand Down Expand Up @@ -168,6 +171,7 @@ def schedule_jobs():
general_internal_chargepoint_handler = GeneralInternalChargepointHandler()
rfid0 = RfidReader("event0")
rfid1 = RfidReader("event1")
changed_values_handler = ChangedValuesHandler(loadvars_.event_module_update_completed)
event_ev_template = threading.Event()
event_ev_template.set()
event_charge_template = threading.Event()
Expand Down

0 comments on commit 3e2bf01

Please sign in to comment.