diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index ad5e296c4d..a27d7cdbcd 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -176,6 +176,7 @@ class Config: configuration: Dict = field(default_factory=empty_dict_factory) ev: int = 0 name: str = "neuer Ladepunkt" + color: str = "#007bff" type: Optional[str] = None template: int = 0 connected_phases: int = 3 diff --git a/packages/control/ev/ev.py b/packages/control/ev/ev.py index b1c1c54767..1136db62c1 100644 --- a/packages/control/ev/ev.py +++ b/packages/control/ev/ev.py @@ -77,6 +77,7 @@ class EvData: charge_template: int = field(default=0, metadata={"topic": "charge_template"}) ev_template: int = field(default=0, metadata={"topic": "ev_template"}) name: str = field(default="neues Fahrzeug", metadata={"topic": "name"}) + color: str = field(default="#17a2b8", metadata={"topic": "color"}) tag_id: List[str] = field(default_factory=empty_list_factory, metadata={ "topic": "tag_id"}) get: Get = field(default_factory=get_factory) diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index 36ccc2fc44..7083698b1d 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -14,83 +14,86 @@ from helpermodules import timecheck from helpermodules.utils.json_file_handler import write_and_check from helpermodules.utils.topic_parser import decode_payload, get_index -from modules.common.utils.component_parser import get_component_name_by_id +from modules.common.utils.component_parser import get_component_name_by_id, get_component_color_by_id log = logging.getLogger(__name__) # erstellt für jeden Tag eine Datei, die die Daten für den Langzeitgraph enthält. # Dazu werden alle 5 Min folgende Daten als json-Liste gespeichert: -# {"entries": [ -# { -# "timestamp": int, -# "date": str, -# "prices": { -# "grid": Preis für Netzbezug, -# "pv": Preis für PV-Strom, -# "bat": Preis für Speicherstrom -# } -# "cp": { -# "cp1": { -# "imported": Zählerstand in Wh, -# "exported": Zählerstand in Wh -# } -# ... (dynamisch, je nach konfigurierter Anzahl) -# "all": { -# "imported": Zählerstand in Wh, -# "exported": Zählerstand in Wh -# } -# } -# "ev": { -# "ev1": { -# "soc": int in % +# { +# "entries": [ +# { +# "timestamp": int, +# "date": str, +# "prices": { +# "grid": Preis für Netzbezug, +# "pv": Preis für PV-Strom, +# "bat": Preis für Speicherstrom # } -# ... (dynamisch, je nach konfigurierter Anzahl) -# } -# "counter": { -# "counter0": { -# "grid": bool, -# "imported": Wh, -# "exported": Wh +# "cp": { +# "cp1": { +# "imported": Zählerstand in Wh, +# "exported": Zählerstand in Wh +# } +# ... (dynamisch, je nach konfigurierter Anzahl) +# "all": { +# "imported": Zählerstand in Wh, +# "exported": Zählerstand in Wh +# } # } -# ... (dynamisch, je nach konfigurierter Anzahl) -# } -# "pv": { -# "all": { -# "exported": Wh +# "ev": { +# "ev1": { +# "soc": int in % +# } +# ... (dynamisch, je nach konfigurierter Anzahl) # } -# "pv0": { -# "exported": Wh +# "counter": { +# "counter0": { +# "grid": bool, +# "imported": Wh, +# "exported": Wh +# } +# ... (dynamisch, je nach konfigurierter Anzahl) # } -# ... (dynamisch, je nach konfigurierter Anzahl) -# } -# "bat": { -# "all": { -# "imported": Wh, -# "exported": Wh, -# "soc": int in % +# "pv": { +# "all": { +# "exported": Wh +# } +# "pv0": { +# "exported": Wh +# } +# ... (dynamisch, je nach konfigurierter Anzahl) # } -# "bat0": { -# "imported": Wh, -# "exported": Wh, -# "soc": int in % +# "bat": { +# "all": { +# "imported": Wh, +# "exported": Wh, +# "soc": int in % +# } +# "bat0": { +# "imported": Wh, +# "exported": Wh, +# "soc": int in % +# } +# ... (dynamisch, je nach konfigurierter Anzahl) # } -# ... (dynamisch, je nach konfigurierter Anzahl) -# } -# "sh": { -# "sh1": { -# "exported": Wh, -# "imported": Wh, -# wenn konfiguriert: -# "temp1": int in °C, -# "temp2": int in °C, -# "temp3": int in °C +# "sh": { +# "sh1": { +# "exported": Wh, +# "imported": Wh, +# wenn konfiguriert: +# "temp1": int in °C, +# "temp2": int in °C, +# "temp3": int in °C +# }, +# ... (dynamisch, je nach Anzahl konfigurierter Geräte) # }, -# ... (dynamisch, je nach Anzahl konfigurierter Geräte) -# }, -# "hc": {"all": {"imported": Wh # Hausverbrauch}} -# }], -# "names": "names": {"sh1": "", "cp1": "", "counter2": "", "pv3": ""} -# } +# "hc": {"all": {"imported": Wh # Hausverbrauch}} +# } +# ], +# "names": {"cp1": "", "counter2": "", "pv3": ""}, +# "colors": {"cp1": "", "counter2": "", "pv3": ""}, +# } class LogType(Enum): @@ -165,6 +168,7 @@ def save_log(log_type: LogType): entries = content["entries"] entries.append(new_entry) content["names"] = get_names(content["entries"][-1], sh_log_data.sh_names) + content["colors"] = get_colors(content["entries"][-1]) write_and_check(filepath, content) return content["entries"] except Exception: @@ -357,3 +361,30 @@ def get_names(elements: Dict, sh_names: Dict, valid_names: Optional[Dict] = None except (ValueError, KeyError, AttributeError): names.update({entry: entry}) return names + + +def get_colors(elements: Dict) -> Dict: + """ Ermittelt die Farben der Fahrzeuge, Ladepunkte und Komponenten, welche + in elements vorhanden sind und gibt diese als Dictionary zurück. + Parameter + --------- + elements: dict + Dictionary, das die Messwerte enthält. + """ + colors = {} + for group in elements.items(): + if group[0] not in ("ev", "cp", "counter", "pv", "bat"): + continue + for entry in group[1]: + if "all" != entry: + try: + if "ev" in entry: + colors.update({entry: data.data.ev_data[entry].data.color}) + elif "cp" in entry: + colors.update({entry: data.data.cp_data[entry].data.config.color}) + else: + id = entry.strip(string.ascii_letters) + colors.update({entry: get_component_color_by_id(int(id))}) + except (ValueError, KeyError, AttributeError): + colors.update({entry: "#000000"}) + return colors diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 2c3fdd573e..1e3e6aa3ad 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -365,6 +365,8 @@ def process_vehicle_topic(self, msg: mqtt.MQTTMessage): try: if "/name" in msg.topic: self._validate_value(msg, str) + elif "/color" in msg.topic: + self._validate_value(msg, str) elif "/info" in msg.topic: self._validate_value(msg, "json") elif "openWB/set/vehicle/set/vehicle_update_completed" in msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 749cfa51ed..6775b1c2a2 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -1,3 +1,4 @@ +from concurrent.futures import ProcessPoolExecutor import copy from dataclasses import asdict import datetime @@ -57,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 101 + DATASTORE_VERSION = 102 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -360,6 +361,7 @@ class UpdateConfig: "^openWB/vehicle/[0-9]+/charge_template$", "^openWB/vehicle/[0-9]+/ev_template$", "^openWB/vehicle/[0-9]+/name$", + "^openWB/vehicle/[0-9]+/color$", "^openWB/vehicle/[0-9]+/info$", "^openWB/vehicle/[0-9]+/soc_module/calculated_soc_state$", "^openWB/vehicle/[0-9]+/soc_module/config$", @@ -523,6 +525,7 @@ class UpdateConfig: ("openWB/counter/config/consider_less_charging", counter_all.Config().consider_less_charging), ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), + ("openWB/vehicle/0/color", "#17a2b8"), ("openWB/vehicle/0/info", {"manufacturer": None, "model": None}), ("openWB/vehicle/0/charge_template", ev.Ev(0).charge_template.data.id), ("openWB/vehicle/0/soc_module/config", NO_MODULE), @@ -2625,3 +2628,83 @@ def upgrade(topic: str, payload) -> None: Pub().pub(topic, payload) self._loop_all_received_topics(upgrade) self._append_datastore_version(101) + + def upgrade_datastore_102(self) -> None: + DEFAULT_COLORS = { + "CHARGEPOINT": "#007bff", + "VEHICLE": "#17a2b8", + "INVERTER": "#28a745", + "COUNTER": "#dc3545", + "BATTERY": "#ffc107", + "UNKNOWN": "#000000" + } + + def _add_colors_to_log(file): + colors = {} + with open(file, "r+") as jsonFile: + content_raw = jsonFile.read() + content = json.loads(content_raw) + if "colors" in content: + return + for key in content["names"].keys(): + if "bat" in key: + colors[key] = DEFAULT_COLORS["BATTERY"] + elif "counter" in key: + colors[key] = DEFAULT_COLORS["COUNTER"] + elif "cp" in key: + colors[key] = DEFAULT_COLORS["CHARGEPOINT"] + elif "ev" in key: + colors[key] = DEFAULT_COLORS["VEHICLE"] + elif "inverter" in key: + colors[key] = DEFAULT_COLORS["INVERTER"] + else: + colors[key] = DEFAULT_COLORS["UNKNOWN"] + content["colors"] = colors + jsonFile.seek(0) + jsonFile.write(json.dumps(content)) + jsonFile.truncate() + + def add_colors_to_logs(): + files = glob.glob(str(self.base_path / "data" / "daily_log") + "/*") + files.extend(glob.glob(str(self.base_path / "data" / "monthly_log") + "/*")) + files.sort() + with ProcessPoolExecutor() as executor: + executor.map(_add_colors_to_log, files) + + def upgrade(topic: str, payload) -> Optional[dict]: + # add vehicle color to vehicle topics + if re.search("^openWB/vehicle/[0-9]+/name$", topic) is not None: + log.debug(f"Received vehicle name topic '{topic}'") + vehicle_color_topic = topic.replace("/name", "/color") + log.debug(f"Checking for vehicle color topic '{vehicle_color_topic}'") + if vehicle_color_topic not in self.all_received_topics: + log.debug(f"Adding vehicle color topic '{vehicle_color_topic}'" + f" with value: '{DEFAULT_COLORS['VEHICLE']}'") + return {vehicle_color_topic: DEFAULT_COLORS['VEHICLE']} + # add property "color" to charge points + if re.search("^openWB/chargepoint/[0-9]+/config$", topic) is not None: + config = decode_payload(payload) + log.debug(f"Received charge point config topic '{topic}' with payload: {payload}") + if "color" not in config: + config.update({"color": DEFAULT_COLORS['CHARGEPOINT']}) + log.debug(f"Added color to charge point config: {config}") + return {topic: config} + # add property "color" to components + if re.search("^openWB/system/device/[0-9]+/component/[0-9]+/config$", topic) is not None: + config = decode_payload(payload) + log.debug(f"Received component config topic '{topic}' with payload: {payload}") + if "color" not in config: + if "counter" in config.get("type").lower(): + config.update({"color": DEFAULT_COLORS['COUNTER']}) + elif "bat" in config.get("type").lower(): + config.update({"color": DEFAULT_COLORS['BATTERY']}) + elif "inverter" in config.get("type").lower(): + config.update({"color": DEFAULT_COLORS['INVERTER']}) + else: + log.warning(f"Unknown component type '{config.get('type')}' for topic '{topic}'.") + config.update({"color": DEFAULT_COLORS['UNKNOWN']}) + log.debug(f"Updated component config with color: {config}") + return {topic: config} + self._loop_all_received_topics(upgrade) + add_colors_to_logs() + self._append_datastore_version(102) diff --git a/packages/modules/common/component_setup.py b/packages/modules/common/component_setup.py index c4eaffe1a1..022ac6571c 100644 --- a/packages/modules/common/component_setup.py +++ b/packages/modules/common/component_setup.py @@ -10,3 +10,12 @@ def __init__(self, name: str, type: str, id: int, configuration: T) -> None: self.type = type self.id = id self.configuration = configuration + if "counter" in type.lower(): + self.color = "#dc3545" + elif "bat" in type.lower(): + self.color = "#ffc107" + elif "inverter" in type.lower(): + self.color = "#28a745" + else: + # Default color for other types + self.color = "#000000" diff --git a/packages/modules/common/utils/component_parser.py b/packages/modules/common/utils/component_parser.py index 5dcbb226ac..4b3a27be95 100644 --- a/packages/modules/common/utils/component_parser.py +++ b/packages/modules/common/utils/component_parser.py @@ -18,6 +18,16 @@ def get_component_name_by_id(id: int): raise ValueError(f"Element {id} konnte keinem Gerät zugeordnet werden.") +def get_component_color_by_id(id: int): + for item in data.data.system_data.values(): + if isinstance(item, AbstractDevice): + for comp in item.components.values(): + if comp.component_config.id == id: + return comp.component_config.color + else: + raise ValueError(f"Element {id} konnte keinem Gerät zugeordnet werden.") + + def get_io_name_by_id(id: int): for item in data.data.system_data.values(): if isinstance(item, AbstractIoDevice):