diff --git a/db_addon/__init__.py b/db_addon/__init__.py old mode 100755 new mode 100644 index f20b78023..d0afe70f1 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -25,13 +25,20 @@ # ######################################################################### +import os import sqlvalidator import datetime import time import re import queue +import threading +import logging +import pickle +import operator from dateutil.relativedelta import relativedelta from typing import Union +from dataclasses import dataclass, InitVar +from collections import deque from lib.model.smartplugin import SmartPlugin from lib.item import Items @@ -40,8 +47,10 @@ from lib.plugin import Plugins from .webif import WebInterface from .item_attributes import * +from .item_attributes_master import ITEM_ATTRIBUTES import lib.db +HOUR = 'hour' DAY = 'day' WEEK = 'week' MONTH = 'month' @@ -53,7 +62,7 @@ class DatabaseAddOn(SmartPlugin): Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '1.2.2' + PLUGIN_VERSION = '1.2.7' def __init__(self, sh): """ @@ -63,18 +72,25 @@ def __init__(self, sh): # Call init code of parent class (SmartPlugin) super().__init__() + self.logger.debug(f'Start of {self.get_shortname()} Plugin.') + # get item and shtime instance self.shtime = Shtime.get_instance() self.items = Items.get_instance() self.plugins = Plugins.get_instance() # define cache dicts - self.current_values = {} # Dict to hold min and max value of current day / week / month / year for items - self.previous_values = {} # Dict to hold value of end of last day / week / month / year for items - self.item_cache = {} # Dict to hold item_id, oldest_log_ts and oldest_entry for items + self.pickle_data_validity_time = 600 # seconds after which the data saved in pickle are not valid anymore + self.current_values = {} # Dict to hold min and max value of current day / week / month / year for items + self.previous_values = {} # Dict to hold value of end of last day / week / month / year for items + self.item_cache = {} # Dict to hold item_id, oldest_log_ts and oldest_entry for items + self.value_list_raw_data = {} # define variables for database, database connection, working queue and status self.item_queue = queue.Queue() # Queue containing all to be executed items + self.update_item_delay_deque = deque() # Deque for delay working of updated item values + # ToDo: Check if still needed + self.queue_consumer_thread = None # Queue consumer thread self._db_plugin = None # object if database plugin self._db = None # object of database self.connection_data = None # connection data list of database @@ -83,17 +99,9 @@ def __init__(self, sh): self.item_attribute_search_str = 'database' # attribute, on which an item configured for database can be identified self.last_connect_time = 0 # mechanism for limiting db connection requests self.alive = None # Is plugin alive? - self.startup_finished = False # Startup of Plugin finished self.suspended = False # Is plugin activity suspended self.active_queue_item: str = '-' # String holding item path of currently executed item - - # define debug logs - self.parse_debug = True # Enable / Disable debug logging for method 'parse item' - self.execute_debug = True # Enable / Disable debug logging for method 'execute items' - self.sql_debug = True # Enable / Disable debug logging for sql stuff - self.ondemand_debug = True # Enable / Disable debug logging for method 'handle_ondemand' - self.onchange_debug = True # Enable / Disable debug logging for method 'handle_onchange' - self.prepare_debug = True # Enable / Disable debug logging for query preparation + self.onchange_delay_time = 30 # define default mysql settings self.default_connect_timeout = 60 @@ -106,9 +114,17 @@ def __init__(self, sh): self.value_filter = self.get_parameter_value('value_filter') self.optimize_value_filter = self.get_parameter_value('optimize_value_filter') self.use_oldest_entry = self.get_parameter_value('use_oldest_entry') + self.lock_db_for_query = self.get_parameter_value('lock_db_for_query') - # init cache dicts - self._init_cache_dicts() + # path and filename for data storage + data_storage_file = 'db_addon_data' + self.data_storage_path = f"{os.getcwd()}/var/plugin_data/{self.get_shortname()}/{data_storage_file}.pkl" + + # get debug log options + self.debug_log = DebugLogOptions(self.log_level) + + # init cache data + self.init_cache_data() # init webinterface self.init_webinterface(WebInterface) @@ -132,20 +148,21 @@ def run(self): return self.deinit() self.logger.debug("Initialization of database API successful") - # init db + # check initialization of db if not self._initialize_db(): self.logger.error("Connection to database failed") return self.deinit() + self._db.close() # check db connection settings - if self.db_driver is not None and self.db_driver.lower() == 'pymysql': + if self.db_driver.lower() == 'pymysql': self._check_db_connection_setting() # add scheduler for cyclic trigger item calculation - self.scheduler_add('cyclic', self.execute_due_items, prio=3, cron='5 0 0 * * *', cycle=None, value=None, offset=None, next=None) + self.scheduler_add('cyclic', self.execute_due_items, prio=3, cron='10 * * * *', cycle=None, value=None, offset=None, next=None) # add scheduler to trigger items to be calculated at startup with delay - dt = self.shtime.now() + datetime.timedelta(seconds=(self.startup_run_delay + 3)) + dt = self.shtime.now() + relativedelta(seconds=(self.startup_run_delay + 3)) self.logger.info(f"Set scheduler for calculating startup-items with delay of {self.startup_run_delay + 3}s to {dt}.") self.scheduler_add('startup', self.execute_startup_items, next=dt) @@ -156,8 +173,12 @@ def run(self): self.alive = True # work item queue + self.work_item_queue() + + # ToDo: Check if still needed + """ try: - self.work_item_queue() + self._queue_consumer_thread_startup() except Exception as e: self.logger.warning(f"During working item queue Exception '{e}' occurred.") self.logger.debug(e, exc_info=True) @@ -165,6 +186,7 @@ def run(self): # self.deinit() self.logger.error("Suspend Plugin and clear Item-Queue.") self.suspend(True) + """ def stop(self): """ @@ -174,6 +196,13 @@ def stop(self): self.logger.debug("Stop method called") self.alive = False self.scheduler_remove('cyclic') + self.scheduler_remove('onchange_delay') + if self._db: + self._db.close() + self.save_cache_data() + + # ToDo: Check if still needed + # self._queue_consumer_thread_shutdown() def parse_item(self, item: Item): """ @@ -190,15 +219,16 @@ def parse_item(self, item: Item): """ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: + """ derived parameters from given db_addon_fct""" # get parameter db_addon_fct_vars = db_addon_fct.split('_') - func = timeframe = timedelta = start = end = group = group2 = method = log_text = None + func = timeframe = timedelta = start = end = group = group2 = data_con_func = log_text = None required_params = None if db_addon_fct in HISTORIE_ATTRIBUTES_ONCHANGE: - # handle functions 'minmax on-change' in format 'minmax_timeframe_func' items like 'minmax_heute_max', 'minmax_heute_min', 'minmax_woche_max', 'minmax_woche_min' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) + # handle functions 'minmax onchange' in format 'minmax_timeframe_func' items like 'minmax_heute_max', 'minmax_heute_min', 'minmax_woche_max', 'minmax_woche_min' + timeframe = translate_timeframe(db_addon_fct_vars[1]) func = db_addon_fct_vars[2] if db_addon_fct_vars[2] in ALLOWED_MINMAX_FUNCS else None start = end = 0 log_text = 'minmax_timeframe_func' @@ -209,7 +239,7 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: func = db_addon_fct_vars[3] start, timeframe = split_sting_letters_numbers(db_addon_fct_vars[2]) start = to_int(start) - timeframe = harmonize_timeframe_expression(timeframe) + timeframe = translate_timeframe(timeframe) end = 0 log_text = 'minmax_last_timedelta|timeframe_function' required_params = [func, timeframe, start, end] @@ -217,61 +247,65 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: elif db_addon_fct in HISTORIE_ATTRIBUTES_TIMEFRAME: # handle functions 'min/max/avg' in format 'minmax_timeframe_timedelta_func' like 'minmax_heute_minus2_max' func = db_addon_fct_vars[3] # min, max, avg - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) # day, week, month, year - end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) - start = end + timeframe = translate_timeframe(db_addon_fct_vars[1]) # day, week, month, year + start = end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) log_text = 'minmax_timeframe_timedelta_func' required_params = [func, timeframe, start, end] elif db_addon_fct in ZAEHLERSTAND_ATTRIBUTES_TIMEFRAME: # handle functions 'zaehlerstand' in format 'zaehlerstand_timeframe_timedelta' like 'zaehlerstand_heute_minus1' - # func = 'max' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) - end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) - start = end + func = 'last' + timeframe = translate_timeframe(db_addon_fct_vars[1]) + start = end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) log_text = 'zaehlerstand_timeframe_timedelta' required_params = [timeframe, start, end] elif db_addon_fct in VERBRAUCH_ATTRIBUTES_ONCHANGE: - # handle functions 'verbrauch on-change' items in format 'verbrauch_timeframe' like 'verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) - end = 0 - start = 1 + # handle functions 'verbrauch onchange' items in format 'verbrauch_timeframe' like 'verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr' + timeframe = translate_timeframe(db_addon_fct_vars[1]) + start = end = 0 log_text = 'verbrauch_timeframe' required_params = [timeframe, start, end] elif db_addon_fct in VERBRAUCH_ATTRIBUTES_TIMEFRAME: # handle functions 'verbrauch on-demand' in format 'verbrauch_timeframe_timedelta' like 'verbrauch_heute_minus2' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) - # end = to_int(db_addon_fct_vars[2][-1]) - end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) - start = end + 1 + timeframe = translate_timeframe(db_addon_fct_vars[1]) + start = end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) log_text = 'verbrauch_timeframe_timedelta' required_params = [timeframe, start, end] + elif db_addon_fct in VERBRAUCH_ATTRIBUTES_LAST: + # handle functions 'verbrauch_last' in format 'verbrauch_last_timedelta|timeframe' like 'verbrauch_last_24h' + start, timeframe = split_sting_letters_numbers(db_addon_fct_vars[2]) + start = to_int(start) + timeframe = translate_timeframe(timeframe) + end = 0 + log_text = 'verbrauch_last_timedelta|timeframe' + required_params = [timeframe, start, end] + elif db_addon_fct in VERBRAUCH_ATTRIBUTES_ROLLING: # handle functions 'verbrauch_on-demand' in format 'verbrauch_rolling_window_timeframe_timedelta' like 'verbrauch_rolling_12m_woche_minus1' func = db_addon_fct_vars[1] window_inc, window_dur = split_sting_letters_numbers(db_addon_fct_vars[2]) window_inc = to_int(window_inc) # 12 - window_dur = harmonize_timeframe_expression(window_dur) # day, week, month, year - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[3]) # day, week, month, year + window_dur = translate_timeframe(window_dur) # day, week, month, year + timeframe = translate_timeframe(db_addon_fct_vars[3]) # day, week, month, year end = to_int(split_sting_letters_numbers(db_addon_fct_vars[4])[1]) if window_dur in ALLOWED_QUERY_TIMEFRAMES and window_inc and timeframe and end: - start = to_int(convert_timeframe(timeframe, window_dur) * window_inc) + end + start = to_int(timeframe_to_timeframe(timeframe, window_dur) * window_inc) + end log_text = 'verbrauch_rolling_window_timeframe_timedelta' required_params = [func, timeframe, start, end] elif db_addon_fct in VERBRAUCH_ATTRIBUTES_JAHRESZEITRAUM: # handle functions of format 'verbrauch_jahreszeitraum_timedelta' like 'verbrauch_jahreszeitraum_minus1' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) # day, week, month, year + timeframe = translate_timeframe(db_addon_fct_vars[1]) # day, week, month, year timedelta = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) log_text = 'verbrauch_jahreszeitraum_timedelta' required_params = [timeframe, timedelta] elif db_addon_fct in TAGESMITTEL_ATTRIBUTES_ONCHANGE: - # handle functions 'tagesmitteltemperatur on-change' items in format 'tagesmitteltemperatur_timeframe' like 'tagesmitteltemperatur_heute', 'tagesmitteltemperatur_woche', 'tagesmitteltemperatur_monat', 'tagesmitteltemperatur_jahr' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) + # handle functions 'tagesmitteltemperatur onchange' items in format 'tagesmitteltemperatur_timeframe' like 'tagesmitteltemperatur_heute', 'tagesmitteltemperatur_woche', 'tagesmitteltemperatur_monat', 'tagesmitteltemperatur_jahr' + timeframe = translate_timeframe(db_addon_fct_vars[1]) func = 'max' start = end = 0 log_text = 'tagesmitteltemperatur_timeframe' @@ -280,41 +314,38 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: elif db_addon_fct in TAGESMITTEL_ATTRIBUTES_TIMEFRAME: # handle 'tagesmitteltemperatur_timeframe_timedelta' like 'tagesmitteltemperatur_heute_minus1' func = 'max' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[1]) - end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) - start = end - method = 'avg_hour' + timeframe = translate_timeframe(db_addon_fct_vars[1]) + start = end = to_int(split_sting_letters_numbers(db_addon_fct_vars[2])[1]) + data_con_func = 'first_hour_avg_day' log_text = 'tagesmitteltemperatur_timeframe_timedelta' - required_params = [func, timeframe, start, end, method] + required_params = [func, timeframe, start, end, data_con_func] elif db_addon_fct in SERIE_ATTRIBUTES_MINMAX: # handle functions 'serie_minmax' in format 'serie_minmax_timeframe_func_start|group' like 'serie_minmax_monat_min_15m' func = db_addon_fct_vars[3] - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[2]) + timeframe = translate_timeframe(db_addon_fct_vars[2]) start, group = split_sting_letters_numbers(db_addon_fct_vars[4]) start = to_int(start) - group = harmonize_timeframe_expression(group) + group = translate_timeframe(group) end = 0 log_text = 'serie_minmax_timeframe_func_start|group' required_params = [func, timeframe, start, end, group] elif db_addon_fct in SERIE_ATTRIBUTES_ZAEHLERSTAND: # handle functions 'serie_zaehlerstand' in format 'serie_zaehlerstand_timeframe_start|group' like 'serie_zaehlerstand_tag_30d' - func = 'max' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[2]) + timeframe = translate_timeframe(db_addon_fct_vars[2]) start, group = split_sting_letters_numbers(db_addon_fct_vars[3]) start = to_int(start) - group = harmonize_timeframe_expression(group) + group = translate_timeframe(group) log_text = 'serie_zaehlerstand_timeframe_start|group' required_params = [timeframe, start, group] elif db_addon_fct in SERIE_ATTRIBUTES_VERBRAUCH: # handle all functions of format 'serie_verbrauch_timeframe_start|group' like 'serie_verbrauch_tag_30d' - func = 'diff_max' - timeframe = harmonize_timeframe_expression(db_addon_fct_vars[2]) + timeframe = translate_timeframe(db_addon_fct_vars[2]) start, group = split_sting_letters_numbers(db_addon_fct_vars[3]) start = to_int(start) - group = harmonize_timeframe_expression(group) + group = translate_timeframe(group) log_text = 'serie_verbrauch_timeframe_start|group' required_params = [timeframe, start, group] @@ -323,7 +354,7 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: func = 'sum_max' start, timeframe = split_sting_letters_numbers(db_addon_fct_vars[3]) start = to_int(start) - timeframe = harmonize_timeframe_expression(timeframe) + timeframe = translate_timeframe(timeframe) end = 0 group = 'day', group2 = 'month' @@ -336,7 +367,7 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: timeframe = 'year' start, group = split_sting_letters_numbers(db_addon_fct_vars[2]) start = to_int(start) - group = harmonize_timeframe_expression(group) + group = translate_timeframe(group) end = 0 log_text = 'serie_tagesmittelwert_count|group' required_params = [func, timeframe, start, end, group] @@ -349,29 +380,29 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: group = 'hour' start, group2 = split_sting_letters_numbers(db_addon_fct_vars[3]) start = to_int(start) - group2 = harmonize_timeframe_expression(group2) + group2 = translate_timeframe(group2) log_text = 'serie_tagesmittelwert_group2_count|group' required_params = [func, timeframe, start, end, group, group2] elif db_addon_fct in SERIE_ATTRIBUTES_MITTEL_H1: - # handle 'serie_tagesmittelwert_stunde_start_end|group' like 'serie_tagesmittelwert_stunde_30_0d' => Stundenmittelwerte von vor 30 Tage bis vor 0 Tagen (also heute) - method = 'avg_hour' + # handle 'serie_tagesmittelwert_stunde_start_end|group' like 'serie_tagesmittelwert_stunde_30_0d' => Stundenmittelwerte von vor 30 Tagen bis vor 0 Tagen (also heute) + data_con_func = 'avg_hour' start = to_int(db_addon_fct_vars[3]) end, timeframe = split_sting_letters_numbers(db_addon_fct_vars[4]) end = to_int(end) - timeframe = harmonize_timeframe_expression(timeframe) + timeframe = translate_timeframe(timeframe) log_text = 'serie_tagesmittelwert_stunde_start_end|group' - required_params = [timeframe, method, start, end] + required_params = [timeframe, data_con_func, start, end] elif db_addon_fct in SERIE_ATTRIBUTES_MITTEL_D_H: # handle 'serie_tagesmittelwert_tag_stunde_end|group' like 'serie_tagesmittelwert_tag_stunde_30d' => Tagesmittelwert auf Basis des Mittelwerts pro Stunden für die letzten 30 Tage - method = 'avg_hour' + data_con_func = 'first_hour_avg_day' end = 0 start, timeframe = split_sting_letters_numbers(db_addon_fct_vars[4]) start = to_int(start) - timeframe = harmonize_timeframe_expression(timeframe) + timeframe = translate_timeframe(timeframe) log_text = 'serie_tagesmittelwert_tag_stunde_end|group' - required_params = [timeframe, method, start, end] + required_params = [timeframe, data_con_func, start, end] elif db_addon_fct in ALL_GEN_ATTRIBUTES: log_text = 'all_gen_attributes' @@ -382,37 +413,44 @@ def get_query_parameters_from_db_addon_fct() -> Union[dict, None]: return if required_params and None in required_params: - self.logger.warning(f"For calculating '{db_addon_fct}' at Item '{item.path()}' not all mandatory parameters given. Definitions are: {func=}, {timeframe=}, {timedelta=}, {start=}, {end=}, {group=}, {group2=}, {method=}") + self.logger.warning(f"For calculating '{db_addon_fct}' at Item '{item.path()}' not all mandatory parameters given. Definitions are: {func=}, {timeframe=}, {timedelta=}, {start=}, {end=}, {group=}, {group2=}, {data_con_func=}") return # create dict and reduce dict to keys with value != None - param_dict = {'func': func, 'timeframe': timeframe, 'timedelta': timedelta, 'start': start, 'end': end, 'group': group, 'group2': group2, 'method': method} + param_dict = {'func': func, 'timeframe': timeframe, 'timedelta': timedelta, 'start': start, 'end': end, 'group': group, 'group2': group2, 'data_con_func': data_con_func} # return reduced dict w keys with value != None return {k: v for k, v in param_dict.items() if v is not None} def get_query_parameters_from_db_addon_params() -> Union[dict, None]: - """get query parameters from item attribute db_addon_params""" + """derives parameters from item attribute db_addon_params, if parameter for db_addon_fct are not sufficient + + possible_params may be given, if not, default value is used + required_params must be given + """ db_addon_params = params_to_dict(self.get_iattr_value(item.conf, 'db_addon_params')) if not db_addon_params: db_addon_params = self.get_iattr_value(item.conf, 'db_addon_params_dict') + if not db_addon_params: + db_addon_params = {} + new_db_addon_params = {} possible_params = required_params = [] - if db_addon_params is None: - self.logger.warning(f"Definition for Item '{item.path()}' with db_addon_fct={db_addon_fct} incomplete, since parameters via 'db_addon_params' not given. Item will be ignored.") - return - # create item config for all functions with 'summe' like waermesumme, kaeltesumme, gruenlandtemperatursumme - if 'summe' in db_addon_fct: + if db_addon_fct in ('kaeltesumme', 'waermesumme', 'gruenlandtempsumme'): possible_params = ['year', 'month'] - # create item config for wachstumsgradtage function + # create item config for wachstumsgradtage attributes elif db_addon_fct == 'wachstumsgradtage': - possible_params = ['year', 'method', 'threshold'] + possible_params = ['year', 'variant', 'threshold', 'result'] + + # create item config for kenntage attributes + elif db_addon_fct in ('wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage'): + possible_params = ['year', 'month'] # create item config for tagesmitteltemperatur elif db_addon_fct == 'tagesmitteltemperatur': @@ -449,8 +487,7 @@ def get_query_parameters_from_db_addon_params() -> Union[dict, None]: if value: new_db_addon_params[key] = value - if new_db_addon_params: - return new_db_addon_params + return new_db_addon_params def get_database_item_path() -> tuple: """ @@ -461,11 +498,12 @@ def get_database_item_path() -> tuple: for i in range(3): if self.has_iattr(_lookup_item.conf, 'db_addon_database_item'): - if self.parse_debug: - self.logger.debug(f"Attribut 'db_addon_database_item' for item='{item.path()}' has been found {i + 1} level above item at '{_lookup_item.path()}'.") + if self.debug_log.parse: + self.logger.debug(f"Attribut 'db_addon_database_item' for item='{item.path()}' has been found {i} level above item at '{_lookup_item.path()}'.") _database_item_path = self.get_iattr_value(_lookup_item.conf, 'db_addon_database_item') - _startup = bool(self.get_iattr_value(_lookup_item.conf, 'db_addon_startup')) - return _database_item_path, _startup + if self.debug_log.parse: + self.logger.debug(f"{_database_item_path=}, {_lookup_item.path()}") + return _database_item_path, _lookup_item else: _lookup_item = _lookup_item.return_parent() @@ -480,7 +518,7 @@ def get_database_item() -> Item: for i in range(2): if self.has_iattr(_lookup_item.conf, self.item_attribute_search_str): - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"Attribut '{self.item_attribute_search_str}' for item='{item.path()}' has been found {i + 1} level above item at '{_lookup_item.path()}'.") return _lookup_item else: @@ -510,7 +548,7 @@ def check_db_addon_fct(check_item) -> bool: Check if item has db_addon_fct and is onchange """ if self.has_iattr(check_item.conf, 'db_addon_fct'): - if self.get_iattr_value(check_item.conf, 'db_addon_fct').lower() in ALL_ONCHANGE_ATTRIBUTES: + if self.get_iattr_value(check_item.conf, 'db_addon_fct').lower() in ONCHANGE_ATTRIBUTES: return True return False @@ -532,7 +570,7 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte db_addon_ignore_value_list_formatted.append(f"{op} {value}") max_values[op].append(value) - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"Summarized 'ignore_value_list' for item {item.path()}: {db_addon_ignore_value_list_formatted}") if not db_addon_ignore_value_list_formatted: @@ -541,7 +579,7 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte if not optimize: return db_addon_ignore_value_list_formatted - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"Optimizing 'ignore_value_list' for item {item.path()} active.") # find low @@ -572,7 +610,7 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte if (not lower_end[0] or (lower_end[0] and v >= lower_end[1])) or (not upper_end[0] or (upper_end[0] and v <= upper_end[1])): db_addon_ignore_value_list_optimized.append(f'!= {v}') - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"Optimized 'ignore_value_list' for item {item.path()}: {db_addon_ignore_value_list_optimized}") return db_addon_ignore_value_list_optimized @@ -580,33 +618,40 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte # handle all items with db_addon_fct if self.has_iattr(item.conf, 'db_addon_fct'): - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"parse item: {item.path()} due to 'db_addon_fct'") # get db_addon_fct attribute value db_addon_fct = self.get_iattr_value(item.conf, 'db_addon_fct').lower() + # read item_attribute_dict aus item_attributes_master + item_attribute_dict = ITEM_ATTRIBUTES['db_addon_fct'].get(db_addon_fct) + self.logger.debug(f"{db_addon_fct}: {item_attribute_dict=}") + # get query parameters from db_addon_fct or db_addon_params - if db_addon_fct in ALL_NEED_PARAMS_ATTRIBUTES: + if item_attribute_dict['params']: query_params = get_query_parameters_from_db_addon_params() else: query_params = get_query_parameters_from_db_addon_fct() - if not query_params: + if query_params is None: return # get database item (and attribute value if item should be calculated at plugin startup) and return if not available - database_item, db_addon_startup = get_database_item_path() + database_item, database_item_definition_item = get_database_item_path() if database_item is None: database_item = get_database_item() - db_addon_startup = bool(self.get_iattr_value(item.conf, 'db_addon_startup')) + database_item_definition_item = item + db_addon_startup = self.get_iattr_value(database_item_definition_item.conf, 'db_addon_startup') + db_addon_ignore_value_list = self.get_iattr_value(database_item_definition_item.conf, 'db_addon_ignore_value_list') # ['> 0', '< 35'] + db_addon_ignore_value = self.get_iattr_value(database_item_definition_item.conf, 'db_addon_ignore_value') # num if database_item is None: - self.logger.warning(f"No database item found for {item.path()}: Item ignored. Maybe you should check instance of database plugin.") + self.logger.warning(f"No database item found for item={item.path()}: Item ignored. Maybe you should check instance of database plugin.") return + else: + if self.debug_log.parse: + self.logger.debug(f"{database_item=}, {db_addon_startup=}, {db_addon_ignore_value_list=}, {db_addon_ignore_value=}") - # get/create list of comparison operators and check it - db_addon_ignore_value_list = self.get_iattr_value(item.conf, 'db_addon_ignore_value_list') # ['> 0', '< 35'] - db_addon_ignore_value = self.get_iattr_value(item.conf, 'db_addon_ignore_value') # num - + # create list of comparison operators and check it if not db_addon_ignore_value_list: db_addon_ignore_value_list = [] @@ -623,49 +668,44 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte if db_addon_ignore_value_list: db_addon_ignore_value_list_final = format_db_addon_ignore_value_list() - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"{db_addon_ignore_value_list_final=}") query_params.update({'ignore_value_list': db_addon_ignore_value_list_final}) # create standard items config - item_config_data_dict = {'db_addon': 'function', 'db_addon_fct': db_addon_fct, 'database_item': database_item, 'query_params': query_params} + item_config_data_dict = {'db_addon': 'function', 'db_addon_fct': db_addon_fct, 'database_item': database_item, 'query_params': query_params, 'suspended': False} if isinstance(database_item, str): item_config_data_dict.update({'database_item_path': True}) else: database_item = database_item.path() # do logging - if self.parse_debug: - self.logger.debug(f"Item '{item.path()}' added with db_addon_fct={db_addon_fct} and database_item={database_item}") + if self.debug_log.parse: + self.logger.debug(f"Item={item.path()} added with db_addon_fct={db_addon_fct} and database_item={database_item}") + + # add type (onchange or ondemand) to item dict + item_config_data_dict.update({'on': item_attribute_dict['on']}) # add cycle for item groups - if db_addon_fct in ALL_DAILY_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'daily'}) - elif db_addon_fct in ALL_WEEKLY_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'weekly'}) - elif db_addon_fct in ALL_MONTHLY_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'monthly'}) - elif db_addon_fct in ALL_YEARLY_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'yearly'}) - elif db_addon_fct in ALL_GEN_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'static'}) - elif db_addon_fct in ALL_ONCHANGE_ATTRIBUTES: - item_config_data_dict.update({'cycle': 'on-change'}) - elif db_addon_fct == 'db_request': + cycle = item_attribute_dict['calc'] + if cycle == 'group': cycle = item_config_data_dict['query_params'].get('group') if not cycle: cycle = item_config_data_dict['query_params'].get('timeframe') - item_config_data_dict.update({'cycle': f"{timeframe_to_updatecyle(cycle)}"}) - elif db_addon_fct == 'minmax': - cycle = item_config_data_dict['query_params']['timeframe'] - item_config_data_dict.update({'cycle': f"{timeframe_to_updatecyle(cycle)}"}) + cycle = f"{timeframe_to_updatecyle(cycle)}" + elif cycle == 'timeframe': + cycle = item_config_data_dict['query_params'].get('timeframe') + cycle = f"{timeframe_to_updatecyle(cycle)}" + elif cycle == 'None': + cycle = None + item_config_data_dict.update({'cycle': cycle}) # do logging - if self.parse_debug: + if self.debug_log.parse: self.logger.debug(f"Item '{item.path()}' added to be run {item_config_data_dict['cycle']}.") # create item config for item to be run on startup - if db_addon_startup or db_addon_fct in ALL_GEN_ATTRIBUTES: + if db_addon_startup or item_attribute_dict['cat'] == 'gen': item_config_data_dict.update({'startup': True}) else: item_config_data_dict.update({'startup': False}) @@ -675,21 +715,21 @@ def format_db_addon_ignore_value_list(optimize: bool = self.optimize_value_filte # handle all items with db_addon_info elif self.has_iattr(item.conf, 'db_addon_info'): - if self.parse_debug: - self.logger.debug(f"parse item: {item.path()} due to used item attribute 'db_addon_info'") + if self.debug_log.parse: + self.logger.debug(f"parse item={item.path()} due to used item attribute 'db_addon_info'") self.add_item(item, config_data_dict={'db_addon': 'info', 'db_addon_fct': f"info_{self.get_iattr_value(item.conf, 'db_addon_info').lower()}", 'database_item': None, 'startup': True}) # handle all items with db_addon_admin elif self.has_iattr(item.conf, 'db_addon_admin'): - if self.parse_debug: - self.logger.debug(f"parse item: {item.path()} due to used item attribute 'db_addon_admin'") + if self.debug_log.parse: + self.logger.debug(f"parse item={item.path()} due to used item attribute 'db_addon_admin'") self.add_item(item, config_data_dict={'db_addon': 'admin', 'db_addon_fct': f"admin_{self.get_iattr_value(item.conf, 'db_addon_admin').lower()}", 'database_item': None}) return self.update_item # Reference to 'update_item' für alle Items mit Attribut 'database', um die on_change Items zu berechnen elif self.has_iattr(item.conf, self.item_attribute_search_str) and has_db_addon_item(): - if self.parse_debug: - self.logger.debug(f"reference to update_item for item '{item.path()}' will be set due to on-change") + if self.debug_log.parse: + self.logger.debug(f"reference to update_item for item={item.path()} will be set due to onchange") self.add_item(item, config_data_dict={'db_addon': 'database'}) return self.update_item @@ -708,13 +748,13 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_shortname(): # handle database items if item in self._database_items(): - if not self.startup_finished: - self.logger.info(f"Handling of 'on-change' is paused for startup. No updated will be processed.") - elif self.suspended: + # if not self.startup_finished: + # self.logger.info(f"Handling of 'onchange' is paused for startup. No updated will be processed.") + if self.suspended: self.logger.info(f"Plugin is suspended. No updated will be processed.") else: - self.logger.info(f"+ Updated item '{item.path()}' with value {item()} will be put to queue for processing. {self.item_queue.qsize() + 1} items to do.") - self.item_queue.put((item, item())) + self.logger.debug(f" Updated Item {item.path()} with value {item()} will be put to queue in approx. {self.onchange_delay_time}s resp. after startup.") + self.update_item_delay_deque.append([item, item(), int(time.time() + self.onchange_delay_time)]) # handle admin items elif self.has_iattr(item.conf, 'db_addon_admin'): @@ -728,57 +768,202 @@ def update_item(self, item, caller=None, source=None, dest=None): self._init_cache_dicts() item(False, self.get_shortname()) + def _save_pickle(self, data) -> None: + """Saves received data as pickle to given file""" + + if data and len(data) > 0: + self.logger.debug(f"Start writing {data=} to '{self.data_storage_path}'") + os.makedirs(os.path.dirname(self.data_storage_path), exist_ok=True) + try: + with open(self.data_storage_path, "wb") as output: + try: + pickle.dump(data, output, pickle.HIGHEST_PROTOCOL) + self.logger.debug(f"Successfully wrote data to '{self.data_storage_path}'") + except Exception as e: + self.logger.debug(f"Unable to write data to '{self.data_storage_path}': {e}") + pass + except OSError as e: + self.logger.debug(f"Unable to write data to '{self.data_storage_path}': {e}") + pass + + def _read_pickle(self): + """read a pickle file to gather data""" + + self.logger.debug(f"Start reading data from '{self.data_storage_path}'") + + if os.path.exists(self.data_storage_path): + with open(self.data_storage_path, 'rb') as data: + try: + data = pickle.load(data) + self.logger.debug(f"Successfully read data from {self.data_storage_path}") + return data + except Exception as e: + self.logger.debug(f"Unable to read data from {self.data_storage_path}: {e}") + return None + + self.logger.debug(f"Unable to read data from {self.data_storage_path}: 'File/Path not existing'") + return None + + def init_cache_data(self): + """init cache dicts by reading pickle""" + + def create_items_1(d): + n_d = {} + for item_str in d: + item = self.items.return_item(item_str) + if item: + n_d[item] = d[item_str] + return n_d + + def create_items_2(d): + n_d = {} + for timeframe in d: + n_d[timeframe] = {} + for item_str in d[timeframe]: + item = self.items.return_item(item_str) + if item: + n_d[timeframe][item] = d[timeframe][item_str] + return n_d + + # init cache dicts + self._init_cache_dicts() + + # read pickle and set data + raw_data = self._read_pickle() + + if not isinstance(raw_data, dict): + self.logger.info("Unable to extract db_addon data from pickle file. Start with empty cache.") + return + + current_values = raw_data.get('current_values') + previous_values = raw_data.get('previous_values') + item_cache = raw_data.get('item_cache') + stop_time = raw_data.get('stop_time') + + if not stop_time or (int(time.time()) - stop_time) > self.pickle_data_validity_time: + self.logger.info("Data for db_addon read from pickle are expired. Start with empty cache.") + return + + if isinstance(current_values, dict): + self.current_values = create_items_2(current_values) + if isinstance(previous_values, dict): + self.previous_values = create_items_2(previous_values) + if isinstance(item_cache, dict): + self.item_cache = create_items_1(item_cache) + + def save_cache_data(self): + """save all relevant data to survive restart, transform items in item_str""" + + def clean_items_1(d): + n_d = {} + for item in d: + n_d[item.path()] = d[item] + return n_d + + def clean_items_2(d): + n_d = {} + for timeframe in d: + n_d[timeframe] = {} + for item in d[timeframe]: + n_d[timeframe][item.path()] = d[timeframe][item] + return n_d + + self._save_pickle({'current_values': clean_items_2(self.current_values), + 'previous_values': clean_items_2(self.previous_values), + 'item_cache': clean_items_1(self.item_cache), + 'stop_time': int(time.time())}) + + ######################################### + # Item Handling + ######################################### + def execute_due_items(self) -> None: """Execute all items, which are due""" self.execute_items() def execute_startup_items(self) -> None: - """Execute all startup_items""" + """Execute all startup_items and set scheduler for delaying onchange items""" + # execute item calculation self.execute_items(option='startup') - self.startup_finished = True - def execute_items(self, option: str = 'due'): + # add scheduler for delayed working if onchange items + self.scheduler_add('onchange_delay', self.work_update_item_delay_deque, prio=3, cron=None, cycle=30, value=None, offset=None, next=None) + + def execute_items(self, option: str = 'due', item: str = None): """Execute all items per option""" def _create_due_items() -> list: """Create list of items which are due and reset cache dicts""" - # täglich zu berechnende Items zur Action Liste hinzufügen + # set für zu berechnende Items erstellen _todo_items = set() - _todo_items.update(set(self._daily_items())) - self.current_values[DAY] = {} - self.previous_values[DAY] = {} - - # wenn Wochentag == Montag, werden auch die wöchentlichen Items berechnet - if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.weekday( - self.shtime.today()) == 1: - _todo_items.update(set(self._weekly_items())) - self.current_values[WEEK] = {} - self.previous_values[WEEK] = {} - - # wenn der erste Tage eines Monates ist, werden auch die monatlichen Items berechnet - if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.now().day == 1: - _todo_items.update(set(self._monthly_items())) - self.current_values[MONTH] = {} - self.previous_values[MONTH] = {} - - # wenn der erste Tage des ersten Monates eines Jahres ist, werden auch die jährlichen Items berechnet - if self.shtime.now().hour == 0 and self.shtime.now().minute == 0 and self.shtime.now().day == 1 and self.shtime.now().month == 1: - _todo_items.update(set(self._yearly_items())) - self.current_values[YEAR] = {} - self.previous_values[YEAR] = {} + _reset_items = set() + + # stündlich zu berechnende Items hinzufügen + _todo_items.update(set(self._ondemand_hourly_items())) + # cache dict leeren + self.current_values[HOUR] = {} + self.previous_values[HOUR] = {} + + # wenn aktuelle Stunde == 0, werden auch die täglichen Items berechnet + if self.shtime.now().hour == 0: + # item zur Aufgabeliste hinzufügen + _todo_items.update(set(self._ondemand_daily_items())) + # cache dict leeren + self.current_values[DAY] = {} + self.previous_values[DAY] = {} + self.value_list_raw_data = {} + # reset Item-Wert alle onchange + _reset_items.update(set(self._onchange_daily_items())) + + # wenn zusätzlich der Wochentag == Montag, werden auch die wöchentlichen Items berechnet + if self.shtime.weekday(self.shtime.today()) == 1: + # item zur Aufgabeliste hinzufügen + _todo_items.update(set(self._ondemand_weekly_items())) + # cache dict leeren + self.current_values[WEEK] = {} + self.previous_values[WEEK] = {} + # reset Item-Wert alle onchange + _reset_items.update(set(self._onchange_weekly_items())) + + # wenn zusätzlich der erste Tage eines Monates ist, werden auch die monatlichen Items berechnet + if self.shtime.now().day == 1: + # item zur Aufgabeliste hinzufügen + _todo_items.update(set(self._ondemand_monthly_items())) + # cache dict leeren + self.current_values[MONTH] = {} + self.previous_values[MONTH] = {} + # reset Item-Wert alle onchange + _reset_items.update(set(self._onchange_monthly_items())) + + # wenn zusätzlich der erste Monat ist, werden auch die jährlichen Items berechnet + if self.shtime.now().month == 1: + # item zur Aufgabeliste hinzufügen + _todo_items.update(set(self._ondemand_yearly_items())) + # cache dict leeren + self.current_values[YEAR] = {} + self.previous_values[YEAR] = {} + # reset Item-Wert alle onchange + _reset_items.update(set(self._onchange_yearly_items())) + + # reset der onchange items + [_item(0, self.get_shortname()) for _item in _reset_items] return list(_todo_items) - if self.execute_debug: + if self.debug_log.execute: self.logger.debug(f"execute_items called with {option=}") if self.suspended: self.logger.info(f"Plugin is suspended. No items will be calculated.") return + suspended_items = self._suspended_items() + if len(suspended_items) > 0: + self.logger.info(f"{len(suspended_items)} are suspended and will not be calculated.") + todo_items = [] if option == 'startup': todo_items = self._startup_items() @@ -794,9 +979,22 @@ def _create_due_items() -> list: todo_items = self._all_items() elif option == 'due': todo_items = _create_due_items() + elif option == 'item': + if isinstance(item, str): + item = self.items.return_item(item) + if isinstance(item, Item): + todo_items = [item] + # remove suspended items + if option != 'item': + todo_items = list(set(todo_items) - set(suspended_items)) + + # put to queue self.logger.info(f"{len(todo_items)} items will be calculated for {option=}.") + if self.debug_log.execute: + self.logger.debug(f"Items to be calculated: {todo_items=}") [self.item_queue.put(i) for i in todo_items] + return True def work_item_queue(self) -> None: """Handles item queue were all to be executed items were be placed in.""" @@ -804,20 +1002,34 @@ def work_item_queue(self) -> None: while self.alive: try: queue_entry = self.item_queue.get(True, 10) + self.logger.debug(f"{queue_entry=}") except queue.Empty: self.active_queue_item = '-' pass else: if isinstance(queue_entry, tuple): item, value = queue_entry - self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-change' item '{item.path()}' with {value=} will be processed.") + self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'onchange' item={item.path()} with {value=} will be processed.") self.active_queue_item = str(item.path()) self.handle_onchange(item, value) else: - self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-demand' item '{queue_entry.path()}' will be processed.") + self.logger.info(f"# {self.item_queue.qsize() + 1} item(s) to do. || 'on-demand' item={queue_entry.path()} will be processed.") self.active_queue_item = str(queue_entry.path()) self.handle_ondemand(queue_entry) + def work_update_item_delay_deque(self): + """check if entries in update_item_delay_deque are due, if so put it to working queue""" + + while self.update_item_delay_deque: + update_time = self.update_item_delay_deque[0][2] + if update_time <= int(time.time()): + [item, value, *_] = self.update_item_delay_deque.popleft() + self.logger.info(f"+ Updated item '{item.path()}' with value {item()} is now due to be put to queue for processing. {self.item_queue.qsize() + 1} items to do.") + self.item_queue.put((item, value)) + else: + self.logger.debug(f"Remaining {len(self.update_item_delay_deque)} items in deque are not due, yet.") + break + def handle_ondemand(self, item: Item) -> None: """ Calculate value for requested item, fill cache dicts and set item value. @@ -827,7 +1039,7 @@ def handle_ondemand(self, item: Item) -> None: # get parameters item_config = self.get_item_config(item) - if self.ondemand_debug: + if self.debug_log.ondemand: self.logger.debug(f"Item={item.path()} with {item_config=}") db_addon_fct = item_config['db_addon_fct'] database_item = item_config['database_item'] @@ -838,7 +1050,7 @@ def handle_ondemand(self, item: Item) -> None: else: params = {} - if self.ondemand_debug: + if self.debug_log.ondemand: self.logger.debug(f"{db_addon_fct=} will _query_item with {params=}.") # handle item starting with 'verbrauch_' @@ -846,7 +1058,7 @@ def handle_ondemand(self, item: Item) -> None: result = self._handle_verbrauch(params) if result and result < 0: - self.logger.warning(f"Result of item {item.path()} with {db_addon_fct=} was negative. Something seems to be wrong.") + self.logger.info(f"Result of item {item.path()} with {db_addon_fct=} was negative. Something seems to be wrong.") # handle 'serie_verbrauch' elif db_addon_fct in SERIE_ATTRIBUTES_VERBRAUCH: @@ -867,7 +1079,7 @@ def handle_ondemand(self, item: Item) -> None: # handle TAGESMITTEL_ATTRIBUTES_TIMEFRAME like tagesmitteltemperatur_heute_minus1 elif db_addon_fct in TAGESMITTEL_ATTRIBUTES_TIMEFRAME: - params.update({'method': 'avg_hour'}) + params.update({'data_con_func': 'first_hour_avg_day'}) _result = self._prepare_value_list(**params) if isinstance(_result, list): @@ -875,6 +1087,14 @@ def handle_ondemand(self, item: Item) -> None: else: result = None + # handle all functions using temperature sums + elif db_addon_fct in ALL_SUMME_ATTRIBUTES: + new_params = {} + for entry in ('threshold', 'variant', 'result', 'data_con_func'): + if entry in params: + new_params.update({entry: params[entry]}) + result = self._handle_temp_sums(func=db_addon_fct, database_item=database_item, year=params.get('year'), month=params.get('month'), ignore_value_list=params.get('ignore_value_list'), params=new_params) + # handle info functions elif db_addon_fct == 'info_db_version': result = self._get_db_version() @@ -887,27 +1107,12 @@ def handle_ondemand(self, item: Item) -> None: elif db_addon_fct == 'general_oldest_log': result = self._get_oldest_log(database_item) - # handle kaeltesumme - elif db_addon_fct == 'kaeltesumme': - result = self._handle_kaeltesumme(database_item=database_item, year=params.get('year'), month=params.get('month')) - - # handle waermesumme - elif db_addon_fct == 'waermesumme': - result = self._handle_waermesumme(database_item=database_item, year=params.get('year'), month=params.get('month')) - - # handle gruenlandtempsumme - elif db_addon_fct == 'gruenlandtempsumme': - result = self._handle_gruenlandtemperatursumme(database_item=database_item, year=params.get('year')) - - # handle wachstumsgradtage - elif db_addon_fct == 'wachstumsgradtage': - result = self._handle_wachstumsgradtage(database_item=database_item, year=params.get('year')) - + # handle everything else else: result = self._query_item(**params)[0][1] # log result - if self.ondemand_debug: + if self.debug_log.ondemand: self.logger.debug(f"result is {result} for item '{item.path()}' with '{db_addon_fct=}'") if result is None: @@ -932,7 +1137,7 @@ def handle_minmax(): cache_dict = self.current_values[timeframe] init = False - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"'minmax' Item={updated_item.path()} with {func=} and {timeframe=} detected. Check for update of cache_dicts {cache_dict=} and item value.") # make sure, that database item is in cache dict @@ -942,55 +1147,55 @@ def handle_minmax(): # get _recent_value; if not already cached, create cache cached_value = cache_dict[database_item].get(func) if cached_value is None: - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"{func} value for {timeframe=} of item={updated_item.path()} not in cache dict. Query database.") query_params = {'func': func, 'database_item': database_item, 'timeframe': timeframe, 'start': 0, 'end': 0, 'ignore_value_list': ignore_value_list, 'use_oldest_entry': True} cached_value = self._query_item(**query_params)[0][1] if cached_value is None: - if self.onchange_debug: - self.logger.debug(f"{func} value for {timeframe=} of item={updated_item.path()} not available in database. Abort calculation.") + if self.debug_log.onchange: + self.logger.debug(f"{func} value for {timeframe=} of item={updated_item.path()} not available in database. Abort calculation.") return init = True - # if value not given -> read + # if value not given if init: - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"initial {func} value for {timeframe=} of item={item.path()} with will be set to {cached_value}") cache_dict[database_item][func] = cached_value return cached_value # check value for update of cache dict min elif func == 'min' and value < cached_value: - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"new value={value} lower then current min_value={cached_value} for {timeframe=}. cache_dict will be updated") cache_dict[database_item][func] = value return value # check value for update of cache dict max elif func == 'max' and value > cached_value: - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"new value={value} higher then current max_value={cached_value} for {timeframe=}. cache_dict will be updated") cache_dict[database_item][func] = value return value # no impact - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"new value={value} will not change max/min for period={timeframe}.") return None def handle_verbrauch(): cache_dict = self.previous_values[timeframe] - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"'verbrauch' item {updated_item.path()} with {func=} and {value=} detected. Check for update of cache_dicts {cache_dict=} and item value.") # get _cached_value for value at end of last period; if not already cached, create cache cached_value = cache_dict.get(database_item) if cached_value is None: - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"Most recent value for last {timeframe=} of item={updated_item.path()} not in cache dict. Query database.") # try to get most recent value of last timeframe, assuming that this is the value at end of last timeframe @@ -1002,30 +1207,31 @@ def handle_verbrauch(): return cache_dict[database_item] = cached_value - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"Value for Item={updated_item.path()} at end of last {timeframe} not in cache dict. Value={cached_value} has been added.") # calculate value, set item value, put data into plugin_item_dict _new_value = value - cached_value - return _new_value if isinstance(_new_value, int) else round(_new_value, 1) + return _new_value if isinstance(_new_value, int) else round(_new_value, 2) def handle_tagesmittel(): - result = self._prepare_value_list(database_item=database_item, timeframe='day', start=0, end=0, ignore_value_list=ignore_value_list, method='first_hour') + result = self._prepare_value_list(database_item=database_item, timeframe='day', start=0, end=0, ignore_value_list=ignore_value_list, data_con_func='first_hour') if isinstance(result, list): return result[0][1] - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"called with updated_item={updated_item.path()} and value={value}.") - relevant_item_list = set(self.get_item_list('database_item', updated_item)) & set(self.get_item_list('cycle', 'on-change')) + relevant_item_list = set(self.get_item_list('database_item', updated_item)) & set(self.get_item_list('on', 'change')) - if self.onchange_debug: + if self.debug_log.onchange: self.logger.debug(f"Following items where identified for update: {relevant_item_list}.") for item in relevant_item_list: item_config = self.get_item_config(item) - self.logger.debug(f"Item={item.path()} with {item_config=}") + if self.debug_log.onchange: + self.logger.debug(f"Item={item.path()} with {item_config=}") db_addon_fct = item_config['db_addon_fct'] database_item = item_config['database_item'] timeframe = item_config['query_params']['timeframe'] @@ -1033,21 +1239,21 @@ def handle_tagesmittel(): ignore_value_list = item_config['query_params'].get('ignore_value_list') new_value = None - # handle all on_change functions - if db_addon_fct not in ALL_ONCHANGE_ATTRIBUTES: - if self.onchange_debug: - self.logger.debug(f"non on-change function detected. Skip update.") + # handle all non on_change functions + if db_addon_fct not in ONCHANGE_ATTRIBUTES: + if self.debug_log.onchange: + self.logger.debug(f"non onchange function detected. Skip update.") continue - # handle minmax on-change items tagesmitteltemperatur_heute, minmax_heute_avg - if db_addon_fct in ['tagesmitteltemperatur_heute', 'minmax_heute_avg']: + # handle minmax onchange items tagesmitteltemperatur_heute, minmax_heute_avg + if db_addon_fct in TAGESMITTEL_ATTRIBUTES_ONCHANGE: new_value = handle_tagesmittel() - # handle minmax on-change items like minmax_heute_max, minmax_heute_min, minmax_woche_max, minmax_woche_min..... + # handle minmax onchange items like minmax_heute_max, minmax_heute_min, minmax_woche_max, minmax_woche_min..... elif db_addon_fct.startswith('minmax'): new_value = handle_minmax() - # handle verbrauch on-change items ending with heute, woche, monat, jahr + # handle verbrauch onchange items ending with heute, woche, monat, jahr elif db_addon_fct.startswith('verbrauch'): new_value = handle_verbrauch() @@ -1060,6 +1266,7 @@ def handle_tagesmittel(): item(new_value, self.get_shortname()) def _update_database_items(self) -> None: + """Turns given as database_item path into database_items""" for item in self._database_item_path_items(): item_config = self.get_item_config(item) database_item_path = item_config.get('database_item') @@ -1074,8 +1281,20 @@ def _update_database_items(self) -> None: if db_addon_startup: item_config.update({'startup': True}) + def _suspend_item_calculation(self, item: Union[str, Item], suspended: bool = False) -> Union[bool, None]: + """suspend calculation od decicated item""" + if isinstance(item, str): + item = self.items.return_item(item) + + if not isinstance(item, Item): + return + + item_config = self.get_item_config(item) + item_config['suspended'] = suspended + return suspended + @property - def log_level(self): + def log_level(self) -> int: return self.logger.getEffectiveLevel() def queue_backlog(self) -> int: @@ -1088,7 +1307,43 @@ def _startup_items(self) -> list: return self.get_item_list('startup', True) def _onchange_items(self) -> list: - return self.get_item_list('cycle', 'on-change') + return self.get_item_list('on', 'change') + + def _onchange_hourly_items(self) -> list: + return list(set(self._onchange_items()) & set(self._hourly_items())) + + def _onchange_daily_items(self) -> list: + return list(set(self._onchange_items()) & set(self._daily_items())) + + def _onchange_weekly_items(self) -> list: + return list(set(self._onchange_items()) & set(self._weekly_items())) + + def _onchange_monthly_items(self) -> list: + return list(set(self._onchange_items()) & set(self._monthly_items())) + + def _onchange_yearly_items(self) -> list: + return list(set(self._onchange_items()) & set(self._yearly_items())) + + def _ondemand_items(self) -> list: + return self.get_item_list('on', 'demand') + + def _ondemand_hourly_items(self) -> list: + return list(set(self._ondemand_items()) & set(self._hourly_items())) + + def _ondemand_daily_items(self) -> list: + return list(set(self._ondemand_items()) & set(self._daily_items())) + + def _ondemand_weekly_items(self) -> list: + return list(set(self._ondemand_items()) & set(self._weekly_items())) + + def _ondemand_monthly_items(self) -> list: + return list(set(self._ondemand_items()) & set(self._monthly_items())) + + def _ondemand_yearly_items(self) -> list: + return list(set(self._ondemand_items()) & set(self._yearly_items())) + + def _hourly_items(self) -> list: + return self.get_item_list('cycle', 'hourly') def _daily_items(self) -> list: return self.get_item_list('cycle', 'daily') @@ -1117,8 +1372,8 @@ def _database_items(self) -> list: def _database_item_path_items(self) -> list: return self.get_item_list('database_item_path', True) - def _ondemand_items(self) -> list: - return self._daily_items() + self._weekly_items() + self._monthly_items() + self._yearly_items() + self._static_items() + def _suspended_items(self) -> list: + return self.get_item_list('suspended', True) def _all_items(self) -> list: # return self._ondemand_items() + self._onchange_items() + self._static_items() + self._admin_items() + self._info_items() @@ -1128,25 +1383,26 @@ def _all_items(self) -> list: # Public functions / Using item_path ######################################### - def gruenlandtemperatursumme(self, item_path: str, year: Union[int, str] = None) -> Union[int, None]: + def gruenlandtemperatursumme(self, item_path: str, year: Union[int, str] = None, ignore_value_list: list = None) -> Union[int, None]: """ Query database for gruenlandtemperatursumme for given year or year https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme - Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Düngungsmaßnahmen zu bestimmen. + Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Duengemaßnahmen zu bestimmen. Dabei erfolgt die Aufsummierung der Tagesmitteltemperaturen über 0 °C, wobei der Januar mit 0.5 und der Februar mit 0.75 gewichtet wird. Bei einer Wärmesumme von 200 Grad ist eine Düngung angesagt. :param item_path: item object or item_id for which the query should be done :param year: year the gruenlandtemperatursumme should be calculated for + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query :return: gruenlandtemperatursumme """ item = self.items.return_item(item_path) if item: - return self._handle_gruenlandtemperatursumme(item, year) + return self._handle_temp_sums(func='gruendlandtempsumme', database_item=item, year=year, ignore_value_list=ignore_value_list) - def waermesumme(self, item_path: str, year: Union[int, str] = None, month: Union[int, str] = None, threshold: int = 0) -> Union[int, None]: + def waermesumme(self, item_path: str, year: Union[int, str] = None, month: Union[int, str] = None, ignore_value_list: list = None, threshold: int = 0) -> Union[int, None]: """ Query database for waermesumme for given year or year/month https://de.wikipedia.org/wiki/W%C3%A4rmesumme @@ -1154,15 +1410,16 @@ def waermesumme(self, item_path: str, year: Union[int, str] = None, month: Union :param item_path: item object or item_id for which the query should be done :param year: year the waermesumme should be calculated for :param month: month the waermesumme should be calculated for + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query :param threshold: threshold for temperature :return: waermesumme """ item = self.items.return_item(item_path) if item: - return self._handle_waermesumme(item, year, month, threshold) + return self._handle_temp_sums(func='waermesumme', database_item=item, year=year, month=month, ignore_value_list=ignore_value_list, params={'threshold': threshold}) - def kaeltesumme(self, item_path: str, year: Union[int, str] = None, month: Union[int, str] = None) -> Union[int, None]: + def kaeltesumme(self, item_path: str, year: Union[int, str] = None, month: Union[int, str] = None, ignore_value_list: list = None) -> Union[int, None]: """ Query database for kaeltesumme for given year or year/month https://de.wikipedia.org/wiki/K%C3%A4ltesumme @@ -1170,70 +1427,62 @@ def kaeltesumme(self, item_path: str, year: Union[int, str] = None, month: Union :param item_path: item object or item_id for which the query should be done :param year: year the kaeltesumme should be calculated for :param month: month the kaeltesumme should be calculated for + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query :return: kaeltesumme """ item = self.items.return_item(item_path) if item: - return self._handle_kaeltesumme(item, year, month) - - def tagesmitteltemperatur(self, item_path: str, timeframe: str = None, count: int = None) -> list: - """ - Query database for tagesmitteltemperatur - https://www.dwd.de/DE/leistungen/klimadatendeutschland/beschreibung_tagesmonatswerte.html - - :param item_path: item object or item_id for which the query should be done - :param timeframe: time increment for determination - :param count: number of time increments starting from now to the left (into the past) - :return: tagesmitteltemperatur - """ - - if not timeframe: - timeframe = 'day' - - if not count: - count = 0 - - item = self.items.return_item(item_path) - if item: - count = to_int(count) - end = 0 - start = end + count - query_params = {'database_item': item, 'func': 'max', 'timeframe': harmonize_timeframe_expression(timeframe), 'start': start, 'end': end} - return self._handle_tagesmitteltemperatur(**query_params) + return self._handle_temp_sums(func='kaeltesumme', database_item=item, year=year, month=month, ignore_value_list=ignore_value_list) - def wachstumsgradtage(self, item_path: str, year: Union[int, str] = None, method: int = 0, threshold: int = 10) -> Union[int, None]: + def wachstumsgradtage(self, item_path: str, year: Union[int, str] = None, ignore_value_list: list = None, variant: int = 0, threshold: int = 10) -> Union[int, None]: """ Query database for wachstumsgradtage https://de.wikipedia.org/wiki/Wachstumsgradtag :param item_path: item object or item_id for which the query should be done :param year: year the wachstumsgradtage should be calculated for - :param method: method to be used + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query + :param variant: variant to be used :param threshold: Temperature in °C as threshold: Ein Tage mit einer Tagesdurchschnittstemperatur oberhalb des Schwellenwertes gilt als Wachstumsgradtag :return: wachstumsgradtage """ item = self.items.return_item(item_path) if item: - return self._handle_wachstumsgradtage(database_item=item, year=year, method=method, threshold=threshold) + return self._handle_temp_sums(func='wachstumsgradtage', database_item=item, year=year, ignore_value_list=ignore_value_list, params={'threshold': threshold, 'variant': variant}) - def temperaturserie(self, item_path: str, year: Union[int, str] = None, method: str = 'avg_hour') -> Union[list, None]: + def temperaturserie(self, item_path: str, year: Union[int, str] = None, ignore_value_list: list = None, data_con_func: str = 'first_hour_avg_day') -> Union[list, None]: """ - Query database for wachstumsgradtage - https://de.wikipedia.org/wiki/Wachstumsgradtag + Query database for temperaturserie :param item_path: item object or item_id for which the query should be done :param year: year the wachstumsgradtage should be calculated for - :param method: Calculation method - :return: wachstumsgradtage + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query + :param data_con_func: data concentration function + :return: temperature series """ item = self.items.return_item(item_path) if item: - return self._handle_temperaturserie(item, year, method) + return self._handle_temp_sums(func='temperaturserie', database_item=item, year=year, ignore_value_list=ignore_value_list, params={'data_con_func': data_con_func}) def query_item(self, func: str, item_path: str, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value_list=None) -> list: + """ + Query database, format response and return it + + :param func: function, defined in query_item method to be used at query + :param item_path: item str or item_id for which the query should be done + :param timeframe: time increment für definition of start, end, count (day, week, month, year) + :param start: start of timeframe (oldest) for query given in x time increments (default = None, meaning complete database) + :param end: end of timeframe (newest) for query given in x time increments (default = 0, meaning today, end of last week, end of last month, end of last year) + :param group: first grouping parameter (default = None, possible values: day, week, month, year) + :param group2: second grouping parameter (default = None, possible values: day, week, month, year) + :param ignore_value_list: list of comparison operators for val_num, which will be applied during query + + :return: formatted query response + """ + item = self.items.return_item(item_path) if item is None: return [] @@ -1244,7 +1493,7 @@ def fetch_log(self, func: str, item_path: str, timeframe: str, start: int = None """ Query database, format response and return it - :param func: function to be used at query + :param func: sql function to be used at query :param item_path: item str or item_id for which the query should be done :param timeframe: time increment für definition of start, end, count (day, week, month, year) :param start: start of timeframe (oldest) for query given in x time increments (default = None, meaning complete database) @@ -1295,11 +1544,11 @@ def suspend(self, state: bool = False) -> bool: """ if state: - self.logger.warning("Plugin is set to 'suspended'. Queries to database will not be made until suspension is cancelled.") + self.logger.info("Plugin is set to 'suspended'. Queries to database will not be made until suspension is cleared.") self.suspended = True self._clear_queue() else: - self.logger.warning("Plugin suspension cancelled. Queries to database will be resumed.") + self.logger.info("Plugin suspension cleared. Queries to database will be resumed.") self.suspended = False # write back value to item, if one exists @@ -1319,22 +1568,18 @@ def _handle_verbrauch(self, query_params: dict) -> Union[None, float]: Ermittlung des Verbrauches innerhalb eines Zeitraumes Die Vorgehensweise ist: - - Endwert: Abfrage des letzten Eintrages im Zeitraum - - Ergibt diese Abfrage einen Wert, gab eines einen Eintrag im Zeitraum in der DB, es wurde also etwas verbraucht, dann entspricht dieser dem Endzählerstand + - Endwert / Endzählerstand: Abfrage des letzten Eintrages (Zählerstandes) im Zeitraum - Ergibt diese Abfrage keinen Wert, gab eines keinen Eintrag im Zeitraum in der DB, es wurde also nichts verbraucht -> Rückgabe von 0 - - Startwert: Abfrage des letzten Eintrages im Zeitraum vor dem Abfragezeitraum - - Ergibt diese Abfrage einen Wert, entspricht dieser dem Zählerstand am Ende des Zeitraumes vor dem Abfragezeitraum - - Ergibt diese Abfrage keinen Wert, wurde in Zeitraum, vor dem Abfragezeitraum nichts verbraucht, der Anfangszählerstand kann so nicht ermittelt werden. - - Abfrage des nächsten Wertes vor dem Zeitraum - - Ergibt diese Abfrage einen Wert, entspricht dieser dem Anfangszählerstand - - Ergibt diese Abfrage keinen Wert, Anfangszählerstand = 0 + - Startwert / Anfangszählerstand: Abfrage des letzten Eintrages (Zählerstandes) vor dem Abfragezeitraum + - Ergibt diese Abfrage einen Wert, entspricht dieser dem Anfangszählerstand + - Ergibt diese Abfrage keinen Wert, Anfangszählerstand = 0 """ # define start, end for verbrauch_jahreszeitraum_timedelta if 'timedelta' in query_params: timedelta = query_params.pop('timedelta') - today = datetime.date.today() - start_date = datetime.date(today.year, 1, 1) - relativedelta(years=timedelta) + today = self.shtime.today(offset=0) + start_date = self.shtime.beginning_of_year(offset=-timedelta) end_date = today - relativedelta(years=timedelta) start = (today - start_date).days end = (today - end_date).days @@ -1343,52 +1588,41 @@ def _handle_verbrauch(self, query_params: dict) -> Union[None, float]: end = query_params['end'] # calculate consumption - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"called with {query_params=}") # get value for end and check it; - query_params.update({'func': 'last', 'start': end, 'end': end}) + query_params.update({'func': 'last', 'start': start, 'end': end}) value_end = self._query_item(**query_params)[0][1] - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"{value_end=}") if value_end is None or value_end == 0: return value_end # get value for start and check it; - query_params.update({'func': 'last', 'start': start, 'end': start}) + query_params.update({'func': 'next', 'start': start, 'end': start}) value_start = self._query_item(**query_params)[0][1] - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"{value_start=}") - if value_start is None: - if self.prepare_debug: - self.logger.debug(f"Error occurred during query. Return.") - return - - if not value_start: - self.logger.info(f"No DB Entry found for requested start date. Looking for next recent DB entry.") - query_params.update({'func': 'next'}) - value_start = self._query_item(**query_params)[0][1] - if self.prepare_debug: - self.logger.debug(f"next recent value is {value_start=}") - if not value_start: value_start = 0 - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"No start value available. Will be set to 0 as default") # calculate consumption consumption = value_end - value_start - if self.prepare_debug: - self.logger.debug(f"{consumption=}") if isinstance(consumption, float): if consumption.is_integer(): consumption = int(consumption) else: - consumption = round(consumption, 1) + consumption = round(consumption, 2) + + if self.debug_log.prepare: + self.logger.debug(f"{consumption=}") return consumption @@ -1400,53 +1634,63 @@ def _handle_verbrauch_serie(self, query_params: dict) -> list: timeframe = query_params['timeframe'] start = query_params['start'] - for i in range(1, start): + for i in range(start, 1, -1): value = self._handle_verbrauch({'database_item': database_item, 'timeframe': timeframe, 'start': i + 1, 'end': i}) - ts_start, ts_end = get_start_end_as_timestamp(timeframe, i, i + 1) + ts_start, ts_end = self._get_start_end_as_timestamp(timeframe, i, i + 1) series.append([ts_end, value]) return series + def _handle_verbrauch_serie_new(self, query_params: dict) -> list: + """Ermittlung einer Serie von Verbräuchen in einem Zeitraum für x Zeiträume""" + + # ToDo: Test method + + query_params.update({'data_con_func': 'max_day', 'cache': True}) + raw_data = self._prepare_value_list(**query_params) + + new_dict = {k[0]: k[1:][0] for k in raw_data} + consumption_list = [] + start_ts = min(new_dict) + start_val = new_dict[start_ts] + + for i in range(query_params['start']): + end_ts = int(start_ts + 24 * 60 * 60) + end_val = new_dict.get(end_ts, None) + if not end_val: + end_val = start_val + consumption_list.append([end_ts, round((end_val - start_val), 2)]) + start_ts = end_ts + start_val = end_val + + return consumption_list + def _handle_zaehlerstand(self, query_params: dict) -> Union[float, int, None]: """ - Ermittlung des Zählerstandes zum Ende eines Zeitraumes + Ermittlung des Zählerstandes zu Beginn des Zeitraumes Die Vorgehensweise ist: - - Abfrage des letzten Eintrages im Zeitraum - - Ergibt diese Abfrage einen Wert, entspricht dieser dem Zählerstand - - Ergibt diese Abfrage keinen Wert, dann - - Abfrage des nächsten Wertes vor dem Zeitraum - - Ergibt diese Abfrage einen Wert, entspricht dieser dem Zählerstand - - Ergibt diese Abfrage keinen Wert, dann Rückgabe von None + - Abfrage des letzten Eintrages vor dem Beginn des Zeitraums """ - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"called with {query_params=}") # get last value of timeframe - query_params.update({'func': 'last'}) + query_params.update({'func': 'next'}) last_value = self._query_item(**query_params)[0][1] - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"{last_value=}") if last_value is None: - if self.prepare_debug: - self.logger.debug(f"Error occurred during query. Return.") - return - - if not last_value: - # get last value (next) before timeframe - self.logger.info(f"No DB Entry found for requested start date. Looking for next recent DB entry.") - query_params.update({'func': 'next'}) - last_value = self._query_item(**query_params)[0][1] - if self.prepare_debug: - self.logger.debug(f"next recent value is {last_value=}") + self.logger.info('No entry in database found. Maybe item was just created. Setting last_value to 0.') + last_value = 0 if isinstance(last_value, float): if last_value.is_integer(): last_value = int(last_value) else: - last_value = round(last_value, 1) + last_value = round(last_value, 2) return last_value @@ -1458,107 +1702,110 @@ def _handle_zaehlerstand_serie(self, query_params: dict) -> list: timeframe = query_params['timeframe'] start = query_params['start'] - for i in range(1, start): + for i in range(start, 1, -1): value = self._handle_zaehlerstand({'database_item': database_item, 'timeframe': timeframe, 'start': i, 'end': i}) - ts_start = get_start_end_as_timestamp(timeframe, i, i)[0] + ts_start = self._get_start_end_as_timestamp(timeframe, i, i)[0] series.append([ts_start, value]) return series - def _handle_kaeltesumme(self, database_item: Item, year: Union[int, str] = None, month: Union[int, str] = None) -> Union[int, None]: - """ - Query database for kaeltesumme for given year or year/month - https://de.wikipedia.org/wiki/K%C3%A4ltesumme - - :param database_item: item object or item_id for which the query should be done - :param year: year the kaeltesumme should be calculated for - :param month: month the kaeltesumme should be calculated for - :return: kaeltesumme - """ + def _handle_zaehlerstand_serie_new(self, query_params: dict) -> list: + """Ermittlung einer Serie von Zählerständen zum Ende eines Zeitraumes für x Zeiträume""" - if self.prepare_debug: - self.logger.debug(f"called with {database_item=}, {year=}, {month=}") + # ToDo: Test method - # check validity of given year - if not valid_year(year): - self.logger.error(f"Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") - return + query_params.update({'data_con_func': 'max_day', 'cache': True}) + raw_data = self._prepare_value_list(**query_params) - # set default year - if not year: - year = 'current' + new_dict = {k[0]: k[1:][0] for k in raw_data} + zaehler_list = [] + start_ts = min(new_dict) + start_val = new_dict[start_ts] - # define year - if year == 'current': - if datetime.date.today() < datetime.date(int(datetime.date.today().year), 9, 21): - year = datetime.date.today().year - 1 - else: - year = datetime.date.today().year + for i in range(query_params['start']): + end_ts = int(start_ts + 24 * 60 * 60) + end_val = new_dict.get(end_ts, None) + if not end_val: + end_val = start_val + zaehler_list.append([end_ts, round(end_val, 2)]) + start_ts = end_ts + start_val = end_val - # define start_date and end_date - if month is None: - start_date = datetime.date(int(year), 9, 21) - end_date = datetime.date(int(year) + 1, 3, 22) - elif valid_month(month): - start_date = datetime.date(int(year), int(month), 1) - end_date = start_date + relativedelta(months=+1) - datetime.timedelta(days=1) - else: - self.logger.error(f"Month for item={database_item.path()} was {month}. This is not a valid month. Query cancelled.") - return + return zaehler_list - # define start / end - today = datetime.date.today() - if start_date > today: - self.logger.error(f"Start time for query of item={database_item.path()} is in future. Query cancelled.") - return - - start = (today - start_date).days - end = (today - end_date).days if end_date < today else 0 - if start < end: - self.logger.error(f"End time for query of item={database_item.path()} is before start time. Query cancelled.") - return - - # get raw data as list - if self.prepare_debug: - self.logger.debug("try to get raw data") - raw_data = self._prepare_value_list(database_item=database_item, timeframe='day', start=start, end=end, method='avg_hour') - if self.execute_debug: - self.logger.debug(f"raw_value_list={raw_data=}") - - # calculate value - if raw_data is None: - return - elif isinstance(raw_data, list): - # akkumulieren alle negativen Werte + def _handle_temp_sums(self, func: str, database_item: Item, year: Union[int, str] = None, month: Union[int, str] = None, ignore_value_list: list = None, params: dict = None) -> Union[list, None]: + """ + Calculates diverse temperature sums and day counts + + :param func: defines which temperature sum or count should be calculated + :param database_item: item object or item_id for which the query should be done + :param year: year the kaeltesumme should be calculated for + :param month: month the kaeltesumme should be calculated for + :param params: params to be used for executing function (see below) + :return: temperature sum or day count + + - kaeltesumme: Kältesumme nach https://de.wikipedia.org/wiki/K%C3%A4ltesumme + - waermesumme: Wärmesumme https://de.wikipedia.org/wiki/W%C3%A4rmesumme + params: threshold + - gruenlandtempsumme: Grünlandtemperatursumme: https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme + - wachstumsgradtage: Wachstumsgradtage https://de.wikipedia.org/wiki/Wachstumsgradtag + params: threshold, variant, result + - temperaturserie: Temperaturserie provide list of lists having timestamp and temperature(s) per day + params: data_con_func + - wuestentage: Wüstentage, Anzahl der Tage mit Tmax ≥ 35 °C + - heisse_tage: Heiße Tage, Anzahl der Tage mit Tmax ≥ 30 °C + - tropennaechte: Tropennächte, Anzahl der Tage mit Tmin ≥ 20 °C + - sommertage: Sommertage, Anzahl der Tage mit Tmax ≥ 25 °C + - heiztage: Heiztage, Anzahl der Tage mit Tmed < 15 °C / 12 °C + - vegetationstage: Vegetationstage, Anzahl der Tage mit Tmed ≥ 5 °C + - frosttage: Frosttage, Anzahl der Tage mit Tmin < 0 °C + - eistage: Eistage, Anzahl der Tage mit Tmax < 0 °C + """ + + timeframe = {1: ((0, 9, 21), (1, 3, 22)), + 2: ((0, 1, 1), (0, 9, 21)), + 3: ((0, 1, 1), (0, 12, 31))} + + defaults = {'kaeltesumme': {'start_end': timeframe[1], 'data_con_func': 'first_hour_avg_day'}, + 'waermesumme': {'start_end': timeframe[2], 'data_con_func': 'first_hour_avg_day'}, + 'gruenlandtempsumme': {'start_end': timeframe[2], 'data_con_func': 'first_hour_avg_day'}, + 'wachstumsgradtage': {'start_end': timeframe[2], 'data_con_func': 'minmax_day'}, + 'temperaturserie': {'start_end': timeframe[2], 'data_con_func': params.get('data_con_func', 'avg_hour')}, + 'wuestentage': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + 'heisse_tage': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + 'tropennaechte': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + 'sommertage': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + 'heiztage': {'start_end': timeframe[3], 'data_con_func': 'first_hour_avg_day'}, + 'vegetationstage': {'start_end': timeframe[3], 'data_con_func': 'first_hour_avg_day'}, + 'frosttage': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + 'eistage': {'start_end': timeframe[3], 'data_con_func': 'minmax_day'}, + } + + if not params: + params = dict() + + def kaeltesumme() -> float: + """Berechnung der Kältesumme durch Akkumulieren aller negativen Tagesdurchschnittstemperaturen im Abfragezeitraum + + :return: value of waermesumme + """ + ks = 0 for entry in raw_data: if entry[1] < 0: ks -= entry[1] return int(round(ks, 0)) - def _handle_waermesumme(self, database_item: Item, year: Union[int, str] = None, month: Union[int, str] = None, threshold: int = 0) -> Union[int, None]: - """ - Query database for waermesumme for given year or year/month - https://de.wikipedia.org/wiki/W%C3%A4rmesumme - - :param database_item: item object or item_id for which the query should be done - :param year: year the waermesumme should be calculated for; "current" for current year - :param month: month the waermesumme should be calculated for - :return: waermesumme - """ - - # get raw data as list - raw_data = self._prepare_waermesumme(database_item=database_item, year=year, month=month) - if self.execute_debug: - self.logger.debug(f"raw_value_list={raw_data=}") + def waermesumme() -> float: + """Berechnung der Wärmesumme durch Akkumulieren aller Tagesdurchschnittstemperaturen im Abfragezeitraum, die größer/gleich dem Schwellenwert sind - # set threshold to min 0 - threshold = max(0, threshold) + :return: value of waermesumme + """ + + # get threshold and set to min 0 + threshold = params.get('threshold', 10) + threshold = max(0, threshold) - # calculate value - if raw_data is None: - return - elif isinstance(raw_data, list): # akkumulieren alle Werte, größer/gleich Schwellenwert ws = 0 for entry in raw_data: @@ -1566,32 +1813,17 @@ def _handle_waermesumme(self, database_item: Item, year: Union[int, str] = None, ws += entry[1] return int(round(ws, 0)) - def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, str] = None) -> Union[int, None]: - """ - Query database for gruenlandtemperatursumme for given year or year/month - https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme - - :param database_item: item object for which the query should be done - :param year: year the gruenlandtemperatursumme should be calculated for - :return: gruenlandtemperatursumme - """ - - # get raw data as list - raw_data = self._prepare_waermesumme(database_item=database_item, year=year) - if self.execute_debug: - self.logger.debug(f"raw_data={raw_data}") - - # calculate value - if raw_data is None: - return + def gruenlandtempsumme() -> float: + """Berechnung der Grünlandtemperatursumme durch Akkumulieren alle positiven Tagesmitteltemperaturen, im Januar gewichtet mit 50%, im Februar mit 75% - elif isinstance(raw_data, list): - # akkumulieren alle positiven Tagesmitteltemperaturen, im Januar gewichtet mit 50%, im Februar mit 75% + :return: value of gruenlandtempsumme + """ + gts = 0 for entry in raw_data: timestamp, value = entry if value > 0: - dt = datetime.datetime.fromtimestamp(timestamp / 1000) + dt = self._timestamp_to_datetime(timestamp / 1000) if dt.month == 1: value = value * 0.5 elif dt.month == 2: @@ -1599,185 +1831,142 @@ def _handle_gruenlandtemperatursumme(self, database_item: Item, year: Union[int, gts += value return int(round(gts, 0)) - def _handle_wachstumsgradtage(self, database_item: Item, year: Union[int, str] = None, method: int = 0, threshold: int = 10) -> Union[list, float, None]: - """ - Calculate "wachstumsgradtage" for given year with temperature threshold - https://de.wikipedia.org/wiki/Wachstumsgradtag - - :param database_item: item object or item_id for which the query should be done - :param year: year the wachstumsgradtage should be calculated for - :param method: calculation method to be used - :param threshold: temperature in °C as threshold for evaluation - :return: wachstumsgradtage - """ - - # set default year - if not year: - year = 'current' - - if not valid_year(year): - self.logger.error(f"Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") - return - - # define year - if year == 'current': - year = datetime.date.today().year - - # define start_date, end_date - start_date = datetime.date(int(year), 1, 1) - end_date = datetime.date(int(year), 9, 21) - - # check start_date - today = datetime.date.today() - if start_date > today: - self.logger.info(f"Start time for query of item={database_item.path()} is in future. Query cancelled.") - return - - # define start / end - start = (today - start_date).days - end = (today - end_date).days if end_date < today else 0 - - # check end - if start < end: - self.logger.error(f"End time for query of item={database_item.path()} is before start time. Query cancelled.") - return - - # get raw data as list - raw_data = self._prepare_value_list(database_item=database_item, timeframe='day', start=start, end=end, method='minmax_hour') - if self.execute_debug: - self.logger.debug(f"raw_value_list={raw_data}") - - # calculate value - if raw_data is None: - return - - if isinstance(raw_data, list): - # Die Berechnung des einfachen Durchschnitts // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert + def wachstumsgradtage() -> Union[list, float, None]: + """Berechnet die Wachstumsgradtage noch 3 möglichen Methoden und gibt entweder den Gesamtwert oder eine Liste mit kumulierten Werten pro Tag zurück + + variant 1: Berechnung des einfachen Durchschnitts + variant 2: modifizierte Berechnung des einfachen Durchschnitts. + variant 3: Zähle Tage, bei denen die Tagesmitteltemperatur oberhalb des Schwellenwertes lag + + result 'value': Rückgabe als Gesamtwert + result 'series: Rückgabe als Liste mit kumulierten Werten pro Tag zurück [['timestamp1', 'kumulierter Wert am Ende von Tag1'], ['timestamp2', ''kumulierter Wert am Ende von Tag2', [...], ...] + """ + + # define defaults wgte = 0 wgte_list = [] - if method == 0 or method == 10: + + # get threshold and set to min 0 + threshold = params.get('threshold', 10) + threshold = max(0, threshold) + + # get variant + variant = params.get('variant', 0) + + # get result type + result = params.get('result', 'value') + + # Berechnung des einfachen Durchschnitts + if variant == 0: self.logger.info(f"Calculate 'Wachstumsgradtag' according to 'Berechnung des einfachen Durchschnitts'.") - for entry in raw_data: - timestamp, min_val, max_val = entry - wgt = (((min_val + min(30, max_val)) / 2) - threshold) - if wgt > 0: - wgte += wgt - wgte_list.append([timestamp, int(round(wgte, 0))]) - if method == 0: - return int(round(wgte, 0)) - else: - return wgte_list - # Die modifizierte Berechnung des einfachen Durchschnitts. // akkumuliere positive Differenz aus Mittelwert aus Tagesminimaltemperatur mit mind Schwellentemperatur und Tagesmaximaltemperatur limitiert auf 30°C und Schwellenwert - elif method == 1 or method == 11: + elif variant == 1: self.logger.info(f"Calculate 'Wachstumsgradtag' according to 'Modifizierte Berechnung des einfachen Durchschnitts'.") - for entry in raw_data: - timestamp, min_val, max_val = entry - wgt = (((max(threshold, min_val) + min(30.0, max_val)) / 2) - threshold) - if wgt > 0: - wgte += wgt - wgte_list.append([timestamp, int(round(wgte, 0))]) - if method == 1: - return int(round(wgte, 0)) - else: - return wgte_list - # Zähle Tage, bei denen die Tagesmitteltemperatur oberhalb des Schwellenwertes lag - elif method == 2 or method == 12: + elif variant == 2: self.logger.info(f"Calculate 'Wachstumsgradtag' according to 'Anzahl der Tage, bei denen die Tagesmitteltemperatur oberhalb des Schwellenwertes lag'.") - for entry in raw_data: - timestamp, min_val, max_val = entry + else: + self.logger.warning(f"Requested variant of 'Wachstumsgradtag' not defined. Aborting...") + return + + for entry in raw_data: + timestamp, min_val, max_val = entry + + if variant == 0: + wgt = (((min_val + min(30, max_val)) / 2) - threshold) + elif variant == 1: + wgt = (((max(threshold, min_val) + min(30.0, max_val)) / 2) - threshold) + elif variant == 2: wgt = (((min_val + min(30, max_val)) / 2) - threshold) - if wgt > 0: - wgte += 1 - wgte_list.append([timestamp, wgte]) - if method == 0: - return wgte else: - return wgte_list + wgt = None - else: - self.logger.info(f"Method for 'Wachstumsgradtag' calculation not defined.'") + if wgt and wgt > 0: + wgte += wgt + wgte_list.append([timestamp, int(round(wgte, 0))]) - def _handle_temperaturserie(self, database_item: Item, year: Union[int, str] = None, method: str = 'avg_hour') -> Union[list, None]: - """ - provide list of lists having timestamp and temperature(s) per day + if result == 'series': + return wgte_list + else: + return int(round(wgte, 0)) - :param database_item: item object or item_id for which the query should be done - :param year: year the wachstumsgradtage should be calculated for - :param method: calculation method to be used - :return: list of temperatures - """ + def temperaturserie() -> list: + """provide list of lists having timestamp and temperature(s) per day""" - # set default year - if not year: - year = 'current' + return raw_data - if not valid_year(year): - self.logger.error(f"Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") - return + def wuestentage() -> int: + """provide number day counted as Wüstentag with Tmax ≥ 35°C""" + return _count(operator.ge, 'max', 35) - # define year - if year == 'current': - year = datetime.date.today().year + def heisse_tage() -> int: + """provide number day counted as heißer Tag with Tmax ≥ 30°C""" + return _count(operator.ge, 'max', 30) - # define start_date, end_date - start_date = datetime.date(int(year), 1, 1) - end_date = datetime.date(int(year), 12, 31) + def tropennaechte() -> int: + """provide number day counted as Tropnenacht with Tmin ≥ 20 °C""" + return _count(operator.ge, 'min', 20) - # check start_date - today = datetime.date.today() - if start_date > today: - self.logger.info(f"Start time for query of item={database_item.path()} is in future. Query cancelled.") - return + def sommertage() -> int: + """provide number day counted as Sommertag with Tmax ≥ 25°C""" + return _count(operator.ge, 'max', 25) - # define start / end - start = (today - start_date).days - end = (today - end_date).days if end_date < today else 0 + def frosttage() -> int: + """provide number day counted as Frosttag with Tmin < 0°C""" + return _count(operator.lt, 'min', 0) - # check end - if start < end: - self.logger.error(f"End time for query of item={database_item.path()} is before start time. Query cancelled.") - return + def eistage() -> int: + """provide number day counted as Frosttag with Tmax < 0°C""" + return _count(operator.lt, 'max', 0) - # get raw data as list - temp_list = self._prepare_value_list(database_item=database_item, timeframe='day', start=start, end=end, method=method) - if self.execute_debug: - self.logger.debug(f"{temp_list=}") + def heiztage() -> int: + """provide number day counted as Frosttag with Tavg < 15°C""" + return _count(operator.lt, 'avg', 15) + + def vegetationstage() -> int: + """provide number day counted as Frosttag with Tavg > 5°C""" + return _count(operator.ge, 'avg', 5) - return temp_list + def _count(op, minmax: str, limit: int) -> int: + count = 0 + for entry in raw_data: + value = entry[2] if minmax == 'max' else entry[1] + if op(value, limit): + count += 1 + return count - def _prepare_waermesumme(self, database_item: Item, year: Union[int, str] = None, month: Union[int, str] = None) -> Union[list, None]: - """Prepares raw data for waermesumme""" + self.logger.debug(f"{func=}, {database_item=}, {year=}, {month=}, {params=}") - # check validity of given year - if not valid_year(year): - self.logger.error(f"Year for item={database_item.path()} was {year}. This is not a valid year. Query cancelled.") + # check if func is defined + if func not in defaults: + self.logger.warning(f"_handle_temp_sums called with {func=}, which is not defined. Aborting...") return - # set default year - if not year: - year = 'current' + # get datetime of today + today = self.shtime.today(offset=0) - # define year - if year == 'current': - year = datetime.date.today().year + # define year or check validity of given year + if not year or year == 'current': + year = today.year + elif not self._valid_year(year): + self.logger.error(f"Year for item={database_item.path()} was {year}. This is not a valid year. Aborting...") + return # define start_date, end_date if month is None: - start_date = datetime.date(int(year), 1, 1) - end_date = datetime.date(int(year), 9, 21) - elif valid_month(month): + ((s_y, s_m, s_d), (e_y, e_m, e_d)) = defaults.get(func, {}).get('start_end', timeframe[3]) + start_date = datetime.date(int(year) + s_y, s_m, s_d) + end_date = datetime.date(int(year) + e_y, e_m, e_d) + elif self._valid_month(month): start_date = datetime.date(int(year), int(month), 1) end_date = start_date + relativedelta(months=+1) - datetime.timedelta(days=1) else: - self.logger.error(f"Month for item={database_item.path()} was {month}. This is not a valid month. Query cancelled.") + self.logger.error(f"Month for item={database_item.path()} was {month}. This is not a valid month. Aborting...") return # check start_date - today = datetime.date.today() if start_date > today: - self.logger.info(f"Start time for query of item={database_item.path()} is in future. Query cancelled.") + self.logger.info(f"Start time for query of item={database_item.path()} is in future. Aborting...") return # define start / end @@ -1786,13 +1975,25 @@ def _prepare_waermesumme(self, database_item: Item, year: Union[int, str] = None # check end if start < end: - self.logger.error(f"End time for query of item={database_item.path()} is before start time. Query cancelled.") + self.logger.error(f"End time for query of item={database_item.path()} is before start time. Aborting...") return + + # get raw data as list + if self.debug_log.prepare: + self.logger.debug("try to get raw data") + data_con_func = defaults.get(func, {}).get('data_con_func') + raw_data = self._prepare_value_list(database_item=database_item, timeframe='day', start=start, end=end, ignore_value_list=ignore_value_list, data_con_func=data_con_func, cache=True) + if self.debug_log.prepare: + self.logger.debug(f"raw_value_list={raw_data}") - # return raw data as list - return self._prepare_value_list(database_item=database_item, timeframe='day', start=start, end=end, method='avg_hour') + # return None, if now raw data + if raw_data is None or not isinstance(raw_data, list): + return + + # calculate value and return it + return locals()[func]() - def _prepare_value_list(self, database_item: Item, timeframe: str, start: int, end: int = 0, ignore_value_list=None, method: str = 'avg_hour') -> Union[list, None]: + def _prepare_value_list(self, database_item: Item, timeframe: str, start: int, end: int = 0, ignore_value_list=None, data_con_func: str = 'avg_day', cache: bool = False) -> Union[list, None]: """ returns list of lists having timestamp and values(s) per day / hour in format of regular database query @@ -1801,35 +2002,44 @@ def _prepare_value_list(self, database_item: Item, timeframe: str, start: int, e :param start: increments for timeframe from now to start :param end: increments for timeframe from now to end :param ignore_value_list: list of comparison operators for val_num, which will be applied during query - :param method: calculation method + :param data_con_func: data concentration function - avg_day: determines average value per day of values within plugin - avg_hour: determines average value per hour of values within plugin - first_day: determines first value per day of values within plugin - first_hour: determines first value per hour of values within plugin - minmax_day: determines min and max value per day of values within plugin - minmax_hour: determines min and max value per hour of values within plugin + - min_day: determines min value per day of values within plugin + - max_hour: determines max value per hour of values within plugin + - min_day: determines min value per day of values within plugin + - max_hour: determines max value per hour of values within plugin + - first_hour_avg_day: 2-step concentration: 1) concentrate values within an hour by using first value 2) concentrate values by average for first value of each hour :return: list of list with [timestamp, value] """ - def _create_raw_value_dict(block: str) -> dict: + def _group_value_by_datetime_block(block: str) -> dict: """ create dict of datetimes (per day or hour) and values based on database query result in format {'datetime1': [values]}, 'datetime1': [values], ..., 'datetimex': [values]} - :param block: defined the increment of datetimes, default is hour, furhter possible is 'day' + :param block: defined the increment of datetime, default is min, further possible is 'day' and 'hour' """ _value_dict = {} for _entry in raw_data: - dt = datetime.datetime.utcfromtimestamp(_entry[0] / 1000) - dt = dt.replace(minute=0, second=0, microsecond=0) + ts = _entry[0] + if len(str(ts)) > 10: + ts = ts / 1000 + dt = self._timestamp_to_datetime(ts) + dt = dt.replace(second=0, microsecond=0, tzinfo=None) + if block == 'hour': + dt = dt.replace(minute=0) if block == 'day': - dt = dt.replace(hour=0) + dt = dt.replace(minute=0, hour=0) if dt not in _value_dict: _value_dict[dt] = [] _value_dict[dt].append(_entry[1]) - return dict(sorted(_value_dict.items())) - def _create_value_list_timestamp_value(option: str) -> list: + def _concentrate_values(option: str) -> list: """ Create list of list with [[timestamp1, value1], [timestamp2, value2], ...] based on value_dict in format of database query result values given in the list will be concentrated as per given option @@ -1838,45 +2048,85 @@ def _create_value_list_timestamp_value(option: str) -> list: 'first' will take first entry of list per datetime to get as close to value at full hour as possible 'avg' will use the calculated average of values in list per datetime 'minmax' will get min and max value of list per datetime + 'min' will get the min value of list per datetime + 'max' will get the min value of list per datetime """ _value_list = [] - # create nested list with timestamp, avg_value per hour/day + # create nested list with timestamp, avg_value or minmax per hour/day for entry in value_dict: - _timestamp = datetime_to_timestamp(entry) + _timestamp = self._datetime_to_timestamp(entry) if option == 'first': _value_list.append([_timestamp, value_dict[entry][0]]) elif option == 'avg': - _value_list.append([_timestamp, round(sum(value_dict[entry]) / len(value_dict[entry]), 1)]) + _value_list.append([_timestamp, round(sum(value_dict[entry]) / len(value_dict[entry]), 2)]) elif option == 'minmax': _value_list.append([_timestamp, min(value_dict[entry]), max(value_dict[entry])]) + elif option == 'max': + _value_list.append([_timestamp, max(value_dict[entry])]) + elif option == 'min': + _value_list.append([_timestamp, min(value_dict[entry])]) return _value_list - # check method - if method in ['avg_day', 'avg_hour', 'minmax_day', 'minmax_hour', 'first_day', 'first_hour']: - _method, _block = method.split('_') - elif method in ['avg', 'minmax', 'first']: - _method = method - _block = 'hour' - else: - self.logger.warning(f"defined {method=} for _prepare_value_list unknown. Need to be 'avg_day', 'avg_hour', 'minmax_day', 'minmax_hour', 'first_day' or 'first_hour'. Aborting...") + if self.debug_log.prepare: + self.logger.debug(f'called with database_item={database_item.path()}, {timeframe=}, {start=}, {end=}, {ignore_value_list=}, {data_con_func=}') + + if data_con_func not in ('min', 'max', 'avg', 'minmax', 'first', 'avg_day', 'avg_hour', 'minmax_day', 'minmax_hour', 'first_day', 'first_hour', 'first_hour_avg_day', 'avg_hour_avg_day', 'min_hour', 'min_day', 'max_hour', 'max_day'): + self.logger.warning(f"defined {data_con_func=} for _prepare_value_list unknown. Need to be 'avg', 'minmax', 'first', 'avg_day', 'avg_hour', 'minmax_day', 'minmax_hour', 'first_day','first_hour', 'first_hour_avg_day' or 'avg_hour_avg_day'. Aborting...") return + # define defaults + _data_con1 = _block1 = _data_con2 = _block2 = result = None + + # check data_con_func + data_con_func_list = data_con_func.split('_') + if len(data_con_func_list) == 1: + _data_con1 = data_con_func_list + _block = 'hour' + elif len(data_con_func_list) == 2: + _data_con1, _block1 = data_con_func_list + elif len(data_con_func_list) == 4: + _data_con1, _block1, _data_con2, _block2 = data_con_func_list + + # define quere params + _query_params = {'func': 'raw', 'database_item': database_item, 'timeframe': timeframe, 'start': start, 'end': end, 'ignore_value_list': ignore_value_list} + # get raw data from database - raw_data = self._query_item(func='raw', database_item=database_item, timeframe=timeframe, start=start, end=end, ignore_value_list=ignore_value_list) - if raw_data in [[[None, None]], [[0, 0]]]: - self.logger.warning("no valid data from database query received during _prepare_value_list. Aborting...") - return + if not cache or str(_query_params) not in self.value_list_raw_data: + raw_data = self._query_item(**_query_params) - # create nested dict with values - value_dict = _create_raw_value_dict(block=_block) - if self.prepare_debug: - self.logger.debug(f"{value_dict=}") + if raw_data == [[None, None]] or raw_data == [[0, 0]]: + self.logger.info(f"no valid data from database query for item={database_item.path()} received during _prepare_value_list. Aborting...") + return - # return value list - result = _create_value_list_timestamp_value(option=_method) - if self.prepare_debug: - self.logger.debug(f"{method=}, {result=}") + if cache: + self.logger.debug(f"raw_data for {_query_params=} put to cache.") + self.value_list_raw_data[str(_query_params)] = raw_data + else: + self.logger.debug(f"raw_data for {_query_params=} read from cache.") + raw_data = self.value_list_raw_data[str(_query_params)] + + if _data_con1 and _block1: + # create nested dict with values + value_dict = _group_value_by_datetime_block(block=_block1) + if self.debug_log.prepare: + self.logger.debug(f"{_block1=}, {value_dict=}") + + # return value list + result = _concentrate_values(option=_data_con1) + if self.debug_log.prepare: + self.logger.debug(f"{_data_con1=}, {result=}") + + if _data_con2 and _block2: + # create nested dict with values + value_dict = _group_value_by_datetime_block(block=_block2) + if self.debug_log.prepare: + self.logger.debug(f"{_block2=}, {value_dict=}") + + # return value list + result = _concentrate_values(option=_data_con2) + if self.debug_log.prepare: + self.logger.debug(f"{_data_con2=}, {result=}") return result @@ -1937,33 +2187,9 @@ def _get_db_parameter(self) -> bool: else: return True - def _initialize_db(self) -> bool: - """ - Initializes database connection - - :return: Status of initialization - """ - - try: - if not self._db.connected(): - # limit connection requests to 20 seconds. - current_time = time.time() - time_delta_last_connect = current_time - self.last_connect_time - if time_delta_last_connect > 20: - self.last_connect_time = time.time() - self._db.connect() - else: - self.logger.error(f"_initialize_db: Database reconnect suppressed: Delta time: {time_delta_last_connect}") - return False - except Exception as e: - self.logger.critical(f"_initialize_db: Database: Initialization failed: {e}") - return False - else: - return True - def _check_db_connection_setting(self) -> None: """ - Check Setting of DB connection for stable use. + Check Setting of mysql connection for stable use. """ try: connect_timeout = int(self._get_db_connect_timeout()[1]) @@ -1998,8 +2224,8 @@ def _get_oldest_log(self, item: Item) -> Union[None, int]: self.item_cache[item] = {} self.item_cache[item]['oldest_log'] = oldest_log - if self.prepare_debug: - self.logger.debug(f"_get_oldest_log for item {item.path()} = {oldest_log}") + if self.debug_log.prepare: + self.logger.debug(f"_get_oldest_log for item={item.path()} = {oldest_log}") return oldest_log @@ -2024,7 +2250,7 @@ def _get_oldest_value(self, item: Item) -> Union[int, float, bool]: oldest_log = self._get_oldest_log(item) if oldest_log is None: validity = True - self.logger.error(f"oldest_log for item {item.path()} could not be read; value is set to -999999999") + self.logger.error(f"oldest_log for item={item.path()} could not be read; value is set to -999999999") oldest_entry = self._read_log_timestamp(item_id, oldest_log) i += 1 if isinstance(oldest_entry, list) and isinstance(oldest_entry[0], tuple) and len(oldest_entry[0]) >= 4: @@ -2035,10 +2261,10 @@ def _get_oldest_value(self, item: Item) -> Union[int, float, bool]: validity = True elif i == 10: validity = True - self.logger.error(f"oldest_value for item {item.path()} could not be read; value is set to -999999999") + self.logger.error(f"oldest_value for item={item.path()} could not be read; value is set to -999999999") - if self.prepare_debug: - self.logger.debug(f"_get_oldest_value for item {item.path()} = {_oldest_value}") + if self.debug_log.prepare: + self.logger.debug(f"_get_oldest_value for item={item.path()} = {_oldest_value}") return _oldest_value @@ -2080,7 +2306,7 @@ def _get_itemid_for_query(self, item: Union[Item, str, int]) -> Union[int, None] item_id = None return item_id - def _query_item(self, func: str, database_item: Item, timeframe: str, start: int = None, end: int = 0, group: str = None, group2: str = None, ignore_value_list=None, use_oldest_entry: bool = False) -> list: + def _query_item(self, func: str, database_item: Item, timeframe: str, start: int = None, end: int = 0, group: str = "", group2: str = "", ignore_value_list=None, use_oldest_entry: bool = False) -> list: """ Do diverse checks of input, and prepare query of log by getting item_id, start / end in timestamp etc. @@ -2097,84 +2323,85 @@ def _query_item(self, func: str, database_item: Item, timeframe: str, start: int :return: query response / list for value pairs [[None, None]] for errors, [[0,0]] for no-data in DB """ - if self.prepare_debug: - self.logger.debug(f"called with {func=}, item={database_item.path()}, {timeframe=}, {start=}, {end=}, {group=}, {group2=}, {ignore_value_list=}") + if self.debug_log.prepare: + self.logger.debug(f" called with {func=}, item={database_item.path()}, {timeframe=}, {start=}, {end=}, {group=}, {group2=}, {ignore_value_list=}, {use_oldest_entry=}") # set default result - default_result = [[None, None]] + error_result = [[None, None]] + nodata_result = [[0, 0]] # check correctness of timeframe if timeframe not in ALLOWED_QUERY_TIMEFRAMES: self.logger.error(f"Requested {timeframe=} for item={database_item.path()} not defined; Need to be 'year' or 'month' or 'week' or 'day' or 'hour''. Query cancelled.") - return default_result + return error_result # define start and end of query as timestamp in microseconds - ts_start, ts_end = get_start_end_as_timestamp(timeframe, start, end) + ts_start, ts_end = self._get_start_end_as_timestamp(timeframe, start, end) oldest_log = self._get_oldest_log(database_item) if oldest_log is None: - return default_result + return error_result # check correctness of ts_start / ts_end if ts_start is None: ts_start = oldest_log if ts_end is None or ts_start > ts_end: - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"{ts_start=}, {ts_end=}") - self.logger.warning(f"Requested {start=} for item={database_item.path()} is not valid since {start=} < {end=} or end not given. Query cancelled.") - return default_result + self.logger.warning(f"Requested {start=} for item={database_item.path()} is not valid since {start=} > {end=} or end not given. Query cancelled.") + return error_result # define item_id item_id = self._get_itemid(database_item) if not item_id: - self.logger.error(f"ItemId for item={database_item.path()} not found. Query cancelled.") - return default_result + self.logger.error(f"DB ItemId for item={database_item.path()} not found. Query cancelled.") + return error_result - if self.prepare_debug: - self.logger.debug(f"Requested {timeframe=} with {start=} and {end=} resulted in start being timestamp={ts_start} / {timestamp_to_timestring(ts_start)} and end being timestamp={ts_end} / {timestamp_to_timestring(ts_end)}") + if self.debug_log.prepare: + self.logger.debug(f" Requested {timeframe=} with {start=} and {end=} resulted in start being timestamp={ts_start}/{self._timestamp_to_timestring(ts_start)} and end being timestamp={ts_end}/{self._timestamp_to_timestring(ts_end)}") # check if values for end time and start time are in database if ts_end < oldest_log: # (Abfrage abbrechen, wenn Endzeitpunkt in UNIX-timestamp der Abfrage kleiner (und damit jünger) ist, als der UNIX-timestamp des ältesten Eintrages) - self.logger.info(f"Requested end time timestamp={ts_end} / {timestamp_to_timestring(ts_end)} of query for Item='{database_item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") - return default_result + self.logger.info(f" Requested end time timestamp={ts_end}/{self._timestamp_to_timestring(ts_end)} of query for item={database_item.path()} is prior to oldest entry with timestamp={oldest_log}/{self._timestamp_to_timestring(oldest_log)}. Query cancelled.") + return error_result if ts_start < oldest_log: if self.use_oldest_entry or use_oldest_entry: - self.logger.info(f"Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{database_item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Oldest available entry will be used.") + self.logger.info(f" Requested start time timestamp={ts_start}/{self._timestamp_to_timestring(ts_start)} of query for item={database_item.path()} is prior to oldest entry with timestamp={oldest_log}/{self._timestamp_to_timestring(oldest_log)}. Oldest available entry will be used.") ts_start = oldest_log else: - self.logger.info(f"Requested start time timestamp={ts_start} / {timestamp_to_timestring(ts_start)} of query for Item='{database_item.path()}' is prior to oldest entry with timestamp={oldest_log} / {timestamp_to_timestring(oldest_log)}. Query cancelled.") - return default_result + self.logger.info(f" Requested start time timestamp={ts_start}/{self._timestamp_to_timestring(ts_start)} of query for item={database_item.path()} is prior to oldest entry with timestamp={oldest_log}/{self._timestamp_to_timestring(oldest_log)}. Query cancelled.") + return error_result # prepare and do query query_params = {'func': func, 'item_id': item_id, 'ts_start': ts_start, 'ts_end': ts_end, 'group': group, 'group2': group2, 'ignore_value_list': ignore_value_list} query_result = self._query_log_timestamp(**query_params) - if self.prepare_debug: - self.logger.debug(f"result of '_query_log_timestamp' {query_result=}") + if self.debug_log.prepare: + self.logger.debug(f" result of '_query_log_timestamp' {query_result=}") # post process query_result if query_result is None: - self.logger.error(f"Error occurred during _query_item. Aborting...") - return default_result + self.logger.error(f"Error occurred during '_query_log_timestamp' of item={database_item.path()}. Aborting...") + return error_result if len(query_result) == 0: - self.logger.info(f"No values for item in requested timeframe in database found.") - return [[0, 0]] + self.logger.info(f" No values for item={database_item.path()} in requested timeframe between {ts_start}/{self._timestamp_to_timestring(ts_start)} and {ts_end}/{self._timestamp_to_timestring(ts_end)} in database found.") + return nodata_result result = [] for element in query_result: timestamp, value = element if timestamp is not None and value is not None: if isinstance(value, float): - value = round(value, 1) + value = round(value, 2) result.append([timestamp, value]) - if self.prepare_debug: - self.logger.debug(f"value for item={database_item.path()} with {query_params=}: {result}") + if self.debug_log.prepare: + self.logger.debug(f" value for item={database_item.path()} with {query_params=}: {result}") if not result: - self.logger.info(f"No values for item in requested timeframe in database found.") - return default_result + self.logger.info(f" No values for item={database_item.path()} in requested timeframe between {ts_start}/{self._timestamp_to_timestring(ts_start)} and {ts_end}/{self._timestamp_to_timestring(ts_end)} in database found.") + return nodata_result return result @@ -2188,6 +2415,7 @@ def _init_cache_dicts(self) -> None: self.item_cache = {} self.current_values = { + HOUR: {}, DAY: {}, WEEK: {}, MONTH: {}, @@ -2195,12 +2423,40 @@ def _init_cache_dicts(self) -> None: } self.previous_values = { + HOUR: {}, DAY: {}, WEEK: {}, MONTH: {}, YEAR: {} } + self.value_list_raw_data = {} + + def _clean_item_cache(self, item: Union[str, Item]) -> bool: + """set cached values for item to None""" + + if isinstance(item, str): + item = self.items.return_item(item) + + if not isinstance(item, Item): + return False + + database_item = self.get_item_config(item).get('database_item') + + if database_item: + for timeframe in self.previous_values: + for cached_item in self.previous_values[timeframe]: + if cached_item == database_item: + self.previous_values[timeframe][cached_item] = None + + for timeframe in self.current_values: + for cached_item in self.current_values[timeframe]: + if cached_item == database_item: + self.current_values[timeframe][cached_item] = {} + + return True + return False + def _clear_queue(self) -> None: """ Clear working queue @@ -2209,11 +2465,115 @@ def _clear_queue(self) -> None: self.logger.info(f"Working queue will be cleared. Calculation run will end.") self.item_queue.queue.clear() + # ToDo: Check if still needed + def _queue_consumer_thread_startup(self): + """Start a thread to work item queue""" + + self.logger = logging.getLogger(__name__) + _name = 'plugins.' + self.get_fullname() + '.work_item_queue' + + try: + self.queue_consumer_thread = threading.Thread(target=self.work_item_queue, name=_name, daemon=False) + self.queue_consumer_thread.start() + self.logger.debug("Thread for 'queue_consumer_thread' has been started") + except threading.ThreadError: + self.logger.error("Unable to launch thread for 'queue_consumer_thread'.") + self.queue_consumer_thread = None + + # ToDo: Check if still needed + def _queue_consumer_thread_shutdown(self): + """Shut down the thread to work item queue""" + + if self.queue_consumer_thread: + self.queue_consumer_thread.join() + if self.queue_consumer_thread.is_alive(): + self.logger.error("Unable to shut down 'queue_consumer_thread' thread") + else: + self.logger.info("Thread 'queue_consumer_thread' has been shut down.") + self.queue_consumer_thread = None + + def _get_start_end_as_timestamp(self, timeframe: str, start: Union[int, str, None], end: Union[int, str, None]) -> tuple: + """ + Provides start and end as timestamp in microseconds from timeframe with start and end + + :param timeframe: timeframe as week, month, year + :param start: beginning timeframe in x timeframes from now + :param end: end of timeframe in x timeframes from now + + :return: start time in timestamp in microseconds, end time in timestamp in microseconds + + """ + + ts_start = ts_end = None + + def get_query_timestamp(_offset) -> int: + if timeframe == 'hour': + dt = self.shtime.now().replace(microsecond=0, second=0, minute=0) - datetime.timedelta(hours=_offset) + return self._datetime_to_timestamp(dt) * 1000 + elif timeframe == 'week': + _date = self.shtime.beginning_of_week(offset=-_offset) + elif timeframe == 'month': + _date = self.shtime.beginning_of_month(offset=-_offset) + elif timeframe == 'year': + _date = self.shtime.beginning_of_year(offset=-_offset) + else: + _date = self.shtime.today(offset=-_offset) + + return self._datetime_to_timestamp(datetime.datetime.combine(_date, datetime.datetime.min.time())) * 1000 + + if isinstance(start, str) and start.isdigit(): + start = int(start) + if isinstance(start, int): + ts_start = get_query_timestamp(start) + + if isinstance(end, str) and end.isdigit(): + end = int(end) + if isinstance(end, int): + if timeframe == 'hour': + ts_end = get_query_timestamp(end) + else: + ts_end = get_query_timestamp(end - 1) + + return ts_start, ts_end + + def _datetime_to_timestamp(self, dt: datetime) -> int: + """Provides timestamp from given datetime""" + + return int(dt.replace(tzinfo=self.shtime.tzinfo()).timestamp()) + + def _timestamp_to_datetime(self, timestamp: float) -> datetime: + """Parse timestamp from db query to datetime""" + + return datetime.datetime.fromtimestamp(timestamp, tz=self.shtime.tzinfo()) + + def _timestamp_to_timestring(self, timestamp: int) -> str: + """Parse timestamp from db query to string representing date and time""" + + return self._timestamp_to_datetime(timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S') + + def _valid_year(self, year: Union[int, str]) -> bool: + """Check if given year is digit and within allowed range""" + + if ((isinstance(year, int) or (isinstance(year, str) and year.isdigit())) and ( + 1980 <= int(year) <= self.shtime.today(offset=0).year)) or (isinstance(year, str) and year == 'current'): + return True + else: + return False + + @staticmethod + def _valid_month(month: Union[int, str]) -> bool: + """Check if given month is digit and within allowed range""" + + if (isinstance(month, int) or (isinstance(month, str) and month.isdigit())) and (1 <= int(month) <= 12): + return True + else: + return False + ################################# # Database Query Preparation ################################# - def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: int, group: str = None, group2: str = None, ignore_value_list=None) -> Union[list, None]: + def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: int, group: str = "", group2: str = "", ignore_value_list=None) -> Union[list, None]: """ Assemble a mysql query str and param dict based on given parameters, get query response and return it @@ -2230,7 +2590,7 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i """ # do debug log - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"Called with {func=}, {item_id=}, {ts_start=}, {ts_end=}, {group=}, {group2=}, {ignore_value_list=}") # define query parts @@ -2254,41 +2614,31 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i } _table_alias = { - 'avg': '', 'avg1': ') AS table1 ', - 'min': '', - 'max': '', 'max1': ') AS table1 ', - 'sum': '', - 'on': '', - 'integrate': '', 'sum_max': ') AS table1 ', 'sum_avg': ') AS table1 ', 'sum_min_neg': ') AS table1 ', 'diff_max': ') AS table1 ', - 'next': '', - 'raw': '', - 'first': '', - 'last': '', } _order = { - 'avg': 'time ASC ', - 'avg1': 'time ASC ', - 'min': 'time ASC ', - 'max': 'time ASC ', - 'max1': 'time ASC ', - 'sum': 'time ASC ', - 'on': 'time ASC ', - 'integrate': 'time ASC ', - 'sum_max': 'time ASC ', - 'sum_avg': 'time ASC ', - 'sum_min_neg': 'time ASC ', - 'diff_max': 'time ASC ', - 'next': 'time DESC LIMIT 1 ', - 'raw': 'time ASC ', - 'first': 'time ASC LIMIT 1 ', - 'last': 'time DESC LIMIT 1 ', + 'avg1': 'ORDER BY time ASC ', + 'max1': 'ORDER BY time ASC ', + 'on': 'ORDER BY time ASC ', + 'sum_max': 'ORDER BY time ASC ', + 'sum_min_neg': 'ORDER BY time ASC ', + 'diff_max': 'ORDER BY time ASC ', + 'next': 'ORDER BY time DESC ', + 'raw': 'ORDER BY time ASC ', + 'first': 'ORDER BY time ASC ', + 'last': 'ORDER BY time DESC ', + } + + _limit = { + 'next': 'LIMIT 1 ', + 'first': 'LIMIT 1 ', + 'last': 'LIMIT 1 ', } _where = "item_id = :item_id AND time < :ts_start " if func == "next" else "item_id = :item_id AND time BETWEEN :ts_start AND :ts_end " @@ -2301,7 +2651,6 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i "week": "GROUP BY YEARWEEK(FROM_UNIXTIME(time/1000), 5) ", "day": "GROUP BY DATE(FROM_UNIXTIME(time/1000)) ", "hour": "GROUP BY FROM_UNIXTIME((time/1000),'%Y%m%d%H') ", - None: "", } _group_by_sqlite = { @@ -2310,7 +2659,6 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i "week": "GROUP BY strftime('%Y%W', date((time/1000),'unixepoch')) ", "day": "GROUP BY date((time/1000),'unixepoch') ", "hour": "GROUP BY strftime('%Y%m%d%H', datetime((time/1000),'unixepoch')) ", - None: "", } # select query parts depending in db driver @@ -2328,10 +2676,10 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i return # check correctness of group and group2 - if group not in _group_by: + if group and group not in _group_by: self.logger.error(f"Requested {group=} for item={item_id=} not defined. Query cancelled.") return - if group2 not in _group_by: + if group2 and group2 not in _group_by: self.logger.error(f"Requested {group2=} for item={item_id=} not defined. Query cancelled.") return @@ -2348,75 +2696,64 @@ def _query_log_timestamp(self, func: str, item_id: int, ts_start: int, ts_end: i params.update({'ts_end': ts_end}) # assemble query - query = f"SELECT {_select[func]}FROM {_db_table}WHERE {_where}{_group_by[group]}ORDER BY {_order[func]}{_table_alias[func]}{_group_by[group2]}".strip() + query = f"SELECT {_select[func]}FROM {_db_table}WHERE {_where}{_group_by.get(group, '')}{_order.get(func, '')}{_limit.get(func, '')}{_table_alias.get(func, '')}{_group_by.get(group2, '')}".strip() if self.db_driver.lower() == 'sqlite3': query = query.replace('IF', 'IIF') # do debug log - if self.prepare_debug: + if self.debug_log.prepare: self.logger.debug(f"{query=}, {params=}") # request database and return result return self._fetchall(query, params) - def _read_log_all(self, item_id: int): + def _read_log_oldest(self, item_id: int) -> int: """ - Read the oldest log record for given item + Read the oldest log record for given database ID - :param item_id: item_id to read the record for - :return: Log record for item_id + :param item_id: Database ID of item to read the record for + :return: timestamp of oldest log entry of given item_id """ - if self.prepare_debug: - self.logger.debug(f"called for {item_id=}") - - query = "SELECT * FROM log WHERE (item_id = :item_id) AND (time = None OR 1 = 1)" params = {'item_id': item_id} + query = "SELECT min(time) FROM log WHERE item_id = :item_id;" result = self._fetchall(query, params) - return result + return None if result is None else result[0][0] - def _read_log_oldest(self, item_id: int, cur=None) -> int: + def _read_log_newest(self, item_id: int) -> int: """ Read the oldest log record for given database ID :param item_id: Database ID of item to read the record for - :type item_id: int - :param cur: A database cursor object if available (optional) - - :return: Log record for the database ID + :return: timestamp of newest log entry of given item_id """ params = {'item_id': item_id} - query = "SELECT min(time) FROM log WHERE item_id = :item_id;" - return self._fetchall(query, params, cur=cur)[0][0] + query = "SELECT max(time) FROM log WHERE item_id = :item_id;" + result = self._fetchall(query, params) + return None if result is None else result[0][0] - def _read_log_timestamp(self, item_id: int, timestamp: int, cur=None) -> Union[list, None]: + def _read_log_timestamp(self, item_id: int, timestamp: int) -> Union[list, None]: """ Read database log record for given database ID :param item_id: Database ID of item to read the record for - :type item_id: int :param timestamp: timestamp for the given value - :type timestamp: int - :param cur: A database cursor object if available (optional) - :return: Log record for the database ID at given timestamp """ params = {'item_id': item_id, 'timestamp': timestamp} query = "SELECT * FROM log WHERE item_id = :item_id AND time = :timestamp;" - return self._fetchall(query, params, cur=cur) + return self._fetchall(query, params) - def _read_item_table(self, item_id: int = None, item_path: str = None): + def _read_item_table(self, item_id: int = None, item_path: str = None) -> Union[list, None]: """ Read item table :param item_id: unique ID for item within database :param item_path: item_path for Item within the database - :return: Data for the selected item - :rtype: tuple """ columns_entries = ('id', 'name', 'time', 'val_str', 'val_num', 'val_bool', 'changed') @@ -2456,9 +2793,34 @@ def _get_db_net_read_timeout(self) -> list: query = "SHOW GLOBAL VARIABLES LIKE 'net_read_timeout'" return self._fetchone(query) - ####################### - # Database Queries - ####################### + ############################### + # Database specific stuff + ############################### + + def _initialize_db(self) -> bool: + """ + Initializes database connection + + :return: Status of initialization + """ + + try: + if not self._db.connected(): + # limit connection requests to 20 seconds. + time_since_last_connect = time.time() - self.last_connect_time + if time_since_last_connect > 20: + self.last_connect_time = time.time() + self.logger.debug(f"Connect to database.") + self._db.connect() + else: + self.logger.warning(f"Database reconnect suppressed since last connection is less then 20sec ago.") + return False + + except Exception as e: + self.logger.critical(f"Initialization of Database Connection failed: {e}") + return False + + return True def _execute(self, query: str, params: dict = None, cur=None) -> list: if params is None: @@ -2476,42 +2838,122 @@ def _fetchall(self, query: str, params: dict = None, cur=None) -> list: if params is None: params = {} - return self._query(self._db.fetchall, query, params, cur) + tuples = self._query(self._db.fetchall, query, params, cur) + return None if tuples is None else list(tuples) def _query(self, fetch, query: str, params: dict = None, cur=None) -> Union[None, list]: + """query using commit to get latest data from db""" + if params is None: params = {} - if self.sql_debug: + if self.debug_log.sql: self.logger.debug(f"Called with {query=}, {params=}, {cur=}") if not self._initialize_db(): return None if cur is None: - if self._db.verify(5) == 0: - self.logger.error("Connection to database not recovered.") + verify_conn = self._db.verify(retry=5) + if verify_conn == 0: + self.logger.error("Connection to database NOT recovered.") return None + + if self.lock_db_for_query and not self._db.lock(300): + self.logger.error("Can't query database due to fail to acquire lock.") + return None + + query_readable = re.sub(r':([a-z_]+)', r'{\1}', query).format(**params) + + # do commit to get latest data during fetch + self._db.commit() + + # fetch data + try: + tuples = fetch(query, params, cur=cur) + except Exception as e: + self.logger.error(f"Error '{e}' for query={query_readable} occurred.") + tuples = None + pass + + if cur is None and self.lock_db_for_query: + self._db.release() + + if self.debug_log.sql: + self.logger.debug(f"Result of query={query_readable}: {tuples}") + + return tuples + + # ToDo: Check if still needed. + def _query_with_close(self, fetch, query: str, params: dict = None, cur=None) -> Union[None, list]: + """query open and close connection for each query to get latest data from db""" + + if params is None: + params = {} + + if self.debug_log.sql: + self.logger.debug(f"Called with {query=}, {params=}, {cur=}") + + # recovery connection to database + if cur is None or not self._db.connected: + verify_conn = self._db.verify(retry=5) + if verify_conn == 0: + self.logger.error("Connection to database NOT recovered.") + return None + else: + if self.debug_log.sql: + self.logger.debug("Connection to database recovered.") + + # lock database if required + if cur is None and self.lock_db_for_query: if not self._db.lock(300): - self.logger.error("Can't query due to fail to acquire lock.") + self.logger.error("Can't query database due to fail to acquire lock.") return None + # fetch data query_readable = re.sub(r':([a-z_]+)', r'{\1}', query).format(**params) - try: tuples = fetch(query, params, cur=cur) except Exception as e: - self.logger.error(f"Error for query '{query_readable}': {e}") + self.logger.error(f"Error '{e}' for query={query_readable} occurred.") tuples = None pass - finally: - if cur is None: - self._db.release() - if self.sql_debug: - self.logger.debug(f"Result of '{query_readable}': {tuples}") + # release database + if cur is None and self.lock_db_for_query: + self._db.release() + + # close connection + self._db.close() + + if self.debug_log.sql: + self.logger.debug(f"Result of query={query_readable}: {tuples}") + return tuples + +@dataclass +class DebugLogOptions: + """Class to simplify use and handling of debug log options.""" + + log_level: InitVar[int] = 10 + parse: bool = True # Enable / Disable debug logging for method 'parse item' + execute: bool = True # Enable / Disable debug logging for method 'execute items' + ondemand: bool = True # Enable / Disable debug logging for method 'handle_ondemand' + onchange: bool = True # Enable / Disable debug logging for method 'handle_onchange' + prepare: bool = True # Enable / Disable debug logging for query preparation + sql: bool = True # Enable / Disable debug logging for sql stuff + + def __post_init__(self, log_level): + if log_level > 10: + self.parse = False + self.execute = False + self.ondemand = False + self.onchange = False + self.prepare = False + self.prepare = False + + ####################### # Helper functions ####################### @@ -2539,42 +2981,11 @@ def params_to_dict(string: str) -> Union[dict, None]: return None elif key in ('start', 'end', 'count') and not isinstance(res_dict[key], int): return None - elif key in 'year': - if not valid_year(res_dict[key]): - return None - elif key in 'month': - if not valid_month(res_dict[key]): - return None return res_dict -def valid_year(year: Union[int, str]) -> bool: - """Check if given year is digit and within allowed range""" - - if ((isinstance(year, int) or (isinstance(year, str) and year.isdigit())) and ( - 1980 <= int(year) <= datetime.date.today().year)) or (isinstance(year, str) and year == 'current'): - return True - else: - return False - - -def valid_month(month: Union[int, str]) -> bool: - """Check if given month is digit and within allowed range""" - - if (isinstance(month, int) or (isinstance(month, str) and month.isdigit())) and (1 <= int(month) <= 12): - return True - else: - return False - - -def timestamp_to_timestring(timestamp: int) -> str: - """Parse timestamp from db query to string representing date and time""" - - return datetime.datetime.utcfromtimestamp(timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S') - - -def harmonize_timeframe_expression(timeframe: str) -> str: - """harmonizes different expression of timeframe""" +def translate_timeframe(timeframe: str) -> str: + """translates different expression of timeframe""" lookup = { 'tag': 'day', @@ -2594,7 +3005,7 @@ def harmonize_timeframe_expression(timeframe: str) -> str: return lookup.get(timeframe) -def convert_timeframe(timeframe_in: str, timeframe_out: str) -> int: +def timeframe_to_timeframe(timeframe_in: str, timeframe_out: str) -> int: """Convert timeframe to timeframe like month in years or years in days""" _h_in_d = 24 @@ -2641,104 +3052,6 @@ def convert_timeframe(timeframe_in: str, timeframe_out: str) -> int: return lookup[timeframe_in][timeframe_out] -def get_start_end_as_timestamp(timeframe: str, start: Union[int, str, None], end: Union[int, str, None]) -> tuple: - """ - Provides start and end as timestamp in microseconds from timeframe with start and end - - :param timeframe: timeframe as week, month, year - :param start: beginning timeframe in x timeframes from now - :param end: end of timeframe in x timeframes from now - - :return: start time in timestamp in microseconds, end time in timestamp in microseconds - - """ - - def get_start() -> datetime: - if timeframe == 'week': - return _week_beginning() - elif timeframe == 'month': - return _month_beginning() - elif timeframe == 'year': - return _year_beginning() - else: - return _day_beginning() - - def get_end() -> datetime: - if timeframe == 'week': - return _week_end() - elif timeframe == 'month': - return _month_end() - elif timeframe == 'year': - return _year_end() - else: - return _day_end() - - def _year_beginning(delta: int = start) -> datetime: - """provides datetime of beginning of year of today minus x years""" - - _dt = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) - return _dt.replace(month=1, day=1) - relativedelta(years=delta) - - def _year_end(delta: int = end) -> datetime: - """provides datetime of end of year of today minus x years""" - - return _year_beginning(delta) + relativedelta(years=1) - - def _month_beginning(delta: int = start) -> datetime: - """provides datetime of beginning of month minus x month""" - - _dt = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) - return _dt.replace(day=1) - relativedelta(months=delta) - - def _month_end(delta: int = end) -> datetime: - """provides datetime of end of month minus x month""" - - return _month_beginning(delta) + relativedelta(months=1) - - def _week_beginning(delta: int = start) -> datetime: - """provides datetime of beginning of week minus x weeks""" - - _dt = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) - return _dt - relativedelta(days=(datetime.date.today().weekday() + (delta * 7))) - - def _week_end(delta: int = end) -> datetime: - """provides datetime of end of week minus x weeks""" - - return _week_beginning(delta) + relativedelta(days=7) - - def _day_beginning(delta: int = start) -> datetime: - """provides datetime of beginning of today minus x days""" - - return datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()) - relativedelta(days=delta) - - def _day_end(delta: int = end) -> datetime: - """provides datetime of end of today minus x days""" - - return _day_beginning(delta) + relativedelta(days=1) - - if isinstance(start, str) and start.isdigit(): - start = int(start) - if isinstance(start, int): - ts_start = datetime_to_timestamp(get_start()) * 1000 - else: - ts_start = None - - if isinstance(end, str) and end.isdigit(): - end = int(end) - if isinstance(end, int): - ts_end = datetime_to_timestamp(get_end()) * 1000 - else: - ts_end = None - - return ts_start, ts_end - - -def datetime_to_timestamp(dt: datetime) -> int: - """Provides timestamp from given datetime""" - - return int(dt.replace(tzinfo=datetime.timezone.utc).timestamp()) - - def to_int(arg) -> Union[int, None]: try: return int(arg) @@ -2776,4 +3089,3 @@ def split_sting_letters_numbers(string) -> list: ALLOWED_QUERY_TIMEFRAMES = ['year', 'month', 'week', 'day', 'hour'] ALLOWED_MINMAX_FUNCS = ['min', 'max', 'avg'] - diff --git a/db_addon/item_attributes.py b/db_addon/item_attributes.py index 7c04066fc..696ffda30 100644 --- a/db_addon/item_attributes.py +++ b/db_addon/item_attributes.py @@ -28,26 +28,39 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -ALL_ONCHANGE_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_monat_min', 'minmax_monat_max', 'minmax_jahr_min', 'minmax_jahr_max', 'tagesmitteltemperatur_heute'] -ALL_DAILY_ATTRIBUTES = ['verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3', 'zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_zaehlerstand_tag_30d', 'serie_tagesmittelwert_0d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_stunde_30_0d', 'serie_tagesmittelwert_tag_stunde_30d', 'kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage'] -ALL_WEEKLY_ATTRIBUTES = ['verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_rolling_12m_woche_minus1', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_verbrauch_woche_30w', 'serie_zaehlerstand_woche_30w'] -ALL_MONTHLY_ATTRIBUTES = ['verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_rolling_12m_monat_minus1', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m'] -ALL_YEARLY_ATTRIBUTES = ['verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_rolling_12m_jahr_minus1', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] -ALL_NEED_PARAMS_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage', 'db_request', 'minmax', 'minmax_last', 'verbrauch', 'zaehlerstand'] -ALL_VERBRAUCH_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3'] -VERBRAUCH_ATTRIBUTES_ONCHANGE = ['verbrauch_heute', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr'] -VERBRAUCH_ATTRIBUTES_TIMEFRAME = ['verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2'] -VERBRAUCH_ATTRIBUTES_ROLLING = ['verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1'] +ONCHANGE_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_tag', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_tag_min', 'minmax_tag_max', 'minmax_tag_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_monat_min', 'minmax_monat_max', 'minmax_jahr_min', 'minmax_jahr_max', 'tagesmitteltemperatur_heute', 'tagesmitteltemperatur_tag'] +ONCHANGE_HOURLY_ATTRIBUTES = [] +ONCHANGE_DAILY_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_tag', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_tag_min', 'minmax_tag_max', 'minmax_tag_avg', 'tagesmitteltemperatur_heute', 'tagesmitteltemperatur_tag'] +ONCHANGE_WEEKLY_ATTRIBUTES = ['verbrauch_woche', 'minmax_woche_min', 'minmax_woche_max'] +ONCHANGE_MONTHLY_ATTRIBUTES = ['verbrauch_monat', 'minmax_monat_min', 'minmax_monat_max'] +ONCHANGE_YEARLY_ATTRIBUTES = ['verbrauch_jahr', 'minmax_jahr_min', 'minmax_jahr_max'] +ONDEMAND_ATTRIBUTES = ['verbrauch_last_24h', 'verbrauch_last_7d', 'verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_heute_minus8', 'verbrauch_tag_minus1', 'verbrauch_tag_minus2', 'verbrauch_tag_minus3', 'verbrauch_tag_minus4', 'verbrauch_tag_minus5', 'verbrauch_tag_minus6', 'verbrauch_tag_minus7', 'verbrauch_tag_minus8', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_jahr_minus3', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_tag_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3', 'zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_tag_minus1', 'zaehlerstand_tag_minus2', 'zaehlerstand_tag_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3', 'minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_tag_minus1_min', 'minmax_tag_minus1_max', 'minmax_tag_minus1_avg', 'minmax_tag_minus2_min', 'minmax_tag_minus2_max', 'minmax_tag_minus2_avg', 'minmax_tag_minus3_min', 'minmax_tag_minus3_max', 'minmax_tag_minus3_avg', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'tagesmitteltemperatur_tag_minus1', 'tagesmitteltemperatur_tag_minus2', 'tagesmitteltemperatur_tag_minus3', 'serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_verbrauch_woche_30w', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_tag_30d', 'serie_zaehlerstand_woche_30w', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m', 'serie_tagesmittelwert_0d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_stunde_30_0d', 'serie_tagesmittelwert_tag_stunde_30d', 'general_oldest_value', 'general_oldest_log', 'kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'wachstumsgradtage', 'wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage', 'tagesmitteltemperatur', 'db_request', 'minmax', 'minmax_last', 'verbrauch', 'zaehlerstand'] +ONDEMAND_HOURLY_ATTRIBUTES = ['verbrauch_last_24h', 'verbrauch_last_7d'] +ONDEMAND_DAILY_ATTRIBUTES = ['verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_heute_minus8', 'verbrauch_tag_minus1', 'verbrauch_tag_minus2', 'verbrauch_tag_minus3', 'verbrauch_tag_minus4', 'verbrauch_tag_minus5', 'verbrauch_tag_minus6', 'verbrauch_tag_minus7', 'verbrauch_tag_minus8', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_tag_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3', 'zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_tag_minus1', 'zaehlerstand_tag_minus2', 'zaehlerstand_tag_minus3', 'minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_tag_minus1_min', 'minmax_tag_minus1_max', 'minmax_tag_minus1_avg', 'minmax_tag_minus2_min', 'minmax_tag_minus2_max', 'minmax_tag_minus2_avg', 'minmax_tag_minus3_min', 'minmax_tag_minus3_max', 'minmax_tag_minus3_avg', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'tagesmitteltemperatur_tag_minus1', 'tagesmitteltemperatur_tag_minus2', 'tagesmitteltemperatur_tag_minus3', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_zaehlerstand_tag_30d', 'serie_tagesmittelwert_0d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_stunde_30_0d', 'serie_tagesmittelwert_tag_stunde_30d', 'kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'wachstumsgradtage', 'wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage', 'tagesmitteltemperatur'] +ONDEMAND_WEEKLY_ATTRIBUTES = ['verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_rolling_12m_woche_minus1', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_verbrauch_woche_30w', 'serie_zaehlerstand_woche_30w'] +ONDEMAND_MONTHLY_ATTRIBUTES = ['verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_rolling_12m_monat_minus1', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m'] +ONDEMAND_YEARLY_ATTRIBUTES = ['verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_jahr_minus3', 'verbrauch_rolling_12m_jahr_minus1', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +ALL_HOURLY_ATTRIBUTES = ['verbrauch_last_24h', 'verbrauch_last_7d'] +ALL_DAILY_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_tag', 'verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_heute_minus8', 'verbrauch_tag_minus1', 'verbrauch_tag_minus2', 'verbrauch_tag_minus3', 'verbrauch_tag_minus4', 'verbrauch_tag_minus5', 'verbrauch_tag_minus6', 'verbrauch_tag_minus7', 'verbrauch_tag_minus8', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_tag_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3', 'zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_tag_minus1', 'zaehlerstand_tag_minus2', 'zaehlerstand_tag_minus3', 'minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_tag_min', 'minmax_tag_max', 'minmax_tag_avg', 'minmax_tag_minus1_min', 'minmax_tag_minus1_max', 'minmax_tag_minus1_avg', 'minmax_tag_minus2_min', 'minmax_tag_minus2_max', 'minmax_tag_minus2_avg', 'minmax_tag_minus3_min', 'minmax_tag_minus3_max', 'minmax_tag_minus3_avg', 'tagesmitteltemperatur_heute', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'tagesmitteltemperatur_tag', 'tagesmitteltemperatur_tag_minus1', 'tagesmitteltemperatur_tag_minus2', 'tagesmitteltemperatur_tag_minus3', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_zaehlerstand_tag_30d', 'serie_tagesmittelwert_0d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_stunde_30_0d', 'serie_tagesmittelwert_tag_stunde_30d', 'kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'wachstumsgradtage', 'wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage', 'tagesmitteltemperatur'] +ALL_WEEKLY_ATTRIBUTES = ['verbrauch_woche', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_rolling_12m_woche_minus1', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'minmax_woche_min', 'minmax_woche_max', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_verbrauch_woche_30w', 'serie_zaehlerstand_woche_30w'] +ALL_MONTHLY_ATTRIBUTES = ['verbrauch_monat', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_rolling_12m_monat_minus1', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'minmax_monat_min', 'minmax_monat_max', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m'] +ALL_YEARLY_ATTRIBUTES = ['verbrauch_jahr', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_jahr_minus3', 'verbrauch_rolling_12m_jahr_minus1', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3', 'minmax_jahr_min', 'minmax_jahr_max', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +ALL_PARAMS_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'wachstumsgradtage', 'wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage', 'tagesmitteltemperatur', 'db_request', 'minmax', 'minmax_last', 'verbrauch', 'zaehlerstand'] +ALL_VERBRAUCH_ATTRIBUTES = ['verbrauch_heute', 'verbrauch_tag', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr', 'verbrauch_last_24h', 'verbrauch_last_7d', 'verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_heute_minus8', 'verbrauch_tag_minus1', 'verbrauch_tag_minus2', 'verbrauch_tag_minus3', 'verbrauch_tag_minus4', 'verbrauch_tag_minus5', 'verbrauch_tag_minus6', 'verbrauch_tag_minus7', 'verbrauch_tag_minus8', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_jahr_minus3', 'verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_tag_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1', 'verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3'] +VERBRAUCH_ATTRIBUTES_ONCHANGE = ['verbrauch_heute', 'verbrauch_tag', 'verbrauch_woche', 'verbrauch_monat', 'verbrauch_jahr'] +VERBRAUCH_ATTRIBUTES_TIMEFRAME = ['verbrauch_heute_minus1', 'verbrauch_heute_minus2', 'verbrauch_heute_minus3', 'verbrauch_heute_minus4', 'verbrauch_heute_minus5', 'verbrauch_heute_minus6', 'verbrauch_heute_minus7', 'verbrauch_heute_minus8', 'verbrauch_tag_minus1', 'verbrauch_tag_minus2', 'verbrauch_tag_minus3', 'verbrauch_tag_minus4', 'verbrauch_tag_minus5', 'verbrauch_tag_minus6', 'verbrauch_tag_minus7', 'verbrauch_tag_minus8', 'verbrauch_woche_minus1', 'verbrauch_woche_minus2', 'verbrauch_woche_minus3', 'verbrauch_woche_minus4', 'verbrauch_monat_minus1', 'verbrauch_monat_minus2', 'verbrauch_monat_minus3', 'verbrauch_monat_minus4', 'verbrauch_monat_minus12', 'verbrauch_jahr_minus1', 'verbrauch_jahr_minus2', 'verbrauch_jahr_minus3'] +VERBRAUCH_ATTRIBUTES_LAST = ['verbrauch_last_24h', 'verbrauch_last_7d'] +VERBRAUCH_ATTRIBUTES_ROLLING = ['verbrauch_rolling_12m_heute_minus1', 'verbrauch_rolling_12m_tag_minus1', 'verbrauch_rolling_12m_woche_minus1', 'verbrauch_rolling_12m_monat_minus1', 'verbrauch_rolling_12m_jahr_minus1'] VERBRAUCH_ATTRIBUTES_JAHRESZEITRAUM = ['verbrauch_jahreszeitraum_minus1', 'verbrauch_jahreszeitraum_minus2', 'verbrauch_jahreszeitraum_minus3'] -ALL_ZAEHLERSTAND_ATTRIBUTES = ['zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3'] -ZAEHLERSTAND_ATTRIBUTES_TIMEFRAME = ['zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3'] -ALL_HISTORIE_ATTRIBUTES = ['minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_min', 'minmax_monat_max', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_min', 'minmax_jahr_max', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] -HISTORIE_ATTRIBUTES_ONCHANGE = ['minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_monat_min', 'minmax_monat_max', 'minmax_jahr_min', 'minmax_jahr_max'] +ALL_ZAEHLERSTAND_ATTRIBUTES = ['zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_tag_minus1', 'zaehlerstand_tag_minus2', 'zaehlerstand_tag_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3'] +ZAEHLERSTAND_ATTRIBUTES_TIMEFRAME = ['zaehlerstand_heute_minus1', 'zaehlerstand_heute_minus2', 'zaehlerstand_heute_minus3', 'zaehlerstand_tag_minus1', 'zaehlerstand_tag_minus2', 'zaehlerstand_tag_minus3', 'zaehlerstand_woche_minus1', 'zaehlerstand_woche_minus2', 'zaehlerstand_woche_minus3', 'zaehlerstand_monat_minus1', 'zaehlerstand_monat_minus2', 'zaehlerstand_monat_minus3', 'zaehlerstand_jahr_minus1', 'zaehlerstand_jahr_minus2', 'zaehlerstand_jahr_minus3'] +ALL_HISTORIE_ATTRIBUTES = ['minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg', 'minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_tag_min', 'minmax_tag_max', 'minmax_tag_avg', 'minmax_tag_minus1_min', 'minmax_tag_minus1_max', 'minmax_tag_minus1_avg', 'minmax_tag_minus2_min', 'minmax_tag_minus2_max', 'minmax_tag_minus2_avg', 'minmax_tag_minus3_min', 'minmax_tag_minus3_max', 'minmax_tag_minus3_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_min', 'minmax_monat_max', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_min', 'minmax_jahr_max', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +HISTORIE_ATTRIBUTES_ONCHANGE = ['minmax_heute_min', 'minmax_heute_max', 'minmax_heute_avg', 'minmax_tag_min', 'minmax_tag_max', 'minmax_tag_avg', 'minmax_woche_min', 'minmax_woche_max', 'minmax_monat_min', 'minmax_monat_max', 'minmax_jahr_min', 'minmax_jahr_max'] HISTORIE_ATTRIBUTES_LAST = ['minmax_last_24h_min', 'minmax_last_24h_max', 'minmax_last_24h_avg', 'minmax_last_7d_min', 'minmax_last_7d_max', 'minmax_last_7d_avg'] -HISTORIE_ATTRIBUTES_TIMEFRAME = ['minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] -ALL_TAGESMITTEL_ATTRIBUTES = ['tagesmitteltemperatur_heute', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3'] -TAGESMITTEL_ATTRIBUTES_ONCHANGE = ['tagesmitteltemperatur_heute'] -TAGESMITTEL_ATTRIBUTES_TIMEFRAME = ['tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3'] +HISTORIE_ATTRIBUTES_TIMEFRAME = ['minmax_heute_minus1_min', 'minmax_heute_minus1_max', 'minmax_heute_minus1_avg', 'minmax_heute_minus2_min', 'minmax_heute_minus2_max', 'minmax_heute_minus2_avg', 'minmax_heute_minus3_min', 'minmax_heute_minus3_max', 'minmax_heute_minus3_avg', 'minmax_tag_minus1_min', 'minmax_tag_minus1_max', 'minmax_tag_minus1_avg', 'minmax_tag_minus2_min', 'minmax_tag_minus2_max', 'minmax_tag_minus2_avg', 'minmax_tag_minus3_min', 'minmax_tag_minus3_max', 'minmax_tag_minus3_avg', 'minmax_woche_minus1_min', 'minmax_woche_minus1_max', 'minmax_woche_minus1_avg', 'minmax_woche_minus2_min', 'minmax_woche_minus2_max', 'minmax_woche_minus2_avg', 'minmax_monat_minus1_min', 'minmax_monat_minus1_max', 'minmax_monat_minus1_avg', 'minmax_monat_minus2_min', 'minmax_monat_minus2_max', 'minmax_monat_minus2_avg', 'minmax_jahr_minus1_min', 'minmax_jahr_minus1_max', 'minmax_jahr_minus1_avg'] +ALL_TAGESMITTEL_ATTRIBUTES = ['tagesmitteltemperatur_heute', 'tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'tagesmitteltemperatur_tag', 'tagesmitteltemperatur_tag_minus1', 'tagesmitteltemperatur_tag_minus2', 'tagesmitteltemperatur_tag_minus3'] +TAGESMITTEL_ATTRIBUTES_ONCHANGE = ['tagesmitteltemperatur_heute', 'tagesmitteltemperatur_tag', 'minmax_heute_avg', 'minmax_tag_avg'] +TAGESMITTEL_ATTRIBUTES_TIMEFRAME = ['tagesmitteltemperatur_heute_minus1', 'tagesmitteltemperatur_heute_minus2', 'tagesmitteltemperatur_heute_minus3', 'tagesmitteltemperatur_tag_minus1', 'tagesmitteltemperatur_tag_minus2', 'tagesmitteltemperatur_tag_minus3'] ALL_SERIE_ATTRIBUTES = ['serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d', 'serie_verbrauch_tag_30d', 'serie_verbrauch_woche_30w', 'serie_verbrauch_monat_18m', 'serie_zaehlerstand_tag_30d', 'serie_zaehlerstand_woche_30w', 'serie_zaehlerstand_monat_18m', 'serie_waermesumme_monat_24m', 'serie_kaeltesumme_monat_24m', 'serie_tagesmittelwert_0d', 'serie_tagesmittelwert_stunde_0d', 'serie_tagesmittelwert_stunde_30_0d', 'serie_tagesmittelwert_tag_stunde_30d'] SERIE_ATTRIBUTES_MINMAX = ['serie_minmax_monat_min_15m', 'serie_minmax_monat_max_15m', 'serie_minmax_monat_avg_15m', 'serie_minmax_woche_min_30w', 'serie_minmax_woche_max_30w', 'serie_minmax_woche_avg_30w', 'serie_minmax_tag_min_30d', 'serie_minmax_tag_max_30d', 'serie_minmax_tag_avg_30d'] SERIE_ATTRIBUTES_ZAEHLERSTAND = ['serie_zaehlerstand_tag_30d', 'serie_zaehlerstand_woche_30w', 'serie_zaehlerstand_monat_18m'] @@ -58,4 +71,5 @@ SERIE_ATTRIBUTES_MITTEL_H1 = ['serie_tagesmittelwert_stunde_30_0d'] SERIE_ATTRIBUTES_MITTEL_D_H = ['serie_tagesmittelwert_tag_stunde_30d'] ALL_GEN_ATTRIBUTES = ['general_oldest_value', 'general_oldest_log'] -ALL_COMPLEX_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'tagesmitteltemperatur', 'wachstumsgradtage', 'db_request', 'minmax', 'minmax_last', 'verbrauch', 'zaehlerstand'] +ALL_SUMME_ATTRIBUTES = ['kaeltesumme', 'waermesumme', 'gruenlandtempsumme', 'wachstumsgradtage', 'wuestentage', 'heisse_tage', 'tropennaechte', 'sommertage', 'heiztage', 'vegetationstage', 'frosttage', 'eistage'] +ALL_COMPLEX_ATTRIBUTES = ['tagesmitteltemperatur', 'db_request', 'minmax', 'minmax_last', 'verbrauch', 'zaehlerstand'] diff --git a/db_addon/item_attributes_master.py b/db_addon/item_attributes_master.py old mode 100755 new mode 100644 index 00b54a8cf..012e0ca2f --- a/db_addon/item_attributes_master.py +++ b/db_addon/item_attributes_master.py @@ -25,133 +25,178 @@ FILENAME_PLUGIN = 'plugin.yaml' +DOC_FILE_NAME = 'user_doc.rst' + +PLUGIN_VERSION = '1.2.6' + ITEM_ATTRIBUTES = { 'db_addon_fct': { - 'verbrauch_heute': {'cat': 'verbrauch', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages)'}, - 'verbrauch_woche': {'cat': 'verbrauch', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch in der aktuellen Woche'}, - 'verbrauch_monat': {'cat': 'verbrauch', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch im aktuellen Monat'}, - 'verbrauch_jahr': {'cat': 'verbrauch', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Verbrauch im aktuellen Jahr'}, - 'verbrauch_heute_minus1': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages davor)'}, - 'verbrauch_heute_minus2': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch vorgestern (heute -2 Tage)'}, - 'verbrauch_heute_minus3': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -3 Tage'}, - 'verbrauch_heute_minus4': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -4 Tage'}, - 'verbrauch_heute_minus5': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -5 Tage'}, - 'verbrauch_heute_minus6': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -6 Tage'}, - 'verbrauch_heute_minus7': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -7 Tage'}, - 'verbrauch_woche_minus1': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch Vorwoche (aktuelle Woche -1)'}, - 'verbrauch_woche_minus2': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -2 Wochen'}, - 'verbrauch_woche_minus3': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -3 Wochen'}, - 'verbrauch_woche_minus4': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -4 Wochen'}, - 'verbrauch_monat_minus1': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch Vormonat (aktueller Monat -1)'}, - 'verbrauch_monat_minus2': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -2 Monate'}, - 'verbrauch_monat_minus3': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -3 Monate'}, - 'verbrauch_monat_minus4': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -4 Monate'}, - 'verbrauch_monat_minus12': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -12 Monate'}, - 'verbrauch_jahr_minus1': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch Vorjahr (aktuelles Jahr -1 Jahr)'}, - 'verbrauch_jahr_minus2': {'cat': 'verbrauch', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch aktuelles Jahr -2 Jahre'}, - 'verbrauch_rolling_12m_heute_minus1': {'cat': 'verbrauch', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages'}, - 'verbrauch_rolling_12m_woche_minus1': {'cat': 'verbrauch', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche'}, - 'verbrauch_rolling_12m_monat_minus1': {'cat': 'verbrauch', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats'}, - 'verbrauch_rolling_12m_jahr_minus1': {'cat': 'verbrauch', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres'}, - 'verbrauch_jahreszeitraum_minus1': {'cat': 'verbrauch', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres'}, - 'verbrauch_jahreszeitraum_minus2': {'cat': 'verbrauch', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren'}, - 'verbrauch_jahreszeitraum_minus3': {'cat': 'verbrauch', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren'}, - 'zaehlerstand_heute_minus1': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag)'}, - 'zaehlerstand_heute_minus2': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag)'}, - 'zaehlerstand_heute_minus3': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag)'}, - 'zaehlerstand_woche_minus1': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorvorletzten Woche (aktuelle Woche -1 Woche)'}, - 'zaehlerstand_woche_minus2': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Wochen)'}, - 'zaehlerstand_woche_minus3': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der aktuellen Woche -3 Wochen'}, - 'zaehlerstand_monat_minus1': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat)'}, - 'zaehlerstand_monat_minus2': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate)'}, - 'zaehlerstand_monat_minus3': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Monats -3 Monate'}, - 'zaehlerstand_jahr_minus1': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr)'}, - 'zaehlerstand_jahr_minus2': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre)'}, - 'zaehlerstand_jahr_minus3': {'cat': 'zaehler', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre'}, - 'minmax_last_24h_min': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 24h'}, - 'minmax_last_24h_max': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 24h'}, - 'minmax_last_24h_avg': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 24h'}, - 'minmax_last_7d_min': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 7 Tage'}, - 'minmax_last_7d_max': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 7 Tage'}, - 'minmax_last_7d_avg': {'cat': 'wertehistorie', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 7 Tage'}, - 'minmax_heute_min': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Tagesbeginn'}, - 'minmax_heute_max': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Tagesbeginn'}, - 'minmax_heute_avg': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Durschnittswert seit Tagesbeginn'}, - 'minmax_heute_minus1_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert gestern (heute -1 Tag)'}, - 'minmax_heute_minus1_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert gestern (heute -1 Tag)'}, - 'minmax_heute_minus1_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert gestern (heute -1 Tag)'}, - 'minmax_heute_minus2_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert vorgestern (heute -2 Tage)'}, - 'minmax_heute_minus2_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert vorgestern (heute -2 Tage)'}, - 'minmax_heute_minus2_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert vorgestern (heute -2 Tage)'}, - 'minmax_heute_minus3_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert heute vor 3 Tagen'}, - 'minmax_heute_minus3_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert heute vor 3 Tagen'}, - 'minmax_heute_minus3_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert heute vor 3 Tagen'}, - 'minmax_woche_min': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Wochenbeginn'}, - 'minmax_woche_max': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Wochenbeginn'}, - 'minmax_woche_minus1_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert Vorwoche (aktuelle Woche -1)'}, - 'minmax_woche_minus1_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert Vorwoche (aktuelle Woche -1)'}, - 'minmax_woche_minus1_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert Vorwoche (aktuelle Woche -1)'}, - 'minmax_woche_minus2_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert aktuelle Woche -2 Wochen'}, - 'minmax_woche_minus2_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert aktuelle Woche -2 Wochen'}, - 'minmax_woche_minus2_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert aktuelle Woche -2 Wochen'}, - 'minmax_monat_min': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Monatsbeginn'}, - 'minmax_monat_max': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Monatsbeginn'}, - 'minmax_monat_minus1_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert Vormonat (aktueller Monat -1)'}, - 'minmax_monat_minus1_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert Vormonat (aktueller Monat -1)'}, - 'minmax_monat_minus1_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert Vormonat (aktueller Monat -1)'}, - 'minmax_monat_minus2_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert aktueller Monat -2 Monate'}, - 'minmax_monat_minus2_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert aktueller Monat -2 Monate'}, - 'minmax_monat_minus2_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert aktueller Monat -2 Monate'}, - 'minmax_jahr_min': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Minimalwert seit Jahresbeginn'}, - 'minmax_jahr_max': {'cat': 'wertehistorie', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Maximalwert seit Jahresbeginn'}, - 'minmax_jahr_minus1_min': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Minimalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, - 'minmax_jahr_minus1_max': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Maximalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, - 'minmax_jahr_minus1_avg': {'cat': 'wertehistorie', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr)'}, - 'tagesmitteltemperatur_heute': {'cat': 'tagesmittel', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'onchange', 'params': False, 'description': 'Tagesmitteltemperatur heute'}, - 'tagesmitteltemperatur_heute_minus1': {'cat': 'tagesmittel', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des letzten Tages (heute -1 Tag)'}, - 'tagesmitteltemperatur_heute_minus2': {'cat': 'tagesmittel', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag)'}, - 'tagesmitteltemperatur_heute_minus3': {'cat': 'tagesmittel', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag)'}, - 'serie_minmax_monat_min_15m': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Minimalwert der letzten 15 Monate (gleitend)'}, - 'serie_minmax_monat_max_15m': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Maximalwert der letzten 15 Monate (gleitend)'}, - 'serie_minmax_monat_avg_15m': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Mittelwert der letzten 15 Monate (gleitend)'}, - 'serie_minmax_woche_min_30w': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Minimalwert der letzten 30 Wochen (gleitend)'}, - 'serie_minmax_woche_max_30w': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Maximalwert der letzten 30 Wochen (gleitend)'}, - 'serie_minmax_woche_avg_30w': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Mittelwert der letzten 30 Wochen (gleitend)'}, - 'serie_minmax_tag_min_30d': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Minimalwert der letzten 30 Tage (gleitend)'}, - 'serie_minmax_tag_max_30d': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Maximalwert der letzten 30 Tage (gleitend)'}, - 'serie_minmax_tag_avg_30d': {'cat': 'serie', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Mittelwert der letzten 30 Tage (gleitend)'}, - 'serie_verbrauch_tag_30d': {'cat': 'serie', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Verbrauch pro Tag der letzten 30 Tage'}, - 'serie_verbrauch_woche_30w': {'cat': 'serie', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch pro Woche der letzten 30 Wochen'}, - 'serie_verbrauch_monat_18m': {'cat': 'serie', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch pro Monat der letzten 18 Monate'}, - 'serie_zaehlerstand_tag_30d': {'cat': 'serie', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Zählerstand am Tagesende der letzten 30 Tage'}, - 'serie_zaehlerstand_woche_30w': {'cat': 'serie', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand am Wochenende der letzten 30 Wochen'}, - 'serie_zaehlerstand_monat_18m': {'cat': 'serie', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand am Monatsende der letzten 18 Monate'}, - 'serie_waermesumme_monat_24m': {'cat': 'serie', 'sub_cat': 'summe', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Wärmesumme der letzten 24 Monate'}, - 'serie_kaeltesumme_monat_24m': {'cat': 'serie', 'sub_cat': 'summe', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Kältesumme der letzten 24 Monate'}, - 'serie_tagesmittelwert_0d': {'cat': 'serie', 'sub_cat': 'mittel_d', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Tagesmittelwert für den aktuellen Tag'}, - 'serie_tagesmittelwert_stunde_0d': {'cat': 'serie', 'sub_cat': 'mittel_h', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert für den aktuellen Tag'}, - 'serie_tagesmittelwert_stunde_30_0d': {'cat': 'serie', 'sub_cat': 'mittel_h1', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert für den aktuellen Tag'}, - 'serie_tagesmittelwert_tag_stunde_30d': {'cat': 'serie', 'sub_cat': 'mittel_d_h', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde'}, - 'general_oldest_value': {'cat': 'gen', 'sub_cat': None, 'item_type': 'num', 'calc': 'no', 'params': False, 'description': 'Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut'}, - 'general_oldest_log': {'cat': 'gen', 'sub_cat': None, 'item_type': 'list', 'calc': 'no', 'params': False, 'description': 'Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut'}, - 'kaeltesumme': {'cat': 'complex', 'sub_cat': 'summe', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)'}, - 'waermesumme': {'cat': 'complex', 'sub_cat': 'summe', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)'}, - 'gruenlandtempsumme': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory)'}, - 'tagesmitteltemperatur': {'cat': 'complex', 'sub_cat': None, 'item_type': 'list', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer)'}, - 'wachstumsgradtage': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur)'}, - 'db_request': {'cat': 'complex', 'sub_cat': None, 'item_type': 'list', 'calc': 'group', 'params': True, 'description': 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)'}, - 'minmax': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen min/max/avg Wert für einen bestimmen Zeitraum: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory)'}, - 'minmax_last': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen min/max/avg Wert für ein bestimmtes Zeitfenster von jetzt zurück: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory, end=mandatory)'}, - 'verbrauch': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen Verbrauchswert für einen bestimmen Zeitraum: db_addon_params: (timeframe=mandatory, start=mandatory end=mandatory)'}, - 'zaehlerstand': {'cat': 'complex', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen Zählerstand für einen bestimmen Zeitpunkt: db_addon_params: (timeframe=mandatory, start=mandatory)'}, + 'verbrauch_heute': {'cat': 'verbrauch', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages)'}, + 'verbrauch_tag': {'cat': 'verbrauch', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages)'}, + 'verbrauch_woche': {'cat': 'verbrauch', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch in der aktuellen Woche'}, + 'verbrauch_monat': {'cat': 'verbrauch', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch im aktuellen Monat'}, + 'verbrauch_jahr': {'cat': 'verbrauch', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch im aktuellen Jahr'}, + 'verbrauch_last_24h': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'hourly', 'params': False, 'description': 'Verbrauch innerhalb letzten 24h'}, + 'verbrauch_last_7d': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'hourly', 'params': False, 'description': 'Verbrauch innerhalb letzten 7 Tage'}, + 'verbrauch_heute_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages davor)'}, + 'verbrauch_heute_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch vorgestern (heute -2 Tage)'}, + 'verbrauch_heute_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -3 Tage'}, + 'verbrauch_heute_minus4': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -4 Tage'}, + 'verbrauch_heute_minus5': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -5 Tage'}, + 'verbrauch_heute_minus6': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -6 Tage'}, + 'verbrauch_heute_minus7': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -7 Tage'}, + 'verbrauch_heute_minus8': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -8 Tage'}, + 'verbrauch_tag_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages davor)'}, + 'verbrauch_tag_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch vorgestern (heute -2 Tage)'}, + 'verbrauch_tag_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -3 Tage'}, + 'verbrauch_tag_minus4': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -4 Tage'}, + 'verbrauch_tag_minus5': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -5 Tage'}, + 'verbrauch_tag_minus6': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -6 Tage'}, + 'verbrauch_tag_minus7': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -7 Tage'}, + 'verbrauch_tag_minus8': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch heute -8 Tage'}, + 'verbrauch_woche_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch Vorwoche (aktuelle Woche -1)'}, + 'verbrauch_woche_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -2 Wochen'}, + 'verbrauch_woche_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -3 Wochen'}, + 'verbrauch_woche_minus4': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch aktuelle Woche -4 Wochen'}, + 'verbrauch_monat_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch Vormonat (aktueller Monat -1)'}, + 'verbrauch_monat_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -2 Monate'}, + 'verbrauch_monat_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -3 Monate'}, + 'verbrauch_monat_minus4': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -4 Monate'}, + 'verbrauch_monat_minus12': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch aktueller Monat -12 Monate'}, + 'verbrauch_jahr_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'verbrauch_jahr_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch aktuelles Jahr -2 Jahre'}, + 'verbrauch_jahr_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch aktuelles Jahr -3 Jahre'}, + 'verbrauch_rolling_12m_heute_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages'}, + 'verbrauch_rolling_12m_tag_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages'}, + 'verbrauch_rolling_12m_woche_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche'}, + 'verbrauch_rolling_12m_monat_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats'}, + 'verbrauch_rolling_12m_jahr_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'rolling', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Jahres'}, + 'verbrauch_jahreszeitraum_minus1': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag des Vorjahres'}, + 'verbrauch_jahreszeitraum_minus2': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 2 Jahren'}, + 'verbrauch_jahreszeitraum_minus3': {'cat': 'verbrauch', 'on': 'demand', 'sub_cat': 'jahrzeit', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Verbrauch seit dem 1.1. bis zum heutigen Tag vor 3 Jahren'}, + 'zaehlerstand_heute_minus1': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag)'}, + 'zaehlerstand_heute_minus2': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag)'}, + 'zaehlerstand_heute_minus3': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag)'}, + 'zaehlerstand_tag_minus1': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag)'}, + 'zaehlerstand_tag_minus2': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag)'}, + 'zaehlerstand_tag_minus3': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag)'}, + 'zaehlerstand_woche_minus1': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorvorletzten Woche (aktuelle Woche -1 Woche)'}, + 'zaehlerstand_woche_minus2': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Wochen)'}, + 'zaehlerstand_woche_minus3': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand / Wert am Ende der aktuellen Woche -3 Wochen'}, + 'zaehlerstand_monat_minus1': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Monates (aktueller Monat -1 Monat)'}, + 'zaehlerstand_monat_minus2': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Monates (aktueller Monat -2 Monate)'}, + 'zaehlerstand_monat_minus3': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Monats -3 Monate'}, + 'zaehlerstand_jahr_minus1': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des letzten Jahres (aktuelles Jahr -1 Jahr)'}, + 'zaehlerstand_jahr_minus2': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des vorletzten Jahres (aktuelles Jahr -2 Jahre)'}, + 'zaehlerstand_jahr_minus3': {'cat': 'zaehler', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Zählerstand / Wert am Ende des aktuellen Jahres -3 Jahre'}, + 'minmax_last_24h_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 24h'}, + 'minmax_last_24h_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 24h'}, + 'minmax_last_24h_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 24h'}, + 'minmax_last_7d_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'minimaler Wert der letzten 7 Tage'}, + 'minmax_last_7d_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'maximaler Wert der letzten 7 Tage'}, + 'minmax_last_7d_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'last', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'durchschnittlicher Wert der letzten 7 Tage'}, + 'minmax_heute_min': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert seit Tagesbeginn'}, + 'minmax_heute_max': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert seit Tagesbeginn'}, + 'minmax_heute_avg': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durschnittswert seit Tagesbeginn'}, + 'minmax_heute_minus1_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert gestern (heute -1 Tag)'}, + 'minmax_heute_minus1_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert gestern (heute -1 Tag)'}, + 'minmax_heute_minus1_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert gestern (heute -1 Tag)'}, + 'minmax_heute_minus2_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus2_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus2_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert vorgestern (heute -2 Tage)'}, + 'minmax_heute_minus3_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert heute vor 3 Tagen'}, + 'minmax_heute_minus3_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert heute vor 3 Tagen'}, + 'minmax_heute_minus3_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert heute vor 3 Tagen'}, + 'minmax_tag_min': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert seit Tagesbeginn'}, + 'minmax_tag_max': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert seit Tagesbeginn'}, + 'minmax_tag_avg': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durschnittswert seit Tagesbeginn'}, + 'minmax_tag_minus1_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert gestern (heute -1 Tag)'}, + 'minmax_tag_minus1_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert gestern (heute -1 Tag)'}, + 'minmax_tag_minus1_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert gestern (heute -1 Tag)'}, + 'minmax_tag_minus2_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert vorgestern (heute -2 Tage)'}, + 'minmax_tag_minus2_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert vorgestern (heute -2 Tage)'}, + 'minmax_tag_minus2_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert vorgestern (heute -2 Tage)'}, + 'minmax_tag_minus3_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Minimalwert heute vor 3 Tagen'}, + 'minmax_tag_minus3_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Maximalwert heute vor 3 Tagen'}, + 'minmax_tag_minus3_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Durchschnittswert heute vor 3 Tagen'}, + 'minmax_woche_min': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert seit Wochenbeginn'}, + 'minmax_woche_max': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert seit Wochenbeginn'}, + 'minmax_woche_minus1_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus1_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus1_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert Vorwoche (aktuelle Woche -1)'}, + 'minmax_woche_minus2_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Minimalwert aktuelle Woche -2 Wochen'}, + 'minmax_woche_minus2_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Maximalwert aktuelle Woche -2 Wochen'}, + 'minmax_woche_minus2_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'weekly', 'params': False, 'description': 'Durchschnittswert aktuelle Woche -2 Wochen'}, + 'minmax_monat_min': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert seit Monatsbeginn'}, + 'minmax_monat_max': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert seit Monatsbeginn'}, + 'minmax_monat_minus1_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus1_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus1_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert Vormonat (aktueller Monat -1)'}, + 'minmax_monat_minus2_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Minimalwert aktueller Monat -2 Monate'}, + 'minmax_monat_minus2_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Maximalwert aktueller Monat -2 Monate'}, + 'minmax_monat_minus2_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'monthly', 'params': False, 'description': 'Durchschnittswert aktueller Monat -2 Monate'}, + 'minmax_jahr_min': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Minimalwert seit Jahresbeginn'}, + 'minmax_jahr_max': {'cat': 'wertehistorie', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Maximalwert seit Jahresbeginn'}, + 'minmax_jahr_minus1_min': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Minimalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'minmax_jahr_minus1_max': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Maximalwert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'minmax_jahr_minus1_avg': {'cat': 'wertehistorie', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'yearly', 'params': False, 'description': 'Durchschnittswert Vorjahr (aktuelles Jahr -1 Jahr)'}, + 'tagesmitteltemperatur_heute': {'cat': 'tagesmittel', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur heute'}, + 'tagesmitteltemperatur_heute_minus1': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des letzten Tages (heute -1 Tag)'}, + 'tagesmitteltemperatur_heute_minus2': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag)'}, + 'tagesmitteltemperatur_heute_minus3': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag)'}, + 'tagesmitteltemperatur_tag': {'cat': 'tagesmittel', 'on': 'change', 'sub_cat': 'onchange', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur heute'}, + 'tagesmitteltemperatur_tag_minus1': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des letzten Tages (heute -1 Tag)'}, + 'tagesmitteltemperatur_tag_minus2': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag)'}, + 'tagesmitteltemperatur_tag_minus3': {'cat': 'tagesmittel', 'on': 'demand', 'sub_cat': 'timeframe', 'item_type': 'num', 'calc': 'daily', 'params': False, 'description': 'Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag)'}, + 'serie_minmax_monat_min_15m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Minimalwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_monat_max_15m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Maximalwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_monat_avg_15m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatlicher Mittelwert der letzten 15 Monate (gleitend)'}, + 'serie_minmax_woche_min_30w': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Minimalwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_woche_max_30w': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Maximalwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_woche_avg_30w': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'wöchentlicher Mittelwert der letzten 30 Wochen (gleitend)'}, + 'serie_minmax_tag_min_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Minimalwert der letzten 30 Tage (gleitend)'}, + 'serie_minmax_tag_max_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Maximalwert der letzten 30 Tage (gleitend)'}, + 'serie_minmax_tag_avg_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'minmax', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'täglicher Mittelwert der letzten 30 Tage (gleitend)'}, + 'serie_verbrauch_tag_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Verbrauch pro Tag der letzten 30 Tage'}, + 'serie_verbrauch_woche_30w': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Verbrauch pro Woche der letzten 30 Wochen'}, + 'serie_verbrauch_monat_18m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'verbrauch', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Verbrauch pro Monat der letzten 18 Monate'}, + 'serie_zaehlerstand_tag_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Zählerstand am Tagesende der letzten 30 Tage'}, + 'serie_zaehlerstand_woche_30w': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'weekly', 'params': False, 'description': 'Zählerstand am Wochenende der letzten 30 Wochen'}, + 'serie_zaehlerstand_monat_18m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'zaehler', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'Zählerstand am Monatsende der letzten 18 Monate'}, + 'serie_waermesumme_monat_24m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'summe', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Wärmesumme der letzten 24 Monate'}, + 'serie_kaeltesumme_monat_24m': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'summe', 'item_type': 'list', 'calc': 'monthly', 'params': False, 'description': 'monatliche Kältesumme der letzten 24 Monate'}, + 'serie_tagesmittelwert_0d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'mittel_d', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Tagesmittelwert für den aktuellen Tag'}, + 'serie_tagesmittelwert_stunde_0d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'mittel_h', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert für den aktuellen Tag'}, + 'serie_tagesmittelwert_stunde_30_0d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'mittel_h1', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert für den aktuellen Tag'}, + 'serie_tagesmittelwert_tag_stunde_30d': {'cat': 'serie', 'on': 'demand', 'sub_cat': 'mittel_d_h', 'item_type': 'list', 'calc': 'daily', 'params': False, 'description': 'Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde'}, + 'general_oldest_value': {'cat': 'gen', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'None', 'params': False, 'description': 'Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut'}, + 'general_oldest_log': {'cat': 'gen', 'on': 'demand', 'sub_cat': None, 'item_type': 'list', 'calc': 'None', 'params': False, 'description': 'Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut'}, + 'kaeltesumme': {'cat': 'summe', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=optional, month=optional)'}, + 'waermesumme': {'cat': 'summe', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=optional, month=optional)'}, + 'gruenlandtempsumme': {'cat': 'summe', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=optional)'}, + 'wachstumsgradtage': {'cat': 'summe', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur)'}, + 'wuestentage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Wüstentage des Jahres, db_addon_params: (year=optional)'}, + 'heisse_tage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der heissen Tage des Jahres, db_addon_params: (year=optional)'}, + 'tropennaechte': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Tropennächte des Jahres, db_addon_params: (year=optional)'}, + 'sommertage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Sommertage des Jahres, db_addon_params: (year=optional)'}, + 'heiztage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Heiztage des Jahres, db_addon_params: (year=optional)'}, + 'vegetationstage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Vegatationstage des Jahres, db_addon_params: (year=optional)'}, + 'frosttage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Frosttage des Jahres, db_addon_params: (year=optional)'}, + 'eistage': {'cat': 'summe', 'on': 'demand', 'sub_cat': 'kenntage', 'item_type': 'num', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Anzahl der Eistage des Jahres, db_addon_params: (year=optional)'}, + 'tagesmitteltemperatur': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'list', 'calc': 'daily', 'params': True, 'description': 'Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer)'}, + 'db_request': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'list', 'calc': 'group', 'params': True, 'description': 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)'}, + 'minmax': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen min/max/avg Wert für einen bestimmen Zeitraum: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory)'}, + 'minmax_last': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen min/max/avg Wert für ein bestimmtes Zeitfenster von jetzt zurück: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory, end=mandatory)'}, + 'verbrauch': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen Verbrauchswert für einen bestimmen Zeitraum: db_addon_params: (timeframe=mandatory, start=mandatory end=mandatory)'}, + 'zaehlerstand': {'cat': 'complex', 'on': 'demand', 'sub_cat': None, 'item_type': 'num', 'calc': 'timeframe', 'params': True, 'description': 'Berechnet einen Zählerstand für einen bestimmen Zeitpunkt: db_addon_params: (timeframe=mandatory, start=mandatory)'}, }, 'db_addon_info': { - 'db_version': {'cat': 'info', 'item_type': 'str', 'calc': 'no', 'params': False, 'description': 'Version der verbundenen Datenbank'}, - }, - 'db_addon_admin': { - 'suspend': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Unterbricht die Aktivitäten des Plugin'}, - 'recalc_all': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Startet einen Neuberechnungslauf aller on-demand Items'}, - 'clean_cache_values': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Löscht Plugin-Cache und damit alle im Plugin zwischengespeicherten Werte'}, + 'db_version': {'cat': 'info', 'item_type': 'str', 'calc': 'no', 'params': False, 'description': 'Version der verbundenen Datenbank'}, + }, + 'db_addon_admin': { + 'suspend': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Unterbricht die Aktivitäten des Plugin'}, + 'recalc_all': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Startet einen Neuberechnungslauf aller on-demand Items'}, + 'clean_cache_values': {'cat': 'admin', 'item_type': 'bool', 'calc': 'no', 'params': False, 'description': 'Löscht Plugin-Cache und damit alle im Plugin zwischengespeicherten Werte'}, }, } @@ -188,6 +233,7 @@ """ + def get_attrs(sub_dict: dict = {}) -> list: attributes = [] for entry in ITEM_ATTRIBUTES: @@ -196,17 +242,35 @@ def get_attrs(sub_dict: dict = {}) -> list: attributes.append(db_addon_fct) return attributes + def export_item_attributes_py(): + + print() + print(f"A) Start generation of {FILENAME_ATTRIBUTES}") + ATTRS = dict() - ATTRS['ALL_ONCHANGE_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'onchange'}) + ATTRS['ONCHANGE_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change'}) + ATTRS['ONCHANGE_HOURLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change', 'calc': 'hourly'}) + ATTRS['ONCHANGE_DAILY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change', 'calc': 'daily'}) + ATTRS['ONCHANGE_WEEKLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change', 'calc': 'weekly'}) + ATTRS['ONCHANGE_MONTHLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change', 'calc': 'monthly'}) + ATTRS['ONCHANGE_YEARLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'change', 'calc': 'yearly'}) + ATTRS['ONDEMAND_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand'}) + ATTRS['ONDEMAND_HOURLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand', 'calc': 'hourly'}) + ATTRS['ONDEMAND_DAILY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand', 'calc': 'daily'}) + ATTRS['ONDEMAND_WEEKLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand', 'calc': 'weekly'}) + ATTRS['ONDEMAND_MONTHLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand', 'calc': 'monthly'}) + ATTRS['ONDEMAND_YEARLY_ATTRIBUTES'] = get_attrs(sub_dict={'on': 'demand', 'calc': 'yearly'}) + ATTRS['ALL_HOURLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'hourly'}) ATTRS['ALL_DAILY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'daily'}) ATTRS['ALL_WEEKLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'weekly'}) ATTRS['ALL_MONTHLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'monthly'}) ATTRS['ALL_YEARLY_ATTRIBUTES'] = get_attrs(sub_dict={'calc': 'yearly'}) - ATTRS['ALL_NEED_PARAMS_ATTRIBUTES'] = get_attrs(sub_dict={'params': True}) + ATTRS['ALL_PARAMS_ATTRIBUTES'] = get_attrs(sub_dict={'params': True}) ATTRS['ALL_VERBRAUCH_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'verbrauch'}) ATTRS['VERBRAUCH_ATTRIBUTES_ONCHANGE'] = get_attrs(sub_dict={'cat': 'verbrauch', 'sub_cat': 'onchange'}) ATTRS['VERBRAUCH_ATTRIBUTES_TIMEFRAME'] = get_attrs(sub_dict={'cat': 'verbrauch', 'sub_cat': 'timeframe'}) + ATTRS['VERBRAUCH_ATTRIBUTES_LAST'] = get_attrs(sub_dict={'cat': 'verbrauch', 'sub_cat': 'last'}) ATTRS['VERBRAUCH_ATTRIBUTES_ROLLING'] = get_attrs(sub_dict={'cat': 'verbrauch', 'sub_cat': 'rolling'}) ATTRS['VERBRAUCH_ATTRIBUTES_JAHRESZEITRAUM'] = get_attrs(sub_dict={'cat': 'verbrauch', 'sub_cat': 'jahrzeit'}) ATTRS['ALL_ZAEHLERSTAND_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'zaehler'}) @@ -228,7 +292,13 @@ def export_item_attributes_py(): ATTRS['SERIE_ATTRIBUTES_MITTEL_H1'] = get_attrs(sub_dict={'cat': 'serie', 'sub_cat': 'mittel_h1'}) ATTRS['SERIE_ATTRIBUTES_MITTEL_D_H'] = get_attrs(sub_dict={'cat': 'serie', 'sub_cat': 'mittel_d_h'}) ATTRS['ALL_GEN_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'gen'}) + ATTRS['ALL_SUMME_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'summe'}) ATTRS['ALL_COMPLEX_ATTRIBUTES'] = get_attrs(sub_dict={'cat': 'complex'}) + ATTRS['TAGESMITTEL_ATTRIBUTES_ONCHANGE'] = get_attrs(sub_dict={'cat': 'tagesmittel', 'sub_cat': 'onchange'}) + + for entry in ATTRS['HISTORIE_ATTRIBUTES_ONCHANGE']: + if entry.endswith('avg'): + ATTRS['TAGESMITTEL_ATTRIBUTES_ONCHANGE'].append(entry) # create file and write header f = open(FILENAME_ATTRIBUTES, "w") @@ -240,9 +310,10 @@ def export_item_attributes_py(): with open(FILENAME_ATTRIBUTES, "a") as f: print(f'{attr} = {alist!r}', file=f) - print('item_attributes.py successfully created!') + print(f" {FILENAME_ATTRIBUTES} successfully generated.") -def create_plugin_yaml_item_attribute_valids(): + +def create_plugin_yaml_item_attribute_valids(attribute): """Create valid_list of db_addon_fct based on master dict""" valid_list_str = """ # NOTE: valid_list is automatically created by using item_attributes_master.py""" @@ -267,33 +338,133 @@ def create_plugin_yaml_item_attribute_valids(): return valid_list_str, valid_list_desc_str, valid_list_item_type, valid_list_calculation + def update_plugin_yaml_item_attributes(): """Update 'valid_list', 'valid_list_description', 'valid_list_item_type' and 'valid_list_calculation' of item attributes in plugin.yaml""" + print() + print(f"B) Start updating valid for attributes in {FILENAME_PLUGIN}") + + for attribute in ITEM_ATTRIBUTES: + + print(f" Attribute {attribute} in progress") + + yaml = ruamel.yaml.YAML() + yaml.indent(mapping=4, sequence=4, offset=4) + yaml.width = 200 + yaml.allow_unicode = True + yaml.preserve_quotes = False + + valid_list_str, valid_list_desc_str, valid_list_item_type_str, valid_list_calc_str = create_plugin_yaml_item_attribute_valids(attribute) + + with open(FILENAME_PLUGIN, 'r', encoding="utf-8") as f: + data = yaml.load(f) + + if data.get('item_attributes', {}).get(attribute): + data['item_attributes'][attribute]['valid_list'] = yaml.load(valid_list_str) + data['item_attributes'][attribute]['valid_list_description'] = yaml.load(valid_list_desc_str) + data['item_attributes'][attribute]['valid_list_item_type'] = yaml.load(valid_list_item_type_str) + data['item_attributes'][attribute]['valid_list_calculation'] = yaml.load(valid_list_calc_str) + + with open(FILENAME_PLUGIN, 'w', encoding="utf-8") as f: + yaml.dump(data, f) + print(f" Successfully updated Attribute '{attribute}' in plugin.yaml!") + else: + print(f" Attribute '{attribute}' not defined in plugin.yaml") + + +def check_plugin_yaml_structs(): + # check structs for wrong attributes + print() + print(f'C) Checking used attributes in structs defined in {FILENAME_PLUGIN} ') + + # open plugin.yaml and update yaml = ruamel.yaml.YAML() yaml.indent(mapping=4, sequence=4, offset=4) yaml.width = 200 yaml.allow_unicode = True yaml.preserve_quotes = False - - valid_list_str, valid_list_desc_str, valid_list_item_type_str, valid_list_calc_str = create_plugin_yaml_item_attribute_valids() - with open(FILENAME_PLUGIN, 'r', encoding="utf-8") as f: data = yaml.load(f) - if data.get('item_attributes', {}).get(attribute): - data['item_attributes'][attribute]['valid_list'] = yaml.load(valid_list_str) - data['item_attributes'][attribute]['valid_list_description'] = yaml.load(valid_list_desc_str) - data['item_attributes'][attribute]['valid_list_item_type'] = yaml.load(valid_list_item_type_str) - data['item_attributes'][attribute]['valid_list_calculation'] = yaml.load(valid_list_calc_str) + structs = data.get('item_structs') + + def get_all_keys(d): + for key, value in d.items(): + yield key, value + if isinstance(value, dict): + yield from get_all_keys(value) + + attr_valid = True + + if structs: + for attr, attr_val in get_all_keys(structs): + if attr in ITEM_ATTRIBUTES: + if attr_val not in ITEM_ATTRIBUTES[attr].keys(): + print(f" - {attr_val} not a valid value for {ITEM_ATTRIBUTES[attr]}") + attr_valid = False + + if attr_valid: + print(f" All used attributes are valid.") + + print(f' Check complete.') + + +def update_user_doc(): + # Update user_doc.rst + print() + print(f'D) Start updating DB-Addon-Attributes and descriptions in {DOC_FILE_NAME}!"') + attribute_list = [ + "Dieses Kapitel wurde automatisch durch Ausführen des Skripts in der Datei 'item_attributes_master.py' erstellt.\n", "\n", + "Nachfolgend eine Auflistung der möglichen Attribute für das Plugin im Format: Attribute: Beschreibung | Berechnungszyklus | Item-Type\n", + "\n"] + + for attribute in ITEM_ATTRIBUTES: + attribute_list.append("\n") + attribute_list.append(f"{attribute}\n") + attribute_list.append('-' * len(attribute)) + attribute_list.append("\n") + attribute_list.append("\n") + + for db_addon_fct in ITEM_ATTRIBUTES[attribute]: + attribute_list.append(f"- {db_addon_fct}: {ITEM_ATTRIBUTES[attribute][db_addon_fct]['description']} " + f"| Berechnung: {ITEM_ATTRIBUTES[attribute][db_addon_fct]['calc']} " + f"| Item-Type: {ITEM_ATTRIBUTES[attribute][db_addon_fct]['item_type']}\n") + attribute_list.append("\n") + + with open(DOC_FILE_NAME, 'r', encoding='utf-8') as file: + lines = file.readlines() + + start = end = None + for i, line in enumerate(lines): + if 'db_addon Item-Attribute' in line: + start = i + 3 + if 'Hinweise' in line: + end = i - 1 + + part1 = lines[0:start] + part3 = lines[end:len(lines)] + new_lines = part1 + attribute_list + part3 + + with open(DOC_FILE_NAME, 'w', encoding='utf-8') as file: + for line in new_lines: + file.write(line) + + print(f" Successfully updated Foshk-Attributes in {DOC_FILE_NAME}!") - with open(FILENAME_PLUGIN, 'w', encoding="utf-8") as f: - yaml.dump(data, f) - print(f"Successfully updated Attribute '{attribute}' in plugin.yaml!") - else: - print(f"Attribute '{attribute}' not defined in plugin.yaml") if __name__ == '__main__': + + print(f'Start automated update and check of {FILENAME_PLUGIN} with generation of {FILENAME_ATTRIBUTES} and update of {DOC_FILE_NAME}.') + print('-------------------------------------------------------------') + export_item_attributes_py() - for attribute in ITEM_ATTRIBUTES: - update_plugin_yaml_item_attributes() + + update_plugin_yaml_item_attributes() + + check_plugin_yaml_structs() + + update_user_doc() + + print() + print(f'Automated update and check of {FILENAME_PLUGIN} and generation of {FILENAME_ATTRIBUTES} complete.') diff --git a/db_addon/locale.yaml b/db_addon/locale.yaml old mode 100755 new mode 100644 index 2c14a15da..b465b6014 --- a/db_addon/locale.yaml +++ b/db_addon/locale.yaml @@ -1,6 +1,7 @@ # translations for the web interface plugin_translations: # Translations for the plugin specially for the web interface + 'hourly': {'de': 'stündlich', 'en': 'hourly'} 'daily': {'de': 'täglich', 'en': 'daily'} 'weekly': {'de': 'wöchentlich', 'en': '='} 'monthly': {'de': 'monatlich', 'en': '='} diff --git a/db_addon/plugin.yaml b/db_addon/plugin.yaml old mode 100755 new mode 100644 index 98115af82..37ac45de6 --- a/db_addon/plugin.yaml +++ b/db_addon/plugin.yaml @@ -11,7 +11,7 @@ plugin: # keywords: iot xyz # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1848494-support-thread-databaseaddon-plugin - version: 1.2.2 # Plugin version (must match the version specified in __init__.py) + version: 1.2.7 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.9.3.5 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: 3.8 # minimum Python version to use for this plugin @@ -36,7 +36,7 @@ parameters: en: Delay in seconds, after which the startup calculations will be run ignore_0: - type: list + type: list(str) default: [] description: de: 'Bei Items, bei denen ein String aus der Liste im Pfadnamen vorkommt, werden 0-Werte (val_num = 0) bei Datenbankauswertungen ignoriert. Beispieleintrag: temp | hum' @@ -62,6 +62,13 @@ parameters: de: 'True: Verwendung des ältesten Eintrags des Items in der Datenbank, falls der Start des Abfragezeitraums zeitlich vor diesem Eintrag liegt False: Abbruch der Datenbankabfrage' en: 'True: Use of oldest entry of item in database, if start of query is prior to oldest entry False: Cancel query' + lock_db_for_query: + type: bool + default: false + description: + de: Sperren der Datenbank während der Abfrage + en: Lock the database during queries + item_attributes: db_addon_fct: type: str @@ -71,9 +78,12 @@ item_attributes: valid_list: # NOTE: valid_list is automatically created by using item_attributes_master.py - verbrauch_heute + - verbrauch_tag - verbrauch_woche - verbrauch_monat - verbrauch_jahr + - verbrauch_last_24h + - verbrauch_last_7d - verbrauch_heute_minus1 - verbrauch_heute_minus2 - verbrauch_heute_minus3 @@ -81,6 +91,15 @@ item_attributes: - verbrauch_heute_minus5 - verbrauch_heute_minus6 - verbrauch_heute_minus7 + - verbrauch_heute_minus8 + - verbrauch_tag_minus1 + - verbrauch_tag_minus2 + - verbrauch_tag_minus3 + - verbrauch_tag_minus4 + - verbrauch_tag_minus5 + - verbrauch_tag_minus6 + - verbrauch_tag_minus7 + - verbrauch_tag_minus8 - verbrauch_woche_minus1 - verbrauch_woche_minus2 - verbrauch_woche_minus3 @@ -92,7 +111,9 @@ item_attributes: - verbrauch_monat_minus12 - verbrauch_jahr_minus1 - verbrauch_jahr_minus2 + - verbrauch_jahr_minus3 - verbrauch_rolling_12m_heute_minus1 + - verbrauch_rolling_12m_tag_minus1 - verbrauch_rolling_12m_woche_minus1 - verbrauch_rolling_12m_monat_minus1 - verbrauch_rolling_12m_jahr_minus1 @@ -102,6 +123,9 @@ item_attributes: - zaehlerstand_heute_minus1 - zaehlerstand_heute_minus2 - zaehlerstand_heute_minus3 + - zaehlerstand_tag_minus1 + - zaehlerstand_tag_minus2 + - zaehlerstand_tag_minus3 - zaehlerstand_woche_minus1 - zaehlerstand_woche_minus2 - zaehlerstand_woche_minus3 @@ -129,6 +153,18 @@ item_attributes: - minmax_heute_minus3_min - minmax_heute_minus3_max - minmax_heute_minus3_avg + - minmax_tag_min + - minmax_tag_max + - minmax_tag_avg + - minmax_tag_minus1_min + - minmax_tag_minus1_max + - minmax_tag_minus1_avg + - minmax_tag_minus2_min + - minmax_tag_minus2_max + - minmax_tag_minus2_avg + - minmax_tag_minus3_min + - minmax_tag_minus3_max + - minmax_tag_minus3_avg - minmax_woche_min - minmax_woche_max - minmax_woche_minus1_min @@ -154,6 +190,10 @@ item_attributes: - tagesmitteltemperatur_heute_minus1 - tagesmitteltemperatur_heute_minus2 - tagesmitteltemperatur_heute_minus3 + - tagesmitteltemperatur_tag + - tagesmitteltemperatur_tag_minus1 + - tagesmitteltemperatur_tag_minus2 + - tagesmitteltemperatur_tag_minus3 - serie_minmax_monat_min_15m - serie_minmax_monat_max_15m - serie_minmax_monat_avg_15m @@ -180,8 +220,16 @@ item_attributes: - kaeltesumme - waermesumme - gruenlandtempsumme - - tagesmitteltemperatur - wachstumsgradtage + - wuestentage + - heisse_tage + - tropennaechte + - sommertage + - heiztage + - vegetationstage + - frosttage + - eistage + - tagesmitteltemperatur - db_request - minmax - minmax_last @@ -189,10 +237,13 @@ item_attributes: - zaehlerstand valid_list_description: # NOTE: valid_list_description is automatically created by using item_attributes_master.py + - Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages) - Verbrauch am heutigen Tag (Differenz zwischen aktuellem Wert und den Wert am Ende des vorherigen Tages) - Verbrauch in der aktuellen Woche - Verbrauch im aktuellen Monat - Verbrauch im aktuellen Jahr + - Verbrauch innerhalb letzten 24h + - Verbrauch innerhalb letzten 7 Tage - Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages davor) - Verbrauch vorgestern (heute -2 Tage) - Verbrauch heute -3 Tage @@ -200,6 +251,15 @@ item_attributes: - Verbrauch heute -5 Tage - Verbrauch heute -6 Tage - Verbrauch heute -7 Tage + - Verbrauch heute -8 Tage + - Verbrauch gestern (heute -1 Tag) (Differenz zwischen Wert am Ende des gestrigen Tages und dem Wert am Ende des Tages davor) + - Verbrauch vorgestern (heute -2 Tage) + - Verbrauch heute -3 Tage + - Verbrauch heute -4 Tage + - Verbrauch heute -5 Tage + - Verbrauch heute -6 Tage + - Verbrauch heute -7 Tage + - Verbrauch heute -8 Tage - Verbrauch Vorwoche (aktuelle Woche -1) - Verbrauch aktuelle Woche -2 Wochen - Verbrauch aktuelle Woche -3 Wochen @@ -211,6 +271,8 @@ item_attributes: - Verbrauch aktueller Monat -12 Monate - Verbrauch Vorjahr (aktuelles Jahr -1 Jahr) - Verbrauch aktuelles Jahr -2 Jahre + - Verbrauch aktuelles Jahr -3 Jahre + - Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages - Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Tages - Verbrauch der letzten 12 Monate ausgehend im Ende der letzten Woche - Verbrauch der letzten 12 Monate ausgehend im Ende des letzten Monats @@ -221,6 +283,9 @@ item_attributes: - Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag) - Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag) - Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag) + - Zählerstand / Wert am Ende des letzten Tages (heute -1 Tag) + - Zählerstand / Wert am Ende des vorletzten Tages (heute -2 Tag) + - Zählerstand / Wert am Ende des vorvorletzten Tages (heute -3 Tag) - Zählerstand / Wert am Ende der vorvorletzten Woche (aktuelle Woche -1 Woche) - Zählerstand / Wert am Ende der vorletzten Woche (aktuelle Woche -2 Wochen) - Zählerstand / Wert am Ende der aktuellen Woche -3 Wochen @@ -248,6 +313,18 @@ item_attributes: - Minimalwert heute vor 3 Tagen - Maximalwert heute vor 3 Tagen - Durchschnittswert heute vor 3 Tagen + - Minimalwert seit Tagesbeginn + - Maximalwert seit Tagesbeginn + - Durschnittswert seit Tagesbeginn + - Minimalwert gestern (heute -1 Tag) + - Maximalwert gestern (heute -1 Tag) + - Durchschnittswert gestern (heute -1 Tag) + - Minimalwert vorgestern (heute -2 Tage) + - Maximalwert vorgestern (heute -2 Tage) + - Durchschnittswert vorgestern (heute -2 Tage) + - Minimalwert heute vor 3 Tagen + - Maximalwert heute vor 3 Tagen + - Durchschnittswert heute vor 3 Tagen - Minimalwert seit Wochenbeginn - Maximalwert seit Wochenbeginn - Minimalwert Vorwoche (aktuelle Woche -1) @@ -273,6 +350,10 @@ item_attributes: - Tagesmitteltemperatur des letzten Tages (heute -1 Tag) - Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag) - Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag) + - Tagesmitteltemperatur heute + - Tagesmitteltemperatur des letzten Tages (heute -1 Tag) + - Tagesmitteltemperatur des vorletzten Tages (heute -2 Tag) + - Tagesmitteltemperatur des vorvorletzten Tages (heute -3 Tag) - monatlicher Minimalwert der letzten 15 Monate (gleitend) - monatlicher Maximalwert der letzten 15 Monate (gleitend) - monatlicher Mittelwert der letzten 15 Monate (gleitend) @@ -296,11 +377,19 @@ item_attributes: - Stundenmittelwert pro Tag der letzten 30 Tage (bspw. zur Berechnung der Tagesmitteltemperatur basierend auf den Mittelwert der Temperatur pro Stunde - Ausgabe des ältesten Wertes des entsprechenden "Parent-Items" mit database Attribut - Ausgabe des Timestamp des ältesten Eintrages des entsprechenden "Parent-Items" mit database Attribut - - 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)' - - 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=mandatory, month=optional)' - - 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=mandatory)' - - Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer) + - 'Berechnet die Kältesumme für einen Zeitraum, db_addon_params: (year=optional, month=optional)' + - 'Berechnet die Wärmesumme für einen Zeitraum, db_addon_params: (year=optional, month=optional)' + - 'Berechnet die Grünlandtemperatursumme für einen Zeitraum, db_addon_params: (year=optional)' - Berechnet die Wachstumsgradtage auf Basis der stündlichen Durchschnittswerte eines Tages für das laufende Jahr mit an Angabe des Temperaturschwellenwertes (threshold=Schwellentemperatur) + - 'Berechnet die Anzahl der Wüstentage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der heissen Tage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Tropennächte des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Sommertage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Heiztage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Vegatationstage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Frosttage des Jahres, db_addon_params: (year=optional)' + - 'Berechnet die Anzahl der Eistage des Jahres, db_addon_params: (year=optional)' + - Berechnet die Tagesmitteltemperatur auf Basis der stündlichen Durchschnittswerte eines Tages für die angegebene Anzahl von Tagen (timeframe=day, count=integer) - 'Abfrage der DB: db_addon_params: (func=mandatory, item=mandatory, timespan=mandatory, start=optional, end=optional, count=optional, group=optional, group2=optional)' - 'Berechnet einen min/max/avg Wert für einen bestimmen Zeitraum: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory)' - 'Berechnet einen min/max/avg Wert für ein bestimmtes Zeitfenster von jetzt zurück: db_addon_params: (func=mandatory, timeframe=mandatory, start=mandatory, end=mandatory)' @@ -392,6 +481,39 @@ item_attributes: - num - num - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num + - num - list - list - list @@ -418,19 +540,39 @@ item_attributes: - num - num - num - - list + - num + - num + - num + - num + - num + - num + - num + - num - num - list + - list - num - num - num - num valid_list_calculation: # NOTE: valid_list_calculation is automatically created by using item_attributes_master.py - - onchange - - onchange - - onchange - - onchange + - daily + - daily + - weekly + - monthly + - yearly + - hourly + - hourly + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily - daily - daily - daily @@ -449,6 +591,8 @@ item_attributes: - monthly - yearly - yearly + - yearly + - daily - daily - weekly - monthly @@ -459,6 +603,9 @@ item_attributes: - daily - daily - daily + - daily + - daily + - daily - weekly - weekly - weekly @@ -474,9 +621,6 @@ item_attributes: - daily - daily - daily - - onchange - - onchange - - onchange - daily - daily - daily @@ -486,28 +630,47 @@ item_attributes: - daily - daily - daily - - onchange - - onchange + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - weekly + - weekly - weekly - weekly - weekly - weekly - weekly - weekly - - onchange - - onchange - monthly - monthly - monthly - monthly - monthly - monthly - - onchange - - onchange + - monthly + - monthly + - yearly - yearly - yearly - yearly - - onchange + - yearly + - daily + - daily + - daily + - daily + - daily - daily - daily - daily @@ -532,8 +695,16 @@ item_attributes: - daily - daily - daily - - no - - no + - None + - None + - daily + - daily + - daily + - daily + - daily + - daily + - daily + - daily - daily - daily - daily @@ -591,31 +762,26 @@ item_attributes: description: de: Parameter für eine Auswertefunktion des DB-Addon Plugins im Format 'kwargs' enclosed in quotes like 'keyword=argument, keyword=argument' en: Parameters of a DB-Addon Plugin evaluation function. Need to have format of 'kwargs' enclosed in quotes like 'keyword=argument, keyword=argument' - db_addon_params_dict: type: dict description: de: Parameter für eine Auswertefunktion des DB-Addon Plugins im Format eines Dictionary en: Parameters of a DB-Addon Plugin evaluation function. Need to have format of a dictionary - db_addon_startup: type: bool description: de: Ausführen der Berechnung bei Plugin Start (mit zeitlichem Abstand, wie in den Plugin Parametern definiert) en: Run function in startup of plugin (with delay, set in plugin parameters) - db_addon_ignore_value: type: num description: de: Wert der bei Abfrage bzw. Auswertung der Datenbank für diese Item ignoriert werden soll en: Value which will be ignored at database query - db_addon_ignore_value_list: type: list(str) description: de: "Liste von Vergleichsoperatoren, die bei Abfrage bzw. Auswertung der Datenbank für dieses Item berücksichtigt werden sollen. Bsp: ['> 0', '< 35']" en: List of comparison operators which will be used at database query - db_addon_database_item: type: str description: @@ -625,13 +791,21 @@ item_attributes: item_structs: verbrauch_1: name: Struct für Verbrauchsauswertung bei Zählern mit stetig ansteigendem Zählerstand (Teil 1) - verbrauch_heute: - name: Verbrauch heute - db_addon_fct: verbrauch_heute + verbrauch_tag: + name: Verbrauch am heutigen Tag + db_addon_fct: verbrauch_tag db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes + + verbrauch_last_24h: + name: Verbrauch innerhalb der letzten 24h + db_addon_fct: verbrauch_last_24h + db_addon_startup: yes + type: num + visu_acl: ro + cache: yes verbrauch_woche: name: Verbrauch seit Wochenbeginn @@ -639,7 +813,7 @@ item_structs: db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_monat: name: Verbrauch seit Monatsbeginn @@ -647,7 +821,7 @@ item_structs: db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_jahr: name: Verbrauch seit Jahresbeginn @@ -655,71 +829,71 @@ item_structs: db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_rolling_12m: name: Verbrauch innerhalb der letzten 12 Monate ausgehend von gestern - db_addon_fct: verbrauch_rolling_12m_heute_minus1 + db_addon_fct: verbrauch_rolling_12m_tag_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_gestern: + verbrauch_tag_minus1: name: Verbrauch gestern - db_addon_fct: verbrauch_heute_minus1 + db_addon_fct: verbrauch_tag_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_gestern_minus1: + verbrauch_tag_minus2: name: Verbrauch vorgestern - db_addon_fct: verbrauch_heute_minus2 + db_addon_fct: verbrauch_tag_minus2 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_gestern_minus2: + verbrauch_tag_minus3: name: Verbrauch vor 3 Tagen - db_addon_fct: verbrauch_heute_minus3 + db_addon_fct: verbrauch_tag_minus3 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_vorwoche: + verbrauch_woche_minus1: name: Verbrauch in der Vorwoche db_addon_fct: verbrauch_woche_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_vorwoche_minus1: + verbrauch_woche_minus2: name: Verbrauch vor 2 Wochen db_addon_fct: verbrauch_woche_minus2 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_vormonat: + verbrauch_monat_minus1: name: Verbrauch im Vormonat db_addon_fct: verbrauch_monat_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - verbrauch_vormonat_minus12: + verbrauch_monat_minus12: name: Verbrauch vor 12 Monaten db_addon_fct: verbrauch_monat_minus12 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_vorjahreszeitraum: name: Verbrauch im Jahreszeitraum 1.1. bis jetzt vor einem Jahr @@ -727,7 +901,7 @@ item_structs: db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_verbrauch_rolling_12m_woche_minus1: name: Verbrauch innerhalb der letzten 12 Monate ausgehend vom Ende letzter Woche @@ -735,74 +909,74 @@ item_structs: db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes verbrauch_2: name: Struct für Verbrauchsauswertung bei Zählern mit stetig ansteigendem Zählerstand (Teil 2) - verbrauch_gestern_minus3: - name: Verbrauch vor 3 Tagen - db_addon_fct: verbrauch_heute_minus3 - type: num - visu_acl: ro - cache: yes - - verbrauch_gestern_minus4: + verbrauch_tag_minus4: name: Verbrauch vor 4 Tagen - db_addon_fct: verbrauch_heute_minus4 + db_addon_fct: verbrauch_tag_minus4 type: num visu_acl: ro cache: yes - verbrauch_gestern_minus5: + verbrauch_tag_minus5: name: Verbrauch vor 5 Tagen - db_addon_fct: verbrauch_heute_minus5 + db_addon_fct: verbrauch_tag_minus5 type: num visu_acl: ro cache: yes - verbrauch_gestern_minus6: + verbrauch_tag_minus6: name: Verbrauch vor 6 Tagen - db_addon_fct: verbrauch_heute_minus6 + db_addon_fct: verbrauch_tag_minus6 type: num visu_acl: ro cache: yes - verbrauch_gestern_minus7: + verbrauch_tag_minus7: name: Verbrauch vor 7 Tagen - db_addon_fct: verbrauch_heute_minus7 + db_addon_fct: verbrauch_tag_minus7 + type: num + visu_acl: ro + cache: yes + + verbrauch_tag_minus8: + name: Verbrauch vor 8 Tagen + db_addon_fct: verbrauch_tag_minus8 type: num visu_acl: ro cache: yes - verbrauch_vorwoche_minus2: + verbrauch_woche_minus3: name: Verbrauch vor 3 Wochen db_addon_fct: verbrauch_woche_minus3 type: num visu_acl: ro cache: yes - verbrauch_vorwoche_minus3: + verbrauch_woche_minus4: name: Verbrauch vor 4 Wochen db_addon_fct: verbrauch_woche_minus4 type: num visu_acl: ro cache: yes - verbrauch_vormonat_minus1: + verbrauch_monat_minus2: name: Verbrauch vor 2 Monaten db_addon_fct: verbrauch_monat_minus2 type: num visu_acl: ro cache: yes - verbrauch_vormonat_minus2: + verbrauch_monat_minus3: name: Verbrauch vor 3 Monaten db_addon_fct: verbrauch_monat_minus3 type: num visu_acl: ro cache: yes - verbrauch_vormonat_minus3: + verbrauch_monat_minus4: name: Verbrauch vor 4 Monaten db_addon_fct: verbrauch_monat_minus4 type: num @@ -813,73 +987,73 @@ item_structs: name: Struct für die Erfassung von Zählerständen zu bestimmten Zeitpunkten bei Zählern mit stetig ansteigendem Zählerstand zaehlerstand_gestern: name: Zählerstand zum Ende des gestrigen Tages - db_addon_fct: zaehlerstand_heute_minus1 + db_addon_fct: zaehlerstand_tag_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - zaehlerstand_vorwoche: + zaehlerstand_woche_minus1: name: Zählerstand zum Ende der vorigen Woche db_addon_fct: zaehlerstand_woche_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - zaehlerstand_vormonat: + zaehlerstand_monat_minus1: name: Zählerstand zum Ende des Vormonates db_addon_fct: zaehlerstand_monat_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - zaehlerstand_vormonat_minus1: + zaehlerstand_monat_minus2: name: Zählerstand zum Monatsende vor 2 Monaten db_addon_fct: zaehlerstand_monat_minus2 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - zaehlerstand_vormonat_minus2: + zaehlerstand_monat_minus3: name: Zählerstand zum Monatsende vor 3 Monaten db_addon_fct: zaehlerstand_monat_minus3 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes - zaehlerstand_vorjahr: + zaehlerstand_jahr_minus1: name: Zählerstand am Ende des vorigen Jahres db_addon_fct: zaehlerstand_jahr_minus1 db_addon_startup: yes type: num visu_acl: ro - # cache: yes + cache: yes minmax_1: name: Struct für Auswertung der Wertehistorie bei schwankenden Werten wie bspw. Temperatur oder Leistung (Teil 1) - heute_min: + tag_min: name: Minimaler Wert seit Tagesbeginn - db_addon_fct: minmax_heute_min + db_addon_fct: minmax_tag_min db_addon_ignore_value: 0 db_addon_startup: yes type: num - # cache: yes + cache: yes - heute_max: + tag_max: name: Maximaler Wert seit Tagesbeginn - db_addon_fct: minmax_heute_max + db_addon_fct: minmax_tag_max db_addon_startup: yes type: num - # cache: yes + cache: yes - heute_avg: + tag_avg: name: Maximaler Wert seit Tagesbeginn - db_addon_fct: minmax_heute_avg + db_addon_fct: minmax_tag_avg db_addon_startup: yes type: num @@ -888,204 +1062,204 @@ item_structs: db_addon_fct: minmax_last_24h_min db_addon_startup: yes type: num - # cache: yes + cache: yes last24h_max: name: Maximaler Wert in den letzten 24h (gleitend) db_addon_fct: minmax_last_24h_max db_addon_startup: yes type: num - # cache: yes + cache: yes woche_min: name: Minimaler Wert seit Wochenbeginn db_addon_fct: minmax_woche_min db_addon_startup: yes type: num - # cache: yes + cache: yes woche_max: name: Maximaler Wert seit Wochenbeginn db_addon_fct: minmax_woche_max db_addon_startup: yes type: num - # cache: yes + cache: yes monat_min: name: Minimaler Wert seit Monatsbeginn db_addon_fct: minmax_monat_min db_addon_startup: yes type: num - # cache: yes + cache: yes monat_max: name: Maximaler Wert seit Monatsbeginn db_addon_fct: minmax_monat_max db_addon_startup: yes type: num - # cache: yes + cache: yes jahr_min: name: Minimaler Wert seit Jahresbeginn db_addon_fct: minmax_jahr_min db_addon_startup: yes type: num - # cache: yes + cache: yes jahr_max: name: Maximaler Wert seit Jahresbeginn db_addon_fct: minmax_jahr_max db_addon_startup: yes type: num - # cache: yes + cache: yes - gestern_min: + tag_minus1_min: name: Minimaler Wert gestern - db_addon_fct: minmax_heute_minus1_min + db_addon_fct: minmax_tag_minus1_min db_addon_startup: yes type: num - # cache: yes + cache: yes - gestern_max: + tag_minus1_max: name: Maximaler Wert gestern - db_addon_fct: minmax_heute_minus1_max + db_addon_fct: minmax_tag_minus1_max db_addon_startup: yes type: num - # cache: yes + cache: yes - gestern_avg: + tag_minus1_avg: name: Durchschnittlicher Wert gestern - db_addon_fct: minmax_heute_minus1_avg + db_addon_fct: minmax_tag_minus1_avg db_addon_startup: yes type: num - # cache: yes + cache: yes - vorwoche_min: + woche_minus1_min: name: Minimaler Wert in der Vorwoche db_addon_fct: minmax_woche_minus1_min db_addon_startup: yes type: num - # cache: yes + cache: yes - vorwoche_max: + woche_minus1_max: name: Maximaler Wert in der Vorwoche db_addon_fct: minmax_woche_minus1_max db_addon_startup: yes type: num - # cache: yes + cache: yes - vorwoche_avg: + woche_minus1_avg: name: Durchschnittlicher Wert in der Vorwoche db_addon_fct: minmax_woche_minus1_avg db_addon_startup: yes type: num - # cache: yes + cache: yes - vormonat_min: + monat_minus1_min: name: Minimaler Wert im Vormonat db_addon_fct: minmax_monat_minus1_min db_addon_startup: yes type: num - # cache: yes + cache: yes - vormonat_max: + monat_minus1_max: name: Maximaler Wert im Vormonat db_addon_fct: minmax_monat_minus1_max db_addon_startup: yes type: num - # cache: yes + cache: yes - vormonat_avg: + monat_minus1_avg: name: Durchschnittlicher Wert im Vormonat db_addon_fct: minmax_monat_minus1_avg db_addon_startup: yes type: num - # cache: yes + cache: yes - vorjahr_min: + jahr_minus1_min: name: Minimaler Wert im Vorjahr db_addon_fct: minmax_jahr_minus1_min db_addon_startup: yes type: num - # cache: yes + cache: yes - vorjahr_max: + jahr_minus1_max: name: Maximaler Wert im Vorjahr db_addon_fct: minmax_jahr_minus1_max db_addon_startup: yes type: num - # cache: yes + cache: yes minmax_2: name: Struct für Auswertung der Wertehistorie bei schwankenden Werten wie bspw. Temperatur oder Leistung (Teil 2) - gestern_minus1_min: + tag_minus2_min: name: Minimaler Wert vorgestern - db_addon_fct: minmax_heute_minus2_min + db_addon_fct: minmax_tag_minus2_min type: num cache: yes - gestern_minus1_max: + tag_minus2_max: name: Maximaler Wert vorgestern - db_addon_fct: minmax_heute_minus2_max + db_addon_fct: minmax_tag_minus2_max type: num cache: yes - gestern_minus1_avg: + tag_minus2_avg: name: Durchschnittlicher Wert vorgestern - db_addon_fct: minmax_heute_minus2_avg + db_addon_fct: minmax_tag_minus2_avg type: num cache: yes - gestern_minus2_min: + tag_minus3_min: name: Minimaler Wert vor 3 Tagen - db_addon_fct: minmax_heute_minus3_min + db_addon_fct: minmax_tag_minus3_min type: num cache: yes - gestern_minus2_max: + tag_minus3_max: name: Maximaler Wert vor 3 Tagen - db_addon_fct: minmax_heute_minus3_max + db_addon_fct: minmax_tag_minus3_max type: num cache: yes - gestern_minus2_avg: + tag_minus3_avg: name: Durchschnittlicher Wert vor 3 Tagen - db_addon_fct: minmax_heute_minus3_avg + db_addon_fct: minmax_tag_minus3_avg type: num cache: yes - vorwoche_minus1_min: + woche_minus2_min: name: Minimaler Wert in der Woche vor 2 Wochen db_addon_fct: minmax_woche_minus2_min type: num cache: yes - vorwoche_minus1_max: + woche_minus2_max: name: Maximaler Wert in der Woche vor 2 Wochen db_addon_fct: minmax_woche_minus2_max type: num cache: yes - vorwoche_minus1_avg: + woche_minus2_avg: name: Durchschnittlicher Wert in der Woche vor 2 Wochen db_addon_fct: minmax_woche_minus2_avg type: num cache: yes - vormonat_minus1_min: + monat_minus2_min: name: Minimaler Wert im Monat vor 2 Monaten db_addon_fct: minmax_monat_minus2_min type: num cache: yes - vormonat_minus1_max: + monat_minus2_max: name: Maximaler Wert im Monat vor 2 Monaten db_addon_fct: minmax_monat_minus2_max type: num cache: yes - vormonat_minus1_avg: + monat_minus2_avg: name: Durchschnittlicher Wert im Monat vor 2 Monaten db_addon_fct: minmax_monat_minus2_avg type: num diff --git a/db_addon/requirements.txt b/db_addon/requirements.txt old mode 100755 new mode 100644 diff --git a/db_addon/user_doc.rst b/db_addon/user_doc.rst old mode 100755 new mode 100644 index 6f7978e1d..9fb0a207b --- a/db_addon/user_doc.rst +++ b/db_addon/user_doc.rst @@ -29,7 +29,7 @@ ermittelt. Alternativ kann mit dem Attribute "db_addon_database_item" auch der absolute Pfad des Items angegeben werden, für das das Database Attribut konfiguriert ist. -Bsp: +Beispiel: .. code-block:: yaml @@ -97,37 +97,36 @@ Dazu folgenden Block am Ende der Datei */etc/mysql/my.cnf* einfügen bzw den exi Hinweise ======== - - Das Plugin startet die Berechnungen der Werte nach einer gewissen (konfigurierbaren) Zeit (Attribut `startup_run_delay`) - nach dem Start von shNG, um den Startvorgang nicht zu beeinflussen. +- Das Plugin startet die Berechnungen der Werte nach einer gewissen (konfigurierbaren) Zeit (Attribut `startup_run_delay`) + nach dem Start von shNG, um den Startvorgang nicht zu beeinflussen. - - Bei Start werden automatisch nur die Items berechnet, für das das Attribute `db_addon_startup` gesetzt wurde. Alle anderen - Items werden erst zur konfigurierten Zeit berechnet. Das Attribute `db_addon_startup` kann auch direkt am `Database-Item` - gesetzt werden. Dabei wird das Attribut auf alle darunter liegenden `db_addon-Items` (bspw. bei Verwendung von structs) vererbt. - Über das WebIF kann die Berechnung aller definierten Items ausgelöst werden. +- Bei Start werden automatisch nur die Items berechnet, für das das Attribute `db_addon_startup` gesetzt wurde. Alle anderen + Items werden erst zur konfigurierten Zeit berechnet. Das Attribute `db_addon_startup` kann auch direkt am `Database-Item` + gesetzt werden. Dabei wird das Attribut auf alle darunter liegenden `db_addon-Items` (bspw. bei Verwendung von structs) vererbt. + Über das WebIF kann die Berechnung aller definierten Items ausgelöst werden. - - Für sogenannte `on_change` Items, also Items, deren Berechnung bis zum Jetzt (bspw. verbrauch-heute) gehen, wird die Berechnung - immer bei eintreffen eines neuen Wertes gestartet. Zu Reduktion der Belastung auf die Datenbank werden die Werte für das Ende der - letzten Periode gecached. +- Für sogenannte `on_change` Items, also Items, deren Berechnung bis zum Jetzt (bspw. verbrauch-heute) gehen, wird die Berechnung + immer bei eintreffen eines neuen Wertes gestartet. Zu Reduktion der Belastung auf die Datenbank werden die Werte für das Ende der + letzten Periode gecached. - - Berechnungen werden nur ausgeführt, wenn für den kompletten abgefragten Zeitraum Werte in der Datenbank vorliegen. Wird bspw. - der Verbrauch des letzten Monats abgefragt wobei erst Werte ab dem 3. des Monats in der Datenbank sind, wird die Berechnung abgebrochen. +- Berechnungen werden nur ausgeführt, wenn für den kompletten abgefragten Zeitraum Werte in der Datenbank vorliegen. Wird bspw. + der Verbrauch des letzten Monats abgefragt wobei erst Werte ab dem 3. des Monats in der Datenbank sind, wird die Berechnung abgebrochen. - - Mit dem Attribut `use_oldest_entry` kann dieses Verhalten verändert werden. Ist das Attribut gesetzt, wird, wenn für den - Beginn der Abfragezeitraums keinen Werte vorliegen, der älteste Eintrag der Datenbank genutzt. +- Mit dem Attribut `use_oldest_entry` kann dieses Verhalten verändert werden. Ist das Attribut gesetzt, wird, wenn für den + Beginn der Abfragezeitraums keinen Werte vorliegen, der älteste Eintrag der Datenbank genutzt. - - Für die Auswertung kann es nützlich sein, bestimmte Werte aus der Datenbank bei der Berechnung auszublenden. Hierfür stehen - 2 Möglichkeiten zur Verfügung: +- Für die Auswertung kann es nützlich sein, bestimmte Werte aus der Datenbank bei der Berechnung auszublenden. Hierfür stehen 2 Möglichkeiten zur Verfügung: - Plugin-Attribut `ignore_0`: (list of strings) Bei Items, bei denen ein String aus der Liste im Pfadnamen vorkommt, werden 0-Werte (val_num = 0) bei Datenbankauswertungen ignoriert. Hat also das Attribut den Wert ['temp'] werden bei allen Items mit 'temp' im Pfadnamen die 0-Werte bei der Auswertung ignoriert. - - Item-Attribut `db_addon_ignore_value`: (num) Dieser Wert wird bei der Abfrage bzw. Auswertung der Datenbank für diese - Item ignoriert. - - Das Plugin enthält sehr ausführliche Logginginformation. Bei unerwartetem Verhalten, den LogLevel entsprechend anpassen, - um mehr information zu erhalten. + - Item-Attribut `db_addon_ignore_value`: (num) Dieser Wert wird bei der Abfrage bzw. Auswertung der Datenbank für dieses Item ignoriert. - - Berechnungen des Plugins können im WebIF unterbrochen werden. Auch das gesamte Plugin kann pausiert werden. Dies kann bei - starker Systembelastung nützlich sein. +- Das Plugin enthält sehr ausführliche Logginginformation. Bei unerwartetem Verhalten, den LogLevel entsprechend anpassen, + um mehr information zu erhalten. + +- Berechnungen des Plugins können im WebIF unterbrochen werden. Auch das gesamte Plugin kann pausiert werden. Dies kann bei + starker Systembelastung nützlich sein. Beispiele @@ -140,6 +139,7 @@ Soll bspw. der Verbrauch von Wasser ausgewertet werden, so ist dies wie folgt m .. code-block:: yaml + wasserzaehler: zaehlerstand: type: num @@ -161,6 +161,7 @@ minmax Soll bspw. die minimalen und maximalen Temperaturen ausgewertet werden, kann dies so umgesetzt werden: .. code-block:: yaml + temperature: aussen: nord: @@ -177,7 +178,6 @@ Soll bspw. die minimalen und maximalen Temperaturen ausgewertet werden, kann die Die Temperaturwerte werden in die Datenbank geschrieben und darauf basierend ausgewertet. Die structs 'db_addon.minmax_1' und 'db_addon.minmax_2' stellen entsprechende Items für die min/max Auswertung zur Verfügung. -| Web Interface ============= @@ -218,8 +218,8 @@ Erläuterungen zu Temperatursummen Grünlandtemperatursumme ----------------------- -Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Düngungsmaßnahmen zu bestimmen. -Dabei erfolgt die Aufsummierung der Tagesmitteltemperaturen über 0 °C, wobei der Januar mit 0.5 und der Februar mit 0.75 gewichtet wird. +Beim Grünland wird die Wärmesumme nach Ernst und Loeper benutzt, um den Vegetationsbeginn und somit den Termin von Düngungsmaßnahmen zu bestimmen. +Dabei erfolgt die Aufsummierung der Tagesmitteltemperaturen über 0 °C, wobei der Januar mit 0.5 und der Februar mit 0.75 gewichtet wird. Bei einer Wärmesumme von 200 Grad ist eine Düngung angesagt. siehe: https://de.wikipedia.org/wiki/Gr%C3%BCnlandtemperatursumme @@ -228,6 +228,7 @@ Folgende Parameter sind möglich / notwendig: .. code-block:: yaml + db_addon_params: "year=current" - year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') @@ -235,10 +236,10 @@ Folgende Parameter sind möglich / notwendig: Wachstumsgradtag ---------------- -Der Begriff Wachstumsgradtage (WGT) ist ein Überbegriff für verschiedene Größen. -Gemeinsam ist ihnen, daß zur Berechnung eine Lufttemperatur von einem Schwellenwert subtrahiert wird. -Je nach Fragestellung und Pflanzenart werden der Schwellenwert unterschiedlich gewählt und die Temperatur unterschiedlich bestimmt. -Verfügbar sind die Berechnung über 0) "einfachen Durchschnitt der Tagestemperaturen", 1) "modifizierten Durchschnitt der Tagestemperaturen" +Der Begriff Wachstumsgradtage (WGT) ist ein Überbegriff für verschiedene Größen. +Gemeinsam ist ihnen, daß zur Berechnung eine Lufttemperatur von einem Schwellenwert subtrahiert wird. +Je nach Fragestellung und Pflanzenart werden der Schwellenwert unterschiedlich gewählt und die Temperatur unterschiedlich bestimmt. +Verfügbar sind die Berechnung über 0) "einfachen Durchschnitt der Tagestemperaturen", 1) "modifizierten Durchschnitt der Tagestemperaturen" und 2) Anzahl der Tage, deren Mitteltempertatur oberhalb der Schwellentemperatur lag. siehe https://de.wikipedia.org/wiki/Wachstumsgradtag @@ -246,11 +247,12 @@ siehe https://de.wikipedia.org/wiki/Wachstumsgradtag Folgende Parameter sind möglich / notwendig: .. code-block:: yaml + db_addon_params: "year=current, method=1, threshold=10" - year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') - method: 0-Berechnung über "einfachen Durchschnitt der Tagestemperaturen", 1-Berechnung über "modifizierten Durchschnitt (default: 0) -der Tagestemperaturen" 2-Anzahl der Tage, mit Mitteltempertatur oberhalb Schwellentemperatur// 10, 11 Ausgabe aus Zeitserie + der Tagestemperaturen" 2-Anzahl der Tage, mit Mitteltempertatur oberhalb Schwellentemperatur// 10, 11 Ausgabe aus Zeitserie - threshold: Schwellentemperatur in °C (int) (default: 10) @@ -258,13 +260,14 @@ Wärmesumme ---------- Die Wärmesumme soll eine Aussage über den Sommer und die Pflanzenreife liefern. Es gibt keine eindeutige Definition der Größe "Wärmesumme". -Berechnet wird die Wärmesumme als Summe aller Tagesmitteltemperaturen über einem Schwellenwert ab dem 1.1. des Jahres. +Berechnet wird die Wärmesumme als Summe aller Tagesmitteltemperaturen über einem Schwellenwert ab dem 1.1. des Jahres. siehe https://de.wikipedia.org/wiki/W%C3%A4rmesumme Folgende Parameter sind möglich / notwendig: .. code-block:: yaml + db_addon_params: "year=current, month=1, threshold=10" - year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') @@ -275,7 +278,7 @@ Folgende Parameter sind möglich / notwendig: Kältesumme ---------- -Die Kältesumme soll eine Aussage über die Härte des Winters liefern. +Die Kältesumme soll eine Aussage über die Härte des Winters liefern. Berechnet wird die Kältesumme als Summe aller negativen Tagesmitteltemperaturen ab dem 21.9. des Jahres bis 31.3. des Folgejahres. siehe https://de.wikipedia.org/wiki/K%C3%A4ltesumme @@ -283,6 +286,7 @@ siehe https://de.wikipedia.org/wiki/K%C3%A4ltesumme Folgende Parameter sind möglich / notwendig: .. code-block:: yaml + db_addon_params: "year=current, month=1" - year: Jahreszahl (str oder int), für das die Berechnung ausgeführt werden soll oder "current" für aktuelles Jahr (default: 'current') diff --git a/db_addon/webif/__init__.py b/db_addon/webif/__init__.py old mode 100755 new mode 100644 index 604f6b147..c7f7c6a71 --- a/db_addon/webif/__init__.py +++ b/db_addon/webif/__init__.py @@ -59,7 +59,7 @@ def __init__(self, webif_dir, plugin): self.tplenv = self.init_template_environment() @cherrypy.expose - def index(self, reload=None): + def index(self, reload=None, action=None, item_path=None, active=None, option=None): """ Build index.html for cherrypy @@ -68,17 +68,31 @@ def index(self, reload=None): :return: contents of the template after being rendered """ + pagelength = self.plugin.get_parameter_value('webif_pagelength') tmpl = self.tplenv.get_template('index.html') + if action is not None: + if action == "recalc_item" and item_path is not None: + self.logger.info(f"Recalc of item={item_path} called via WebIF. Item put to Queue for new calculation.") + self.plugin.execute_items(option='item', item=item_path) + + elif action == "clean_item_cache" and item_path is not None: + self.logger.info(f"Clean item cache of item={item_path} called via WebIF. Plugin item value cache will be cleaned.") + self.plugin._clean_item_cache(item=item_path) + + elif action == "_activate_item_calculation" and item_path is not None and active is not None: + self.logger.info(f"Item calculation of item={item_path} will be set to {bool(int(active))} via WebIF.") + self.plugin._activate_item_calculation(item=item_path, active=bool(int(active))) + return tmpl.render(p=self.plugin, - webif_pagelength=self.plugin.get_parameter_value('webif_pagelength'), + webif_pagelength=pagelength, suspended='true' if self.plugin.suspended else 'false', items=self.plugin.get_item_list('db_addon', 'function'), item_count=len(self.plugin.get_item_list('db_addon', 'function')), plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), - maintenance=True if self.plugin.log_level == 10 else False, + maintenance=True if self.plugin.log_level < 20 else False, ) @cherrypy.expose @@ -94,24 +108,70 @@ def get_data_html(self, dataSet=None): if dataSet is None: # get the new data data = dict() - data['items'] = {} + data['items'] = {} for item in self.plugin.get_item_list('db_addon', 'function'): - data['items'][item.id()] = {} - data['items'][item.id()]['value'] = item.property.value - data['items'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') - data['items'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + data['items'][item.path()] = {} + data['items'][item.path()]['value'] = item.property.value + data['items'][item.path()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') + data['items'][item.path()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') data['plugin_suspended'] = self.plugin.suspended data['maintenance'] = True if self.plugin.log_level == 10 else False data['queue_length'] = self.plugin.queue_backlog() data['active_queue_item'] = self.plugin.active_queue_item + data['debug_log'] = {} + for debug in ['parse', 'execute', 'ondemand', 'onchange', 'prepare', 'sql']: + data['debug_log'][debug] = getattr(self.plugin.debug_log, debug) + try: return json.dumps(data, default=str) except Exception as e: self.logger.error(f"get_data_html exception: {e}") + @cherrypy.expose + def submit(self, item=None): + result = None + item_path, cmd = item.split(':') + if item_path is not None and cmd is not None: + self.logger.debug(f"Received db_addon {cmd=} for {item_path=} via web interface") + + if cmd == "recalc_item": + self.logger.info(f"Recalc of item={item_path} called via WebIF. Item put to Queue for new calculation.") + result = self.plugin.execute_items(option='item', item=item_path) + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') + + elif cmd == "clean_item_cache": + self.logger.info(f"Clean item cache of item={item_path} called via WebIF. Plugin item value cache will be cleaned.") + result = self.plugin._clean_item_cache(item=item_path) + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') + + elif cmd.startswith("suspend_plugin_calculation"): + self.logger.debug(f"set_plugin_calculation {cmd=}") + cmd, value = cmd.split(',') + value = True if value == "True" else False + self.logger.info(f"Plugin will be set to suspended: {value} via WebIF.") + result = self.plugin.suspend(value) + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') + + elif cmd.startswith("suspend_item_calculation"): + cmd, value = cmd.split(',') + self.logger.info(f"Item calculation of item={item_path} will be set to suspended: {value} via WebIF.") + value = True if value == "True" else False + result = self.plugin._suspend_item_calculation(item=item_path, suspended=value) + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') + + if result is not None: + # JSON zurücksenden + cherrypy.response.headers['Content-Type'] = 'application/json' + self.logger.debug(f"Result for web interface: {result}") + return json.dumps(result).encode('utf-8') + @cherrypy.expose def recalc_all(self): self.logger.debug(f"recalc_all called") @@ -136,3 +196,69 @@ def activate(self): def suspend(self): self.logger.debug(f"suspend called") self.plugin.suspend(True) + + @cherrypy.expose + def debug_log_option(self, log: str = None, state: bool = None): + self.logger.warning(f"debug_log_option called with {log=}, {state=}") + _state = True if state == 'true' else False + setattr(self.plugin.debug_log, log, _state) + + @cherrypy.expose + def debug_log_option_parse_true(self): + self.logger.debug("debug_log_option_parse_true") + setattr(self.plugin.debug_log, 'parse', True) + + @cherrypy.expose + def debug_log_option_parse_false (self): + self.logger.debug("debug_log_option_parse_false") + setattr(self.plugin.debug_log, 'parse', False) + + @cherrypy.expose + def debug_log_option_execute_true(self): + self.logger.debug("debug_log_option_execute_true") + setattr(self.plugin.debug_log, 'execute', True) + + @cherrypy.expose + def debug_log_option_execute_false (self): + self.logger.debug("debug_log_option_execute_false") + setattr(self.plugin.debug_log, 'execute', False) + + @cherrypy.expose + def debug_log_option_ondemand_true(self): + self.logger.debug("debug_log_option_ondemand_true") + setattr(self.plugin.debug_log, 'ondemand', True) + + @cherrypy.expose + def debug_log_option_ondemand_false (self): + self.logger.debug("debug_log_option_ondemand_false") + setattr(self.plugin.debug_log, 'ondemand', False) + + @cherrypy.expose + def debug_log_option_onchange_true(self): + self.logger.debug("debug_log_option_onchange_true") + setattr(self.plugin.debug_log, 'onchange', True) + + @cherrypy.expose + def debug_log_option_onchange_false (self): + self.logger.debug("debug_log_option_onchange_false") + setattr(self.plugin.debug_log, 'onchange', False) + + @cherrypy.expose + def debug_log_option_prepare_true(self): + self.logger.debug("debug_log_option_prepare_true") + setattr(self.plugin.debug_log, 'prepare', True) + + @cherrypy.expose + def debug_log_option_prepare_false (self): + self.logger.debug("debug_log_option_prepare_false") + setattr(self.plugin.debug_log, 'prepare', False) + + @cherrypy.expose + def debug_log_option_sql_true(self): + self.logger.debug("debug_log_option_sql_true") + setattr(self.plugin.debug_log, 'sql', True) + + @cherrypy.expose + def debug_log_option_sql_false (self): + self.logger.debug("debug_log_option_sql_false") + setattr(self.plugin.debug_log, 'sql', False) diff --git a/db_addon/webif/static/img/plugin_logo.png b/db_addon/webif/static/img/plugin_logo.png old mode 100755 new mode 100644 diff --git a/db_addon/webif/templates/index.html b/db_addon/webif/templates/index.html old mode 100755 new mode 100644 index 62f398e70..270751ff7 --- a/db_addon/webif/templates/index.html +++ b/db_addon/webif/templates/index.html @@ -35,6 +35,9 @@ } table th.last { width: 150px; + } + table th.aktion { + width: 100px; } table th.dict { width: 150px; @@ -56,11 +59,69 @@ .shng_effect_standard { background-color: none; } + button.actionbutton { + width: 32px; + } {% endblock pluginstyles %} {% block pluginscripts %} + + - + + + {% endblock pluginscripts %} {% block headtable %} - - + {% set first = True %} - {% for key, value in p._db._params.items() %} - {% if loop.index % 4 == 0 %} - - {% endif %} - {% if key != "passwd" %} - - {% else %} - - {% endif %} - {% if loop.index % 3 > 0 and loop.last %} - - {% endif %} - {% if loop.index % 4 == 0 and not first %} - - {% endif %} - {% endfor %} + {% if p._db %} + {% for key, value in p._db._params.items() %} + {% if loop.index % 4 == 0 %} + + {% endif %} + {% if key != "passwd" %} + + {% else %} + + {% endif %} + {% if loop.index % 3 > 0 and loop.last %} + + {% endif %} + {% if loop.index % 4 == 0 and not first %} + + {% endif %} + {% endfor %} + {% endif %} - + - - + + + +
{{ _('Verbunden') }}{% if p._db._connected %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}{% if p._db._connected %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %} {{ _('Treiber') }} {{ p.db_driver }} {{ _('Startup Delay') }} {{ (p.startup_run_delay) }}s
{{ key }}{{ value }}{{ key }}{% for letter in value %}*{% endfor %}
{{ key }}{{ value }}{{ key }}{% for letter in value %}*{% endfor %}
{{ _('Item in Berechnung') }}{{ _('Item in Berechnung') }} {{ p.active_queue_item }}{{ _('Arbeitsvorrat') }}{{ p.queue_backlog }} {{ _('Items') }} {{ _('Arbeitsvorrat') }}{{ p.queue_backlog }} {{ _('Items') }} {{ _('LogLevel') }} + {{ p.log_level }} + {% if p.log_level == 10 %} + {{' || ' }} +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ {% endif %} +
@@ -271,22 +367,20 @@ -
- - -
+ + +
+ + +
{% endblock %} {% set tabcount = 3 %} {% set tab1title = "" ~ plugin_shortname ~ " Items (" ~ item_count ~ ")" %} -{% if maintenance %} - {% set tab2title = "" ~ plugin_shortname ~ " Maintenance" %} -{% else %} - {% set tab2title = "hidden" %} -{% endif %} +{% set tab2title = "" ~ plugin_shortname ~ " Maintenance" %} {% set tab3title = "" ~ plugin_shortname ~ " API/Doku" %} @@ -309,13 +403,23 @@ {{ p.get_item_config(item._path)['db_addon_fct'] }} {{ _(p.get_item_config(item)['cycle']|string) }} {% if p.get_item_config(item)['startup'] %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %} - {{ item._value | float | round(2) }} - {{ item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') }} - {{ item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') }} +   +   +   + + + + + + {% endfor %} +
+ + +
{% endblock bodytab1 %}