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

metadata for automatic pub of changed values #1092

Merged
merged 1 commit into from
Oct 17, 2023
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
112 changes: 112 additions & 0 deletions packages/helpermodules/changed_values_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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

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)
112 changes: 112 additions & 0 deletions packages/helpermodules/changed_values_handler_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from dataclasses import asdict, dataclass, field
from typing import Dict, List, Optional, Tuple
from unittest.mock import Mock

import pytest

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(Mock())

# 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
6 changes: 5 additions & 1 deletion packages/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import threading
import traceback
from threading import Thread

from helpermodules.changed_values_handler import ChangedValuesHandler
from helpermodules.measurement_logging.update_daily_yields import update_daily_yields
from helpermodules.measurement_logging.write_log import save_log

from modules import loadvars
from modules import configuration
from helpermodules import timecheck, update_config
Expand Down Expand Up @@ -50,6 +51,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 @@ -61,6 +63,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 @@ -170,6 +173,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