From 5635c05aac9d82b2858b08756fccbd145aedce8d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 20 Nov 2024 08:00:52 +0100 Subject: [PATCH 01/34] uzsu plugin: "once" feature to trigger uzsu entry only once and deactivate afterwards --- uzsu/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index c6483b9a4..4659bf584 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -306,7 +306,7 @@ def resume(self, activevalue=True, item=None): self.logger.info(f'Resuming item {item}: Activated and value set to {lastvalue}. Active value: {activevalue}') return lastvalue - def activate(self, activevalue=None, item=None): + def activate(self, activevalue=None, item=None, caller='logic'): if self._items.get(item) is None: try: self.logger.warning(f'Item {item.property.path} is no valid UZSU item!') @@ -323,8 +323,8 @@ def activate(self, activevalue=None, item=None): if isinstance(activevalue, bool): self._items[item] = item() self._items[item]['active'] = activevalue - self.logger.info(f'Item {item} is set via logic to: {activevalue}') - self._update_item(item, 'UZSU Plugin', 'logic') + self.logger.info(f'Item {item} is set via {caller} to: {activevalue}') + self._update_item(item, 'UZSU Plugin', caller) return activevalue if activevalue is None: return self._items[item].get('active') @@ -780,6 +780,8 @@ def _set(self, item=None, value=None, caller=None): _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) + if self._items[item].get('once'): + self.activate(False, item, 'once') if not caller or caller == "Scheduler": self._schedule(item, caller='set') From a01adce9ba1646de2c82fb0d352138d5b41c8e83 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 21 Nov 2024 18:13:06 +0100 Subject: [PATCH 02/34] uzsu plugin: logging improvement --- uzsu/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 4659bf584..4f669f705 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -323,7 +323,8 @@ def activate(self, activevalue=None, item=None, caller='logic'): if isinstance(activevalue, bool): self._items[item] = item() self._items[item]['active'] = activevalue - self.logger.info(f'Item {item} is set via {caller} to: {activevalue}') + _activeinfo = "deactivated" if activevalue is False else "activated" + self.logger.info(f'Item {item} is set via {caller} to: {_activeinfo}') self._update_item(item, 'UZSU Plugin', caller) return activevalue if activevalue is None: From 9e7f94a7170d96151f08c12b6b0963d22036a8d9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 1 Dec 2024 20:10:02 +0100 Subject: [PATCH 03/34] uzsu: make once work with individual list entries --- uzsu/__init__.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 4f669f705..eb440eee1 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -653,6 +653,7 @@ def _schedule(self, item, caller=None): _caller = "dry_run" _next = None _value = None + _listentry = None self._update_sun(item, caller=_caller) self._add_dicts(item) if not self._items[item]['interpolation'].get('itemtype') or \ @@ -678,10 +679,12 @@ def _schedule(self, item, caller=None): if _next is None: _next = next _value = value + _listentry = i elif next and next < _next: self.logger.debug(f'uzsu active entry for item {item} using now {next}, value {value} and tzinfo {next.tzinfo}') _next = next _value = value + _listentry = i else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') elif not self._items[item].get('list') and self._items[item].get('active') is True: @@ -762,7 +765,7 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self.scheduler_add(item.property.path, self._set, - value={'item': item, 'value': _value, 'caller': 'Scheduler'}, next=_next) + value={'item': item, 'value': _value, 'caller': 'Scheduler', 'listentry': _listentry}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -771,18 +774,25 @@ def _schedule(self, item, caller=None): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - def _set(self, item=None, value=None, caller=None): + def _set(self, item=None, value=None, caller=None, listentry=None): """ This function sets the specific item - :param item: item to be updated towards the plugin - :param value: value the item should be set to - :param caller: if given it represents the callers name + :param item: item to be updated towards the plugin + :param value: value the item should be set to + :param caller: if given it represents the callers name + :param listentry: if given it represents the index of the entry in uzsu list responsible for setting the value """ _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) + if self._items[item]['list'][listentry].get('once'): + self._items[item]['list'][listentry]['active'] = False + self._update_item(item, 'UZSU Plugin', 'once') + self.logger.debug( + f'Deactivate list entry {self._items[item]["list"][listentry]} of item {item} as it has "once" set to True') if self._items[item].get('once'): self.activate(False, item, 'once') + self.logger.debug(f'Deactivate UZSU for item {item} as it has "once" set to True') if not caller or caller == "Scheduler": self._schedule(item, caller='set') From cbdbe7f1bd19fc033b9a121b32d7696d9c49c451 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 2 Dec 2024 06:29:32 +0100 Subject: [PATCH 04/34] uzsu: make 'once' work with interpolation --- uzsu/__init__.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index eb440eee1..2ff78c0a4 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -570,7 +570,7 @@ def update_item(self, item, caller=None, source='', dest=None): if self._items[item] != self.itemsApi.return_item(str(item)) and cond: self._update_item(item, 'UZSU Plugin', 'update') - def _update_item(self, item, caller="", comment=""): + def _update_item(self, item, caller="", comment="", entryindex=None): success = self._get_sun4week(item, caller="_update_item") if success: self.logger.debug(f'Updated weekly sun info for item {item} caller: {caller} comment: {comment}') @@ -581,6 +581,8 @@ def _update_item(self, item, caller="", comment=""): self.logger.debug(f'Updated seriesCalculated for item {item} caller: {caller} comment: {comment}') else: self.logger.debug(f'Issues with updating seriesCalculated for item {item} caller: {caller} comment: {comment}, issue: {success}') + if entryindex: + self._items[item]['list'][entryindex]['active'] = False success = self._update_sun(item, caller="_update_item") if success is True: self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') @@ -653,7 +655,7 @@ def _schedule(self, item, caller=None): _caller = "dry_run" _next = None _value = None - _listentry = None + _entryindex = None self._update_sun(item, caller=_caller) self._add_dicts(item) if not self._items[item]['interpolation'].get('itemtype') or \ @@ -679,12 +681,12 @@ def _schedule(self, item, caller=None): if _next is None: _next = next _value = value - _listentry = i + _entryindex = i elif next and next < _next: self.logger.debug(f'uzsu active entry for item {item} using now {next}, value {value} and tzinfo {next.tzinfo}') _next = next _value = value - _listentry = i + _entryindex = i else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') elif not self._items[item].get('list') and self._items[item].get('active') is True: @@ -693,6 +695,7 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) if _next and _value is not None and (self._items[item].get('active') is True or _caller == "dry_run"): _reset_interpolation = False + _interpolated = False _interval = self._items[item]['interpolation'].get('interval') _interval = self._interpolation_interval if not _interval else int(_interval) if _interval < 0: @@ -745,6 +748,7 @@ def _schedule(self, item, caller=None): elif _interpolation.lower() in ('cubic', 'linear') and _interval > 0: try: _nextinterpolation = datetime.now(self._timezone) + timedelta(minutes=_interval) + _interpolated = True if _next > _nextinterpolation else False _next = _nextinterpolation if _next > _nextinterpolation else _next _value = self._interpolate(self._itpl[item], _next.timestamp() * 1000.0, _interpolation.lower() == 'linear') _value_now = self._interpolate(self._itpl[item], entry_now, _interpolation.lower() == 'linear') @@ -765,7 +769,7 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self.scheduler_add(item.property.path, self._set, - value={'item': item, 'value': _value, 'caller': 'Scheduler', 'listentry': _listentry}, next=_next) + value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -774,22 +778,23 @@ def _schedule(self, item, caller=None): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - def _set(self, item=None, value=None, caller=None, listentry=None): + def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False): """ This function sets the specific item - :param item: item to be updated towards the plugin - :param value: value the item should be set to - :param caller: if given it represents the callers name - :param listentry: if given it represents the index of the entry in uzsu list responsible for setting the value + :param item: item to be updated towards the plugin + :param value: value the item should be set to + :param caller: if given it represents the callers name + :param entryindex: if given it represents the index of the entry in uzsu list responsible for setting the value + :param interpolated: True if value is set due to interpolation, False if uzsu trigger equals exact entry in list """ _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) - if self._items[item]['list'][listentry].get('once'): - self._items[item]['list'][listentry]['active'] = False - self._update_item(item, 'UZSU Plugin', 'once') + if self._items[item]['list'][entryindex].get('once') and not interpolated: + + self._update_item(item, 'UZSU Plugin', 'once', entryindex) self.logger.debug( - f'Deactivate list entry {self._items[item]["list"][listentry]} of item {item} as it has "once" set to True') + f'Deactivate list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') if self._items[item].get('once'): self.activate(False, item, 'once') self.logger.debug(f'Deactivate UZSU for item {item} as it has "once" set to True') @@ -805,8 +810,9 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): date rrule dtstart - :param item: item to be updated towards the plugin :param timescan: defines whether to find values in the future or past + :param item: item to be updated towards the plugin + :param entryindex: if given it represents the index of the entry in uzsu list responsible for setting the value :param caller: defines the caller of the method. If it's name is dry_run just simulate getting time even if entry is not active """ From b06198f6385a1d380e510d2eb63ca98e75cb2940 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Dec 2024 22:23:07 +0100 Subject: [PATCH 05/34] uzsu: implement once for series, part 1 --- uzsu/__init__.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 2ff78c0a4..0aae66e10 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -656,6 +656,7 @@ def _schedule(self, item, caller=None): _next = None _value = None _entryindex = None + _series = False self._update_sun(item, caller=_caller) self._add_dicts(item) if not self._items[item]['interpolation'].get('itemtype') or \ @@ -669,8 +670,10 @@ def _schedule(self, item, caller=None): elif self._items[item].get('active') is True or _caller == "dry_run": self._itpl[item] = OrderedDict() for i, entry in enumerate(self._items[item]['list']): - next, value = self._get_time(entry, 'next', item, i, _caller) - previous, previousvalue = self._get_time(entry, 'previous', item, i, _caller) + next, value, next_series = self._get_time(entry, 'next', item, i, _caller) + previous, previousvalue, previous_series = self._get_time(entry, 'previous', item, i, _caller) + _series = True if next_series + previous_series >= 3 else False + self.logger.debug(f'next series value is {next_series}, prev {previous_series}') cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: @@ -763,6 +766,9 @@ def _schedule(self, item, caller=None): self._items[item]['interpolation']['type'] = 'none' self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": + if self._items[item]["list"][_entryindex].get("series"): + self.logger.debug(f'Series is running? {_series}') + self._items[item]["list"][_entryindex]["series"]["running"] = _series self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) @@ -823,15 +829,16 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): time = None try: if not isinstance(entry, dict): - return None, None + return None, None, None if 'value' not in entry: - return None, None + return None, None, None if 'active' not in entry: - return None, None + return None, None, None if 'time' not in entry: - return None, None + return None, None, None value = entry['value'] next = None + series = 0 active = True if caller == "dry_run" else entry['active'] today = datetime.today() tomorrow = today + timedelta(days=1) @@ -839,7 +846,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): weekbefore = today - timedelta(days=7) time = entry['time'] if not active: - return None, None + return None, None, None if 'rrule' in entry and 'series' not in time: if entry['rrule'] == '': entry['rrule'] = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU' @@ -868,7 +875,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): while self.alive: dt = rrule.before(dt) if timescan == 'previous' else rrule.after(dt) if dt is None: - return None, None + return None, None, None if 'sun' in time: sleep(0.01) next = self._sun(datetime.combine(dt.date(), @@ -884,7 +891,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self._itpl[item][next.timestamp() * 1000.0] = value if next - timedelta(seconds=1) > datetime.now().replace(tzinfo=self._timezone): self.logger.debug(f'{item}: Return from rrule {timescan}: {next}, value {value}.') - return next, value + return next, value, series else: self.logger.debug(f"{item}: Not returning {timescan} rrule {next} because it's in the past.") if 'sun' in time and 'series' not in time: @@ -914,8 +921,9 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): # Get next Time for Series next = self._series_get_time(entry, timescan) if next is None: - return None, None + return None, None, None self._itpl[item][next.timestamp() * 1000.0] = value + series += 1 rstr = str(entry['rrule']).replace('\n', ';') self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} with start-time {entry["series"]["timeSeriesMin"]}') @@ -928,23 +936,26 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if next and cond_today and cond_next: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next today: {next}, value {value}') - return next, value + series += 1 + return next, value, series if next and cond_tomorrow and cond_next: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next tomorrow: {next}, value {value}') - return next, value + return next, value, series if 'series' in time and next and cond_next: self.logger.debug(f'{item}: Return next for series: {next}, value {value}') - return next, value + return next, value, series if next and cond_today and cond_previous_today: self._itpl[item][(next - timedelta(seconds=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous today {next} because it‘s in the past.') + return None, None, series if next and cond_yesterday and cond_previous_yesterday: self._itpl[item][(next - timedelta(days=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous yesterday {next} because it‘s in the past.') + return None, None, series except Exception as e: self.logger.error(f'{item}: Error "{time}" parsing time: {e}') - return None, None + return None, None, None def _series_calculate(self, item, caller=None, source=None): """ From e71299b519869dd031f3f5a0b8982b74a29a2d7e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Dec 2024 23:40:11 +0100 Subject: [PATCH 06/34] uzsu: make once work with series --- uzsu/__init__.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 0aae66e10..348206544 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -656,7 +656,7 @@ def _schedule(self, item, caller=None): _next = None _value = None _entryindex = None - _series = False + _series = {} self._update_sun(item, caller=_caller) self._add_dicts(item) if not self._items[item]['interpolation'].get('itemtype') or \ @@ -672,8 +672,7 @@ def _schedule(self, item, caller=None): for i, entry in enumerate(self._items[item]['list']): next, value, next_series = self._get_time(entry, 'next', item, i, _caller) previous, previousvalue, previous_series = self._get_time(entry, 'previous', item, i, _caller) - _series = True if next_series + previous_series >= 3 else False - self.logger.debug(f'next series value is {next_series}, prev {previous_series}') + _series.update({i: True if next_series + previous_series >= 3 else False}) cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: @@ -767,13 +766,19 @@ def _schedule(self, item, caller=None): self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": if self._items[item]["list"][_entryindex].get("series"): - self.logger.debug(f'Series is running? {_series}') - self._items[item]["list"][_entryindex]["series"]["running"] = _series - self._update_item(item, 'UZSU Plugin', 'add_scheduler') - self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value}') + series_previous = self._items[item]["list"][_entryindex]["series"].get("running") + series_running = _series.get(_entryindex) + self._items[item]["list"][_entryindex]["series"]["running"] = series_running + if self._items[item]["list"][_entryindex].get("once") and series_running is False and series_previous is True: + self._items[item]["list"][_entryindex]["active"] = False + self._update_item(item, 'UZSU Plugin', 'series_once') + self.logger.debug(f'Deactivating uzsu entry {_entryindex} because series is finished and set to once') + self._schedule(item, caller) + self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._update_count['done'] = self._update_count.get('done', 0) + 1 + self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): @@ -829,13 +834,13 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): time = None try: if not isinstance(entry, dict): - return None, None, None + return None, None, 0 if 'value' not in entry: - return None, None, None + return None, None, 0 if 'active' not in entry: - return None, None, None + return None, None, 0 if 'time' not in entry: - return None, None, None + return None, None, 0 value = entry['value'] next = None series = 0 @@ -846,7 +851,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): weekbefore = today - timedelta(days=7) time = entry['time'] if not active: - return None, None, None + return None, None, 0 if 'rrule' in entry and 'series' not in time: if entry['rrule'] == '': entry['rrule'] = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU' @@ -875,7 +880,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): while self.alive: dt = rrule.before(dt) if timescan == 'previous' else rrule.after(dt) if dt is None: - return None, None, None + return None, None, 0 if 'sun' in time: sleep(0.01) next = self._sun(datetime.combine(dt.date(), @@ -921,7 +926,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): # Get next Time for Series next = self._series_get_time(entry, timescan) if next is None: - return None, None, None + return None, None, 0 self._itpl[item][next.timestamp() * 1000.0] = value series += 1 rstr = str(entry['rrule']).replace('\n', ';') @@ -955,7 +960,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, series except Exception as e: self.logger.error(f'{item}: Error "{time}" parsing time: {e}') - return None, None, None + return None, None, 0 def _series_calculate(self, item, caller=None, source=None): """ From 31ac401309d419a67d42b374101b7d4c250a5358 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Dec 2024 23:40:28 +0100 Subject: [PATCH 07/34] uzsu: bump version to 2.1.0 --- uzsu/__init__.py | 2 +- uzsu/plugin.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 348206544..1748a505d 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -101,7 +101,7 @@ class UZSU(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "2.0.2" # item buffer for all uzsu enabled items + PLUGIN_VERSION = "2.1.0" # item buffer for all uzsu enabled items def __init__(self, smarthome): """ diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index f03cf0f8a..251ea7d90 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -24,7 +24,7 @@ plugin: keywords: scheduler uzsu trigger series support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1364692-supportthread-für-uzsu-plugin - version: 2.0.2 # Plugin version + version: 2.1.0 # Plugin version sh_minversion: '1.6' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance From 8cdf0e685c15208bafe48dad783b62c6d8950274 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 4 Dec 2024 00:06:26 +0100 Subject: [PATCH 08/34] uzsu: fix scheduling for series once --- uzsu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 1748a505d..81085525e 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -774,6 +774,7 @@ def _schedule(self, item, caller=None): self._update_item(item, 'UZSU Plugin', 'series_once') self.logger.debug(f'Deactivating uzsu entry {_entryindex} because series is finished and set to once') self._schedule(item, caller) + return self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) @@ -802,7 +803,6 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) if self._items[item]['list'][entryindex].get('once') and not interpolated: - self._update_item(item, 'UZSU Plugin', 'once', entryindex) self.logger.debug( f'Deactivate list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') From 45bc390e4d05bba1c3e325d3f76825441c15c1b8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 4 Dec 2024 18:30:53 +0100 Subject: [PATCH 09/34] uzsu: fix once for series --- uzsu/__init__.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 81085525e..b2d6ecc07 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -679,7 +679,7 @@ def _schedule(self, item, caller=None): next = previous value = previousvalue if next is not None: - self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}') + self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {_series}') if _next is None: _next = next _value = value @@ -691,6 +691,13 @@ def _schedule(self, item, caller=None): _entryindex = i else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') + if self._items[item]["list"][i].get("series"): + series_previous = self._items[item]["list"][i]["series"].get("running") + series_running = _series.get(i) + self._items[item]["list"][i]["series"]["running"] = series_running + if self._items[item]["list"][i].get("once") and series_running is False and series_previous is True: + self._items[item]["list"][i]["active"] = False + self.logger.debug(f'Deactivating uzsu entry {i} because series is finished and set to once') elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') self._planned.update({item: None}) @@ -765,23 +772,15 @@ def _schedule(self, item, caller=None): self._items[item]['interpolation']['type'] = 'none' self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": - if self._items[item]["list"][_entryindex].get("series"): - series_previous = self._items[item]["list"][_entryindex]["series"].get("running") - series_running = _series.get(_entryindex) - self._items[item]["list"][_entryindex]["series"]["running"] = series_running - if self._items[item]["list"][_entryindex].get("once") and series_running is False and series_previous is True: - self._items[item]["list"][_entryindex]["active"] = False - self._update_item(item, 'UZSU Plugin', 'series_once') - self.logger.debug(f'Deactivating uzsu entry {_entryindex} because series is finished and set to once') - self._schedule(item, caller) - return self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, - value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated}, next=_next) + value={'item': item, 'value': _value, 'caller': 'Scheduler', + 'entryindex': _entryindex, 'interpolated': _interpolated, + 'seriesrunning': _series.get(_entryindex)}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -790,7 +789,7 @@ def _schedule(self, item, caller=None): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False): + def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False, seriesrunning=False): """ This function sets the specific item :param item: item to be updated towards the plugin @@ -802,10 +801,10 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) - if self._items[item]['list'][entryindex].get('once') and not interpolated: - self._update_item(item, 'UZSU Plugin', 'once', entryindex) + if self._items[item]['list'][entryindex].get('once') and not interpolated and not seriesrunning: self.logger.debug( - f'Deactivate list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') + f'Deactivating list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') + self._update_item(item, 'UZSU Plugin', 'once', entryindex) if self._items[item].get('once'): self.activate(False, item, 'once') self.logger.debug(f'Deactivate UZSU for item {item} as it has "once" set to True') From 022900512d0eb47718606eb54197a5a49e881e6f Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 4 Dec 2024 20:50:38 +0100 Subject: [PATCH 10/34] uzsu: update webif and docu --- uzsu/__init__.py | 1 + uzsu/user_doc.rst | 10 ++++++++-- uzsu/webif/templates/index.html | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index b2d6ecc07..5fc59d5f2 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -775,6 +775,7 @@ def _schedule(self, item, caller=None): self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) + self._webdata['items'][item.property.path].update({'seriesrunning': str(_series.get(_entryindex))}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, diff --git a/uzsu/user_doc.rst b/uzsu/user_doc.rst index 1b42ca28c..98a3b5b58 100755 --- a/uzsu/user_doc.rst +++ b/uzsu/user_doc.rst @@ -96,6 +96,7 @@ Für die universelle Zeitschaltuhr können folgende Einstellungen vorgenommen we * Wert: Der zu schaltende Wert * Zeit: Die Uhrzeit, zu der der gewünschte Wert geschaltet werden soll. Im Experten- und Serienmodus kann dieser Parameter auch detaillierter konfiguriert werden. * Aktivieren: Eintrag aktivieren oder deaktivieren. +* Einmal-Schaltung ("Once"): Sowohl auf globaler Ebene als auch pro Eintrag kann eingestellt werden, dass ein Schaltvorgang nur ein Mal ausgeführt wird. Experteneinstellungen @@ -112,6 +113,8 @@ Zeitserie Für wiederkehrende Schaltungen können auch Serien angelegt werden. Dabei ist ein Startzeitpunkt und ein Intervall zu definieren. Das Ende kann entweder über einen Zeitpunkt oder die Anzahl Wiederholungen definiert werden. Start- und Endzeitpunkte können wie bei der normalen UZSU auch sonnenstandsabhängig deklariert werden. +Wird bei einer Serie die Einmal-Funktion aktiviert, wird die Serie (erst) nach Abarbeiten aller Wiederholungen deaktiviert. + Interpolation ============= @@ -132,6 +135,7 @@ Interpolation ist ein eigenes Dict innerhalb des UZSU Dictionary mit folgenden E - **initizialized**: bool, wird beim Pluginstart automatisch gesetzt, sobald ein gültiger Eintrag innerhalb der initage Zeit gefunden wurde und diese Initialisierung tatsächlich ausgeführt wurde. +Ist die Interpolation und die Einmal-Funktion (global oder für einen Eintrag) aktiviert, so werden so viele interpolierte Schaltvorgänge durchgeführt, bis der tatsächlich hinterlegte Wert erreich ist. Erst dann wird der Eintrag oder die UZSU deaktiviert. Pluginfunktionen ================ @@ -148,7 +152,7 @@ Das Webinterface bietet folgende Informationen: - **Allgemeines**: Oben rechts werden die berechneten Sonnenauf- und Sonnenuntergänge der nächsten 7 Tage und die Anzahl der UZSU Items angezeigt. -- **UZSUs**: Liste aller UZSU Items mit farbkodierter Information über den Status (inaktiv = grau, aktiv = grün, Problem = rot) +- **UZSUs**: Liste aller UZSU Items mit farbkodierter Information über den Status (inaktiv = grau, aktiv = grün, aktive Serie = orange, Problem = rot) - **UZSU Items**: Info zu den Items, die über die UZSU geschaltet werden (inkl. Typ) @@ -193,7 +197,7 @@ Folgender Python Aufruf bzw. Dictionary Eintrag schaltet das Licht jeden zweiten Datenformat =========== -Jedes USZU Item wird als dict-Typ gespeichert. Jeder Listen-Eintrag ist wiederum ein dict, das aus Key und Value-Paaren besteht. Im Folgenden werden die möglichen Dictionary-Keys gelistet. Nutzt man das USZU Widget der SmartVISU, muss man sich um diese Einträge nicht kümmern. +Jedes USZU Item wird als dict-Typ gespeichert. Darin enthalten sind einige allgemeine Informationen und Berechnungen und das Herzstück - die Liste mit den Schaltvorgängen. Jeder Listen-Eintrag ist wiederum ein dict, das aus Key und Value-Paaren besteht. Im Folgenden werden die möglichen Dictionary-Keys gelistet. Nutzt man das USZU Widget der SmartVISU, muss man sich um diese Einträge nicht kümmern. - **dtstart**: Ein datetime Objekt, das den exakten Startwert für den rrule Algorithmus bestimmt. Dieser Parameter ist besonders bei FREQ=MINUTELY rrules relevant. @@ -201,6 +205,8 @@ Jedes USZU Item wird als dict-Typ gespeichert. Jeder Listen-Eintrag ist wiederum - **active**: ``True`` wenn die UZSU aktiviert ist, ``False`` wenn keine Aktualisierungen vorgenommen werden sollen. Dieser Wert kann über die Pluginfunktion activate gesteuert werden. +- **once**: ``True`` wenn die UZSU oder ein Eintrag nach einmaligem Schalten deaktiviert werden soll, ``False`` wenn ein Eintrag oder die UZSU öfters evaluiert werden soll. + - **time**: Zeit als String. Entweder eine direkte Zeitangabe wie ``17:00`` oder eine Kombination mit Sonnenauf- und Untergang wie bei einem crontab, z.B. ``17:008:00``, ``17:00`_ beschrieben festgelegt werden. diff --git a/uzsu/webif/templates/index.html b/uzsu/webif/templates/index.html index b3edbd801..42e6351fd 100755 --- a/uzsu/webif/templates/index.html +++ b/uzsu/webif/templates/index.html @@ -38,8 +38,10 @@ for (item in objResponse['items']) { planned = objResponse['items'][item]['planned']['value']; lastvalue = objResponse['items'][item]['lastvalue']; + seriesrunning = objResponse['items'][item]['seriesrunning']; if (objResponse['items'][item]['active'] == 'True' && planned == '-') {color = 'red';} else if (planned == '-' || objResponse['items'][item]['active'] == 'False' ){color = 'gray';} + else if (seriesrunning == 'True'){color = '#FEC34D';} else {color = 'green'} if (objResponse['items'][item]['depend']['item'] == null) { @@ -197,6 +199,8 @@ {% set color = 'red' %} {% elif p._webdata['items'][item]['planned'] == '-' or p._webdata['items'][item]['active'] == 'False' %} {% set color = 'gray' %} + {% elif p._webdata['items'][item]['seriesrunning'] == 'True' %} + {% set color = '#FEC34D' %} {% else %} {% set color = 'green' %} {% endif %} From a460a1ef851a568921e5b20adcc4b66955391266 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 7 Dec 2024 22:29:20 +0100 Subject: [PATCH 11/34] uzsu: fix series interval calculation. Actually, now end of series is the last time the series is triggered. --- uzsu/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 5fc59d5f2..1b0966750 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -930,7 +930,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self._itpl[item][next.timestamp() * 1000.0] = value series += 1 rstr = str(entry['rrule']).replace('\n', ';') - self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} with start-time {entry["series"]["timeSeriesMin"]}') + self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') cond_today = False if next is None else next.date() == today.date() cond_yesterday = False if next is None else next.date() - timedelta(days=1) == yesterday.date() @@ -1046,9 +1046,9 @@ def _series_calculate(self, item, caller=None, source=None): original_daycount = daycount if daycount is None: - daycount = int(timediff.total_seconds() / 60 / interval) + daycount = int((timediff.total_seconds() // 60) // interval + 1) else: - new_daycount = int(timediff.total_seconds() / 60 / interval) + new_daycount = int((timediff.total_seconds() // 60) // interval + 1) if int(daycount) > new_daycount: self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} x SerieCount {daycount} is not possible between {starttime} and {endtime}') daycount = new_daycount @@ -1221,13 +1221,13 @@ def _series_get_time(self, mydict, timescan=''): if endtime < starttime: endtime += timedelta(days=1) timediff = endtime - starttime - daycount = int(timediff.total_seconds() / 60 / interval) + daycount = int((timediff.total_seconds() // 60) // interval + 1) else: if seriesend is None: endtime = starttime endtime += timedelta(minutes=interval * int(daycount)) timediff = endtime - starttime - daycount = int(timediff.total_seconds() / 60 / interval) + daycount = int((timediff.total_seconds() // 60) // interval + 1) else: endtime = datetime.strptime(seriesend, "%H:%M") timediff = endtime - starttime @@ -1238,7 +1238,7 @@ def _series_get_time(self, mydict, timescan=''): count = int(1439 / interval) self.logger.warning(f'Cut your SerieCount to {count} - because interval {interval} x SerieCount {org_count} is more than 24h') else: - new_daycount = int(timediff.total_seconds() / 60 / interval) + new_daycount = int((timediff.total_seconds() // 60) // interval + 1) if int(daycount) > new_daycount: self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} x SerieCount {daycount} is not possible between {datetime.strftime(starttime, "%H:%M")} and {datetime.strftime(endtime, "%H:%M")}') daycount = new_daycount From b60eb576e668a91beb8afd51dbdaeba59377dea6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 8 Dec 2024 01:20:09 +0100 Subject: [PATCH 12/34] uzsu: implement activeToday for smartvisu, fix once for series --- uzsu/__init__.py | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 1b0966750..a2d354ac4 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -139,7 +139,8 @@ def run(self): self.alive = True self.scheduler_add('uzsu_sunupdate', self._update_all_suns, value={'caller': 'Scheduler:UZSU'}, cron=self._suncalculation_cron) - self.logger.info("Adding sun update schedule for midnight") + self.logger.info(f"Adding sun update schedule for {self._suncalculation_cron}") + self.scheduler_add('uzsu_resetactivetoday', self._reset_activetoday, cron='55 59 23 * * *') _invaliditems = [] for item in self._items: self._add_dicts(item) @@ -195,6 +196,15 @@ def stop(self): self.logger.debug(f'Scheduler for item {item.property.path} not removed. Problem: {err}') self.alive = False + def _reset_activetoday(self): + """ + Set activeToday to False for all list entries (for smartVISU) + """ + for item in self._items: + for entry in self._items[item].get('list'): + entry['activeToday'] = False + self._update_item(item, 'UZSU Plugin', 'reset_activetoday') + def _update_all_suns(self, caller=None): """ Update sun information for all uzsu items @@ -204,7 +214,7 @@ def _update_all_suns(self, caller=None): for item in self._items: success = self._update_sun(item, caller="update_all_suns") if success: - self.logger.debug(f'Updating sun info for item {item}. Caller: {caller}') + self.logger.debug(f'Updated sun info for item {item}. Caller: {caller}') self._update_item(item, 'UZSU Plugin', 'update_all_suns') def _update_sun(self, item, caller=None): @@ -571,6 +581,9 @@ def update_item(self, item, caller=None, source='', dest=None): self._update_item(item, 'UZSU Plugin', 'update') def _update_item(self, item, caller="", comment="", entryindex=None): + if comment in ['lastvalue removed', 'reset_activetoday']: + item(self._items[item], caller, comment) + return success = self._get_sun4week(item, caller="_update_item") if success: self.logger.debug(f'Updated weekly sun info for item {item} caller: {caller} comment: {comment}') @@ -672,7 +685,8 @@ def _schedule(self, item, caller=None): for i, entry in enumerate(self._items[item]['list']): next, value, next_series = self._get_time(entry, 'next', item, i, _caller) previous, previousvalue, previous_series = self._get_time(entry, 'previous', item, i, _caller) - _series.update({i: True if next_series + previous_series >= 3 else False}) + status = "running" if next_series + previous_series >= 3 else "starting" if next_series >= 1 else "-" + _series.update({i: status}) cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: @@ -692,12 +706,15 @@ def _schedule(self, item, caller=None): else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') if self._items[item]["list"][i].get("series"): - series_previous = self._items[item]["list"][i]["series"].get("running") - series_running = _series.get(i) - self._items[item]["list"][i]["series"]["running"] = series_running - if self._items[item]["list"][i].get("once") and series_running is False and series_previous is True: + series_previous = self._items[item]["list"][i]["series"].get("status") + series_status = _series.get(i) + self._items[item]["list"][i]["series"]["status"] = series_status + if self._items[item]["list"][i].get("once") and series_status != "running" and series_previous == "running": self._items[item]["list"][i]["active"] = False + _next = None self.logger.debug(f'Deactivating uzsu entry {i} because series is finished and set to once') + self._update_item(item, 'UZSU Plugin', 'once', i) + elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') self._planned.update({item: None}) @@ -775,22 +792,21 @@ def _schedule(self, item, caller=None): self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) - self._webdata['items'][item.property.path].update({'seriesrunning': str(_series.get(_entryindex))}) + self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if _series.get(_entryindex) == "running" else 'False'}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated, - 'seriesrunning': _series.get(_entryindex)}, next=_next) + 'seriesstatus': _series.get(_entryindex)}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} elif self._items[item].get('active') is True and self._items[item].get('list'): - self.logger.warning(f'item "{item}" is active but has no active entries.') self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False, seriesrunning=False): + def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False, seriesstatus="-"): """ This function sets the specific item :param item: item to be updated towards the plugin @@ -801,11 +817,17 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated """ _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') + update = None self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) - if self._items[item]['list'][entryindex].get('once') and not interpolated and not seriesrunning: + if entryindex and not self._items[item]['list'][entryindex].get('activeToday'): + self._items[item]['list'][entryindex]['activeToday'] = True + update = 'activeToday' + if entryindex and self._items[item]['list'][entryindex].get('once') and not interpolated and seriesstatus == "-": self.logger.debug( f'Deactivating list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') - self._update_item(item, 'UZSU Plugin', 'once', entryindex) + update = 'once' + if update is not None: + self._update_item(item, 'UZSU Plugin', update, entryindex) if self._items[item].get('once'): self.activate(False, item, 'once') self.logger.debug(f'Deactivate UZSU for item {item} as it has "once" set to True') From 5cafb5d5f73a70fee60ba47d034d3a7fa17e9b79 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 13 Dec 2024 10:26:38 +0100 Subject: [PATCH 13/34] uzsu: fix comparison of new and old dictionary --- uzsu/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index a2d354ac4..803713ac2 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -576,8 +576,11 @@ def update_item(self, item, caller=None, source='', dest=None): else: self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries. Source: {source}') self._schedule(item, caller='dry_run') - - if self._items[item] != self.itemsApi.return_item(str(item)) and cond: + try: + current_value = self.itemsApi.return_item(str(item)).property.value + except: + current_value = None + if cond and self._items[item] != current_value: self._update_item(item, 'UZSU Plugin', 'update') def _update_item(self, item, caller="", comment="", entryindex=None): From b2d1a34bb2dd8a264aa2abf0c7d31d19db7f8242 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 14 Dec 2024 13:36:08 +0100 Subject: [PATCH 14/34] uzsu: update handling of once for series. might work now? ;) --- uzsu/__init__.py | 69 +++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 803713ac2..acb66d334 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -122,6 +122,7 @@ def __init__(self, smarthome): self._suncalculation_cron = self.get_parameter_value('suncalculation_cron') self._sh = smarthome self._items = {} + self._series = {} self._lastvalues = {} self._planned = {} self._webdata = {'sunCalculated': {}, 'items': {}} @@ -604,6 +605,8 @@ def _update_item(self, item, caller="", comment="", entryindex=None): self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') else: self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} caller: {caller} comment: {comment}, issue: {success}') + for entry in self._items[item]['list']: + entry['activeToday'] = False item(self._items[item], caller, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) self._webdata['items'][item.property.path].update({'active': str(self._items[item].get('active'))}) @@ -672,7 +675,6 @@ def _schedule(self, item, caller=None): _next = None _value = None _entryindex = None - _series = {} self._update_sun(item, caller=_caller) self._add_dicts(item) if not self._items[item]['interpolation'].get('itemtype') or \ @@ -686,17 +688,20 @@ def _schedule(self, item, caller=None): elif self._items[item].get('active') is True or _caller == "dry_run": self._itpl[item] = OrderedDict() for i, entry in enumerate(self._items[item]['list']): - next, value, next_series = self._get_time(entry, 'next', item, i, _caller) - previous, previousvalue, previous_series = self._get_time(entry, 'previous', item, i, _caller) - status = "running" if next_series + previous_series >= 3 else "starting" if next_series >= 1 else "-" - _series.update({i: status}) + next, value, series_finished = self._get_time(entry, 'next', item, i, _caller) + previous, previousvalue, series_started = self._get_time(entry, 'previous', item, i, _caller) + cond_running = series_finished is False and series_started is True + cond_finished = series_finished is True and self._series.get(i) in ["running", "preparing"] + cond_preparing = series_finished is False and not series_started + series_status = "preparing" if cond_preparing else "running" if cond_running else "finished" if cond_finished else "-" + self._series[i] = series_status cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: next = previous value = previousvalue if next is not None: - self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {_series}') + self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {self._series}') if _next is None: _next = next _value = value @@ -709,13 +714,12 @@ def _schedule(self, item, caller=None): else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') if self._items[item]["list"][i].get("series"): - series_previous = self._items[item]["list"][i]["series"].get("status") - series_status = _series.get(i) - self._items[item]["list"][i]["series"]["status"] = series_status - if self._items[item]["list"][i].get("once") and series_status != "running" and series_previous == "running": + #series_status = self._series.get(i) + if self._items[item]["list"][i].get("once") and series_status == "finished": + self.logger.debug(f'Deactivating list entry {i} because series is finished and set to once') self._items[item]["list"][i]["active"] = False _next = None - self.logger.debug(f'Deactivating uzsu entry {i} because series is finished and set to once') + self._series[i] = "waiting" self._update_item(item, 'UZSU Plugin', 'once', i) elif not self._items[item].get('list') and self._items[item].get('active') is True: @@ -795,13 +799,13 @@ def _schedule(self, item, caller=None): self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) - self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if _series.get(_entryindex) == "running" else 'False'}) + self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series.get(_entryindex) == "running" else 'False'}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated, - 'seriesstatus': _series.get(_entryindex)}, next=_next) + 'seriesstatus': self._series.get(_entryindex)}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -829,6 +833,9 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated self.logger.debug( f'Deactivating list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') update = 'once' + if entryindex: + self._series[entryindex] = seriesstatus + self.logger.debug(f"Updated series index {entryindex} to {seriesstatus}. Series is now {self._series}") if update is not None: self._update_item(item, 'UZSU Plugin', update, entryindex) if self._items[item].get('once'): @@ -859,16 +866,15 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): time = None try: if not isinstance(entry, dict): - return None, None, 0 + return None, None, None if 'value' not in entry: - return None, None, 0 + return None, None, None if 'active' not in entry: - return None, None, 0 + return None, None, None if 'time' not in entry: - return None, None, 0 + return None, None, None value = entry['value'] next = None - series = 0 active = True if caller == "dry_run" else entry['active'] today = datetime.today() tomorrow = today + timedelta(days=1) @@ -876,7 +882,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): weekbefore = today - timedelta(days=7) time = entry['time'] if not active: - return None, None, 0 + return None, None, None if 'rrule' in entry and 'series' not in time: if entry['rrule'] == '': entry['rrule'] = 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU' @@ -905,7 +911,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): while self.alive: dt = rrule.before(dt) if timescan == 'previous' else rrule.after(dt) if dt is None: - return None, None, 0 + return None, None, None if 'sun' in time: sleep(0.01) next = self._sun(datetime.combine(dt.date(), @@ -921,12 +927,11 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self._itpl[item][next.timestamp() * 1000.0] = value if next - timedelta(seconds=1) > datetime.now().replace(tzinfo=self._timezone): self.logger.debug(f'{item}: Return from rrule {timescan}: {next}, value {value}.') - return next, value, series + return next, value, None else: self.logger.debug(f"{item}: Not returning {timescan} rrule {next} because it's in the past.") if 'sun' in time and 'series' not in time: - next = self._sun(datetime.combine(today, datetime.min.time()).replace( - tzinfo=self._timezone), time, timescan) + next = self._sun(datetime.combine(today, datetime.min.time()).replace(tzinfo=self._timezone), time, timescan) cond_future = next > datetime.now(self._timezone) if cond_future: self.logger.debug(f'{item}: Result parsing time today (sun) {time}: {next}') @@ -951,9 +956,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): # Get next Time for Series next = self._series_get_time(entry, timescan) if next is None: - return None, None, 0 + return None, None, False self._itpl[item][next.timestamp() * 1000.0] = value - series += 1 rstr = str(entry['rrule']).replace('\n', ';') self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') @@ -966,26 +970,25 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if next and cond_today and cond_next: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next today: {next}, value {value}') - series += 1 - return next, value, series + return next, value, False if 'series' in time else None if next and cond_tomorrow and cond_next: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next tomorrow: {next}, value {value}') - return next, value, series - if 'series' in time and next and cond_next: + return next, value, True if 'series' in time else None + if 'series' in time and cond_next: self.logger.debug(f'{item}: Return next for series: {next}, value {value}') - return next, value, series + return next, value, True if next and cond_today and cond_previous_today: self._itpl[item][(next - timedelta(seconds=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous today {next} because it‘s in the past.') - return None, None, series + return None, None, True if next and cond_yesterday and cond_previous_yesterday: self._itpl[item][(next - timedelta(days=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous yesterday {next} because it‘s in the past.') - return None, None, series + return None, None, False except Exception as e: self.logger.error(f'{item}: Error "{time}" parsing time: {e}') - return None, None, 0 + return None, None, None def _series_calculate(self, item, caller=None, source=None): """ From a2d9dccf2a0b07f3f7001b574ed3277d2e209b0e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 16 Dec 2024 14:20:18 +0100 Subject: [PATCH 15/34] uzsu: fix once handling .. again --- uzsu/__init__.py | 49 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index acb66d334..d2660f1a8 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -150,6 +150,7 @@ def run(self): _invaliditems.append(item) continue self._items[item]['interpolation']['itemtype'] = itemtype + self._series.update({item: {}}) self._lastvalues[item] = None self._webdata['items'][item.property.path].update({'lastvalue': '-'}) self._update_item(item, 'UZSU Plugin', 'run') @@ -584,7 +585,7 @@ def update_item(self, item, caller=None, source='', dest=None): if cond and self._items[item] != current_value: self._update_item(item, 'UZSU Plugin', 'update') - def _update_item(self, item, caller="", comment="", entryindex=None): + def _update_item(self, item, caller="", comment=""): if comment in ['lastvalue removed', 'reset_activetoday']: item(self._items[item], caller, comment) return @@ -598,15 +599,13 @@ def _update_item(self, item, caller="", comment="", entryindex=None): self.logger.debug(f'Updated seriesCalculated for item {item} caller: {caller} comment: {comment}') else: self.logger.debug(f'Issues with updating seriesCalculated for item {item} caller: {caller} comment: {comment}, issue: {success}') - if entryindex: - self._items[item]['list'][entryindex]['active'] = False success = self._update_sun(item, caller="_update_item") if success is True: self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') else: self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} caller: {caller} comment: {comment}, issue: {success}') - for entry in self._items[item]['list']: - entry['activeToday'] = False + #for entry in self._items[item]['list']: + # entry['activeToday'] = False item(self._items[item], caller, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) self._webdata['items'][item.property.path].update({'active': str(self._items[item].get('active'))}) @@ -681,7 +680,10 @@ def _schedule(self, item, caller=None): self._items[item]['interpolation']['itemtype'] == 'none': self._items[item]['interpolation']['itemtype'] = self._get_type(item) if self._items[item].get('interpolation') is None: - self.logger.error("Something is wrong with your UZSU item. You most likely use a wrong smartVISU widget version! Use the latest device.uzsu SV 2.9. or higher If you write your uzsu dict directly please use the format given in the documentation: https://www.smarthomeng.de/user/plugins/uzsu/user_doc.html and include the interpolation array correctly!") + self.logger.error(f"Something is wrong with your UZSU item {item}. You most likely use a wrong smartVISU widget " + "version! Use the latest device.uzsu SV 2.9. or higher If you write your uzsu dict directly " + "please use the format given in the documentation: " + "https://www.smarthomeng.de/user/plugins/uzsu/user_doc.html and include the interpolation array correctly!") return elif not self._items[item]['interpolation'].get('itemtype'): self.logger.error(f'item "{self.get_iattr_value(item.conf, ITEM_TAG[0])}" to be set by uzsu does not exist.') @@ -691,17 +693,18 @@ def _schedule(self, item, caller=None): next, value, series_finished = self._get_time(entry, 'next', item, i, _caller) previous, previousvalue, series_started = self._get_time(entry, 'previous', item, i, _caller) cond_running = series_finished is False and series_started is True - cond_finished = series_finished is True and self._series.get(i) in ["running", "preparing"] + cond_finished = series_finished is True and self._series[item].get(i) in ["running", "preparing"] cond_preparing = series_finished is False and not series_started series_status = "preparing" if cond_preparing else "running" if cond_running else "finished" if cond_finished else "-" - self._series[i] = series_status + self._series[item][i] = series_status + self.logger.debug(f"{item}, i {i} series_finished {series_finished} series_started {series_started} cond_running {cond_running} series_status {series_status}") cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: next = previous value = previousvalue if next is not None: - self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {self._series}') + self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {self._series[item]}') if _next is None: _next = next _value = value @@ -716,11 +719,11 @@ def _schedule(self, item, caller=None): if self._items[item]["list"][i].get("series"): #series_status = self._series.get(i) if self._items[item]["list"][i].get("once") and series_status == "finished": - self.logger.debug(f'Deactivating list entry {i} because series is finished and set to once') + self.logger.debug(f'Deactivating list entry {i} for item {item} because series is finished and set to once') self._items[item]["list"][i]["active"] = False _next = None - self._series[i] = "waiting" - self._update_item(item, 'UZSU Plugin', 'once', i) + self._series[item][i] = "waiting" + self._update_item(item, 'UZSU Plugin', 'once') elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') @@ -799,13 +802,13 @@ def _schedule(self, item, caller=None): self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) - self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series.get(_entryindex) == "running" else 'False'}) + self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) self._update_count['done'] = self._update_count.get('done', 0) + 1 self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated, - 'seriesstatus': self._series.get(_entryindex)}, next=_next) + 'seriesstatus': self._series[item].get(_entryindex)}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') self._update_count = {'done': 0, 'todo': 0} @@ -825,22 +828,24 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') update = None + self.logger.debug(f'Setting {item} entryindex: {entryindex}, seriesstatus {seriesstatus}, interpolation {interpolated}') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) - if entryindex and not self._items[item]['list'][entryindex].get('activeToday'): + if entryindex is not None and not self._items[item]['list'][entryindex].get('activeToday'): self._items[item]['list'][entryindex]['activeToday'] = True update = 'activeToday' - if entryindex and self._items[item]['list'][entryindex].get('once') and not interpolated and seriesstatus == "-": + if entryindex is not None and self._items[item]['list'][entryindex].get('once') and not interpolated and seriesstatus == "-": self.logger.debug( f'Deactivating list entry {self._items[item]["list"][entryindex]} of item {item} as it has "once" set to True') + self._items[item]['list'][entryindex]['active'] = False update = 'once' - if entryindex: - self._series[entryindex] = seriesstatus - self.logger.debug(f"Updated series index {entryindex} to {seriesstatus}. Series is now {self._series}") + if entryindex is not None and item: + self._series[item][entryindex] = seriesstatus + self.logger.debug(f"Updated series index {entryindex} for item {item} to {seriesstatus}. Series is now {self._series[item]}") if update is not None: - self._update_item(item, 'UZSU Plugin', update, entryindex) + self._update_item(item, 'UZSU Plugin', update) if self._items[item].get('once'): self.activate(False, item, 'once') - self.logger.debug(f'Deactivate UZSU for item {item} as it has "once" set to True') + self.logger.debug(f'Deactivating UZSU for item {item} as it has "once" set to True') if not caller or caller == "Scheduler": self._schedule(item, caller='set') @@ -940,7 +945,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): else: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Include previous today (sun): {next}, value {value} for interpolation.') - if entryindex: + if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) next = self._sun(datetime.combine(tomorrow, datetime.min.time()).replace( tzinfo=self._timezone), time, timescan) From c820c20134cf24e4c4a33a64ea78c8f08225f7fe Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Dec 2024 00:33:56 +0100 Subject: [PATCH 16/34] uzsu: adjust struct --- uzsu/plugin.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index 251ea7d90..ec0b7d3e7 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -138,9 +138,7 @@ item_structs: status: type: bool eval: sh....activate() - eval_trigger: - - .. - - ... + eval_trigger: ... on_change: .. = value crontab: init = 0 next: From c4adb7eec3240b4e040bf0b39aeff09d0236e914 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Dec 2024 00:38:07 +0100 Subject: [PATCH 17/34] uzsu: introduce "perday" interpolation feature --- uzsu/__init__.py | 60 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index d2660f1a8..c7c5458a3 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -342,7 +342,7 @@ def activate(self, activevalue=None, item=None, caller='logic'): if activevalue is None: return self._items[item].get('active') - def interpolation(self, intpl_type=None, interval=5, backintime=0, item=None): + def interpolation(self, intpl_type=None, interval=5, backintime=0, perday=False, item=None): if self._items.get(item) is None: try: self.logger.warning(f'Item {item.property.path} is no valid UZSU item!') @@ -361,7 +361,8 @@ def interpolation(self, intpl_type=None, interval=5, backintime=0, item=None): self._items[item]['interpolation']['type'] = str(intpl_type).lower() self._items[item]['interpolation']['interval'] = abs(int(interval)) self._items[item]['interpolation']['initage'] = abs(int(backintime)) - self.logger.info(f'Item {item} interpolation is set via logic to: type={intpl_type}, interval={abs(interval)}, backintime={backintime}') + self._items[item]['interpolation']['perday'] = bool(perday) + self.logger.info(f'Item {item} interpolation is set via logic to: type={intpl_type}, interval={abs(interval)}, backintime={backintime}, perday={perday}') self._update_item(item, 'UZSU Plugin', 'logic') return self._items[item].get('interpolation') @@ -425,7 +426,7 @@ def planned(self, item=None): self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) return None else: - self.logger.info(f'Nothing planned for item "{item}".') + self.logger.info(f'Nothing planned for item "{item}": {self._planned.get(item)}.') return None def _add_dicts(self, item): @@ -438,6 +439,7 @@ def _add_dicts(self, item): self._items[item]['interpolation'] = { 'type': 'none', 'initialized': False, + 'perday': False, 'interval': self._interpolation_interval, 'initage': self._backintime } @@ -450,6 +452,8 @@ def _add_dicts(self, item): self._items[item]['interpolation']['interval'] = self._interpolation_interval if 'initage' not in self._items[item]['interpolation']: self._items[item]['interpolation']['initage'] = self._backintime + if 'perday' not in self._items[item]['interpolation']: + self._items[item]['interpolation']['perday'] = False self._items[item]['plugin_version'] = self.PLUGIN_VERSION if 'list' not in self._items[item]: @@ -631,7 +635,8 @@ def _interpolate(self, data: dict, time: float, linear=True, use_precision=True) # use <= to get last data value for series of identical timestamps if ts >= time and (ts <= ts_next or ts_next == -1): ts_next = ts - + if ts_next <= 0 or ts_last <= 0 or len(data.keys()) <= 1: + return None if time == ts_next: value = float(data[ts_next]) elif time == ts_last: @@ -674,8 +679,10 @@ def _schedule(self, item, caller=None): _next = None _value = None _entryindex = None + update = None self._update_sun(item, caller=_caller) self._add_dicts(item) + cond_activetoday = self._items[item].get('activeToday') is True and self._items[item]['interpolation'].get('perday') if not self._items[item]['interpolation'].get('itemtype') or \ self._items[item]['interpolation']['itemtype'] == 'none': self._items[item]['interpolation']['itemtype'] = self._get_type(item) @@ -687,7 +694,7 @@ def _schedule(self, item, caller=None): return elif not self._items[item]['interpolation'].get('itemtype'): self.logger.error(f'item "{self.get_iattr_value(item.conf, ITEM_TAG[0])}" to be set by uzsu does not exist.') - elif self._items[item].get('active') is True or _caller == "dry_run": + elif self._items[item].get('active') is True or cond_activetoday or _caller == "dry_run": self._itpl[item] = OrderedDict() for i, entry in enumerate(self._items[item]['list']): next, value, series_finished = self._get_time(entry, 'next', item, i, _caller) @@ -729,7 +736,7 @@ def _schedule(self, item, caller=None): self.logger.warning(f'item "{item}" is active but has no entries.') self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - if _next and _value is not None and (self._items[item].get('active') is True or _caller == "dry_run"): + if _next and _value is not None and (cond_activetoday or self._items[item].get('active') is True or _caller == "dry_run"): _reset_interpolation = False _interpolated = False _interval = self._items[item]['interpolation'].get('interval') @@ -752,7 +759,10 @@ def _schedule(self, item, caller=None): _initvalue = itpl_list[entry_index - min(1, entry_index)][1] itpl_list = itpl_list[entry_index - min(2, entry_index):entry_index + min(3, len(itpl_list))] itpl_list.remove((entry_now, 'NOW')) - if _caller != "dry_run": + if _initvalue == 'NOW': + self._webdata['items'][item.property.path].update({'lastvalue': '-'}) + self._lastvalues[item] = None + elif _caller != "dry_run": self._lastvalues[item] = _initvalue self._webdata['items'][item.property.path].update({'lastvalue': _initvalue}) _timediff = datetime.now(self._timezone) - timedelta(minutes=_initage) @@ -781,16 +791,24 @@ def _schedule(self, item, caller=None): elif cond2 and _itemtype not in ['num']: self.logger.warning(f'Interpolation is set to {item} but type of item {_interpolation} is {_itemtype}. Ignoring interpolation and setting UZSU interpolation to none.') _reset_interpolation = True - elif _interpolation.lower() in ('cubic', 'linear') and _interval > 0: + elif _interpolation.lower() in ('cubic', 'linear') and _interval > 0 and self._itpl[item]: try: + _oldnext = _next _nextinterpolation = datetime.now(self._timezone) + timedelta(minutes=_interval) _interpolated = True if _next > _nextinterpolation else False _next = _nextinterpolation if _next > _nextinterpolation else _next + _oldvalue = _value _value = self._interpolate(self._itpl[item], _next.timestamp() * 1000.0, _interpolation.lower() == 'linear') _value_now = self._interpolate(self._itpl[item], entry_now, _interpolation.lower() == 'linear') - if _caller != "dry_run": - self._set(item=item, value=_value_now, caller=_caller) - self.logger.info(f'Updated: {item}, {_interpolation.lower()} interpolation value: {_value_now}, based on dict: {self._itpl[item]}. Next: {_next}, value: {_value}') + if _caller != "dry_run" and _interpolated and _value: + self._set(item=item, value=_value_now, caller=_caller, interpolated=_interpolated) + self.logger.info(f'Updated: {item}, {_interpolation.lower()} interpolation value: {_value_now}, based on dict: {self._itpl[item]}. Next: {_next}, value: {_value}') + if _value is None: + _value = _oldvalue + _next = _oldnext + _interpolated = False + self.logger.info(f'Not interpolating: {item}. Next: {_next}, value: {_value}') + except Exception as e: self.logger.error(f'Error {_interpolation.lower()} interpolation for item {item} with interpolation list {self._itpl[item]}: {e}') if cond5 and _value < 0: @@ -828,7 +846,7 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated _uzsuitem, _itemvalue = self._get_dependant(item) _uzsuitem(value, 'UZSU Plugin', 'set') update = None - self.logger.debug(f'Setting {item} entryindex: {entryindex}, seriesstatus {seriesstatus}, interpolation {interpolated}') + self.logger.debug(f'Setting {item} entryindex: {entryindex}, seriesstatus {seriesstatus}, interpolation {interpolated} caller {caller}') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) if entryindex is not None and not self._items[item]['list'][entryindex].get('activeToday'): self._items[item]['list'][entryindex]['activeToday'] = True @@ -880,7 +898,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, None value = entry['value'] next = None - active = True if caller == "dry_run" else entry['active'] + cond_activetoday = entry.get('activeToday') is True and self._items[item]['interpolation'].get('perday') + active = True if caller == "dry_run" or cond_activetoday else entry.get('active') today = datetime.today() tomorrow = today + timedelta(days=1) yesterday = today - timedelta(days=1) @@ -919,8 +938,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, None if 'sun' in time: sleep(0.01) - next = self._sun(datetime.combine(dt.date(), - datetime.min.time()).replace(tzinfo=self._timezone), + next = self._sun(datetime.combine(dt.date(),datetime.min.time()).replace(tzinfo=self._timezone), time, timescan) self.logger.debug(f'{item}: Result parsing time (rrule) {time}: {next}') if entryindex is not None and timescan == 'next': @@ -929,7 +947,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): next = datetime.combine(dt.date(), parser.parse(time.strip()).time()).replace(tzinfo=self._timezone) self._update_suncalc(item, entry, entryindex, None) if next and next.date() == dt.date(): - self._itpl[item][next.timestamp() * 1000.0] = value + if not self._items[item]['interpolation'].get('perday') or next.date() == datetime.now().date(): + self._itpl[item][next.timestamp() * 1000.0] = value if next - timedelta(seconds=1) > datetime.now().replace(tzinfo=self._timezone): self.logger.debug(f'{item}: Return from rrule {timescan}: {next}, value {value}.') return next, value, None @@ -943,7 +962,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) else: - self._itpl[item][next.timestamp() * 1000.0] = value + if not self._items[item]['interpolation'].get('perday'): + self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Include previous today (sun): {next}, value {value} for interpolation.') if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) @@ -977,7 +997,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self.logger.debug(f'{item}: Return next today: {next}, value {value}') return next, value, False if 'series' in time else None if next and cond_tomorrow and cond_next: - self._itpl[item][next.timestamp() * 1000.0] = value + if not self._items[item]['interpolation'].get('perday'): + self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next tomorrow: {next}, value {value}') return next, value, True if 'series' in time else None if 'series' in time and cond_next: @@ -988,7 +1009,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self.logger.debug(f'{item}: Not returning previous today {next} because it‘s in the past.') return None, None, True if next and cond_yesterday and cond_previous_yesterday: - self._itpl[item][(next - timedelta(days=1)).timestamp() * 1000.0] = value + if not self._items[item]['interpolation'].get('perday'): + self._itpl[item][(next - timedelta(days=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous yesterday {next} because it‘s in the past.') return None, None, False except Exception as e: From 119f93502e6ed5b27ccef01a89fd6db4d87b4c14 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Dec 2024 00:38:46 +0100 Subject: [PATCH 18/34] uzsu: minimize/optimize dict writing to item --- uzsu/__init__.py | 51 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index c7c5458a3..1ac214b7f 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -143,6 +143,7 @@ def run(self): self.logger.info(f"Adding sun update schedule for {self._suncalculation_cron}") self.scheduler_add('uzsu_resetactivetoday', self._reset_activetoday, cron='55 59 23 * * *') _invaliditems = [] + update = '' for item in self._items: self._add_dicts(item) itemtype = self._get_type(item) @@ -154,6 +155,7 @@ def run(self): self._lastvalues[item] = None self._webdata['items'][item.property.path].update({'lastvalue': '-'}) self._update_item(item, 'UZSU Plugin', 'run') + update = 'run' cond1 = self._items[item].get('active') and self._items[item]['active'] cond2 = self._items[item].get('list') if cond1 and cond2: @@ -168,7 +170,7 @@ def run(self): # remove lastvalue dict entry, it is not used anymore try: self._items[item].pop('lastvalue') - self._update_item(item, 'UZSU Plugin', 'lastvalue removed') + update = 'lastvalue removed' self.logger.debug(f'Item "{item}": removed lastvalue dict entry as it is deprecated.') except Exception: pass @@ -183,6 +185,7 @@ def run(self): self.logger.debug(f'Not scheduling item {item}, cond1 {cond1}, cond2 {cond2}') self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries') self._schedule(item, caller='dry_run') + self._write_dict_to_item(item, 'UZSU Plugin', update) def stop(self): """ @@ -205,7 +208,7 @@ def _reset_activetoday(self): for item in self._items: for entry in self._items[item].get('list'): entry['activeToday'] = False - self._update_item(item, 'UZSU Plugin', 'reset_activetoday') + self._write_dict_to_item(item, 'UZSU Plugin', 'reset_activetoday') def _update_all_suns(self, caller=None): """ @@ -216,8 +219,9 @@ def _update_all_suns(self, caller=None): for item in self._items: success = self._update_sun(item, caller="update_all_suns") if success: - self.logger.debug(f'Updated sun info for item {item}. Caller: {caller}') self._update_item(item, 'UZSU Plugin', 'update_all_suns') + self.logger.debug(f'Updated sun info for item {item}. Caller: {caller}') + self._write_dict_to_item(item, 'UZSU Plugin', 'update_all_suns') def _update_sun(self, item, caller=None): """ @@ -259,10 +263,12 @@ def _update_suncalc(self, item, entry, entryindex, entryvalue): self.logger.debug(f'No sunset/rise in time for current entry {entry}. Removing calculated value.') self._items[item]['list'][entryindex].pop('calculated') self._update_item(item, 'UZSU Plugin', 'update_sun') + self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') elif update is True and not entry.get('calculated') == entryvalue: self.logger.debug(f'Updated calculated time for item {item} entry {self._items[item]["list"][entryindex]} with value {entryvalue}.') self._items[item]['list'][entryindex]['calculated'] = entryvalue self._update_item(item, 'UZSU Plugin', 'update_sun') + self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') elif entry.get('calculated'): self.logger.debug(f'Sun calculation {entryvalue} entry not updated for item {item} with value {entry["calculated"]}') @@ -338,6 +344,7 @@ def activate(self, activevalue=None, item=None, caller='logic'): _activeinfo = "deactivated" if activevalue is False else "activated" self.logger.info(f'Item {item} is set via {caller} to: {_activeinfo}') self._update_item(item, 'UZSU Plugin', caller) + self._write_dict_to_item(item, 'UZSU Plugin', caller) return activevalue if activevalue is None: return self._items[item].get('active') @@ -364,6 +371,7 @@ def interpolation(self, intpl_type=None, interval=5, backintime=0, perday=False, self._items[item]['interpolation']['perday'] = bool(perday) self.logger.info(f'Item {item} interpolation is set via logic to: type={intpl_type}, interval={abs(interval)}, backintime={backintime}, perday={perday}') self._update_item(item, 'UZSU Plugin', 'logic') + self._write_dict_to_item(item, 'UZSU Plugin', 'logic') return self._items[item].get('interpolation') def clear(self, clear=False, item=None): @@ -383,6 +391,7 @@ def clear(self, clear=False, item=None): self._items[item] = {'interpolation': {}, 'active': False} self.logger.info(f'UZSU settings for item "{item}" are cleared') self._update_item(item, 'UZSU Plugin', 'clear') + self._write_dict_to_item(item, 'UZSU Plugin', 'clear') return True else: return False @@ -498,6 +507,7 @@ def parse_item(self, item): self._webdata['items'].update({item.property.path: {}}) self._update_item(item, 'UZSU Plugin', 'init') + self._write_dict_to_item(item, 'UZSU Plugin', 'init') self._planned.update({item: 'notinit'}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) @@ -545,6 +555,7 @@ def _check_rruleandplanned(self, item): if count > 0: self.logger.debug(f'Updated {count} rrule entries for item: {item}') self._update_item(item, 'UZSU Plugin', 'create_rrule') + self._write_dict_to_item(item, 'UZSU Plugin', 'create_rrule') if _inactive >= len(self._items[item]['list']): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) @@ -588,11 +599,19 @@ def update_item(self, item, caller=None, source='', dest=None): current_value = None if cond and self._items[item] != current_value: self._update_item(item, 'UZSU Plugin', 'update') + self._write_dict_to_item(item, 'UZSU Plugin', 'update') - def _update_item(self, item, caller="", comment=""): - if comment in ['lastvalue removed', 'reset_activetoday']: + def _write_dict_to_item(self, item, caller="", comment=""): + try: + current_value = item.property.value + #current_value = self.itemsApi.return_item(str(item)).property.value + except: + current_value = None + if self._items[item] != current_value: + self.logger.debug(f"Writing dict to item {item} due to caller {caller}, comment {comment}") item(self._items[item], caller, comment) - return + + def _update_item(self, item, caller="", comment=""): success = self._get_sun4week(item, caller="_update_item") if success: self.logger.debug(f'Updated weekly sun info for item {item} caller: {caller} comment: {comment}') @@ -608,12 +627,9 @@ def _update_item(self, item, caller="", comment=""): self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') else: self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} caller: {caller} comment: {comment}, issue: {success}') - #for entry in self._items[item]['list']: - # entry['activeToday'] = False - item(self._items[item], caller, comment) + #item(self._items[item], caller, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) self._webdata['items'][item.property.path].update({'active': str(self._items[item].get('active'))}) - # self._webdata['items'][item.property.path].update({'sun': self._items[item].get('SunCalculated')}) _suncalc = self._items[item].get('SunCalculated') self._webdata['items'][item.property.path].update({'sun': _suncalc}) self._webdata['sunCalculated'] = _suncalc @@ -730,6 +746,7 @@ def _schedule(self, item, caller=None): self._items[item]["list"][i]["active"] = False _next = None self._series[item][i] = "waiting" + update = 'once' self._update_item(item, 'UZSU Plugin', 'once') elif not self._items[item].get('list') and self._items[item].get('active') is True: @@ -744,6 +761,7 @@ def _schedule(self, item, caller=None): if _interval < 0: _interval = abs(int(_interval)) self._items[item]['interpolation']['interval'] = _interval + update = 'intervalchange' self._update_item(item, 'UZSU Plugin', 'intervalchange') _interpolation = self._items[item]['interpolation'].get('type') _interpolation = self._interpolation_type if not _interpolation else _interpolation @@ -781,6 +799,7 @@ def _schedule(self, item, caller=None): if not cond2 and cond3 and cond4: self.logger.info(f'Looking if there was a value set after {_timediff} for item {item}') self._items[item]['interpolation']['initialized'] = True + update = 'init' self._update_item(item, 'UZSU Plugin', 'init') if cond1 and not cond2 and cond3 and cond6: self._set(item=item, value=_initvalue, caller=_caller) @@ -815,6 +834,7 @@ def _schedule(self, item, caller=None): self.logger.warning(f'value {_value} for item "{item}" is negative. This might be due to not enough values set in the UZSU.') if _reset_interpolation is True: self._items[item]['interpolation']['type'] = 'none' + update = 'init' if update == 'init' else 'reset_interpolation' self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') @@ -822,6 +842,7 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) self._update_count['done'] = self._update_count.get('done', 0) + 1 + update = 'init' if update == 'init' else 'add_scheduler' self._update_item(item, 'UZSU Plugin', 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', @@ -833,6 +854,8 @@ def _schedule(self, item, caller=None): elif self._items[item].get('active') is True and self._items[item].get('list'): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + if update is not None: + self._write_dict_to_item(item, caller, update) def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False, seriesstatus="-"): """ @@ -859,11 +882,13 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated if entryindex is not None and item: self._series[item][entryindex] = seriesstatus self.logger.debug(f"Updated series index {entryindex} for item {item} to {seriesstatus}. Series is now {self._series[item]}") + if self._items[item].get('once') and not interpolated and seriesstatus == "-": + self._items[item]['active'] = False + update = 'globalonce' + self.logger.debug(f'Deactivating UZSU for item {item} as it has "once" set to True') if update is not None: self._update_item(item, 'UZSU Plugin', update) - if self._items[item].get('once'): - self.activate(False, item, 'once') - self.logger.debug(f'Deactivating UZSU for item {item} as it has "once" set to True') + self._write_dict_to_item(item, 'UZSU Plugin', update) if not caller or caller == "Scheduler": self._schedule(item, caller='set') From 64eeaddf2a4d8106175379245c1740ef7b3bc37d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Dec 2024 13:58:25 +0100 Subject: [PATCH 19/34] uzsu: improve web interface --- uzsu/__init__.py | 8 ++-- uzsu/webif/templates/index.html | 66 ++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 1ac214b7f..2686f00b7 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -629,6 +629,8 @@ def _update_item(self, item, caller="", comment=""): self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} caller: {caller} comment: {comment}, issue: {success}') #item(self._items[item], caller, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) + if self._webdata['items'][item.property.path].get('interpolationrunning') is None: + self._webdata['items'][item.property.path].update({'interpolationrunning': 'False'}) self._webdata['items'][item.property.path].update({'active': str(self._items[item].get('active'))}) _suncalc = self._items[item].get('SunCalculated') self._webdata['items'][item.property.path].update({'sun': _suncalc}) @@ -838,9 +840,10 @@ def _schedule(self, item, caller=None): self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') - self._planned.update({item: {'value': _value, 'next': _next.strftime('%Y-%m-%d %H:%M:%S')}}) - self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M')}}) + self._planned.update({item: {'value': _value, 'next': _next.strftime('%d.%m.%Y %H:%M:%S')}}) + self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) + self._webdata['items'][item.property.path].update({'interpolationrunning': str(_interpolated)}) self._update_count['done'] = self._update_count.get('done', 0) + 1 update = 'init' if update == 'init' else 'add_scheduler' self._update_item(item, 'UZSU Plugin', 'add_scheduler') @@ -1495,7 +1498,6 @@ def get_itemdict(self, item): :item: uzsu item :return: sanitized dict from uzsu item """ - return html.escape(json.dumps(self._items[item])) def get_items(self): diff --git a/uzsu/webif/templates/index.html b/uzsu/webif/templates/index.html index 42e6351fd..f202066e3 100755 --- a/uzsu/webif/templates/index.html +++ b/uzsu/webif/templates/index.html @@ -39,30 +39,32 @@ planned = objResponse['items'][item]['planned']['value']; lastvalue = objResponse['items'][item]['lastvalue']; seriesrunning = objResponse['items'][item]['seriesrunning']; - if (objResponse['items'][item]['active'] == 'True' && planned == '-') {color = 'red';} - else if (planned == '-' || objResponse['items'][item]['active'] == 'False' ){color = 'gray';} - else if (seriesrunning == 'True'){color = '#FEC34D';} - else {color = 'green'} - + if (objResponse['items'][item]['active'] === 'True' && planned === '-') {color = 'red';} + else if (planned === '-' || objResponse['items'][item]['active'] === 'False' ){color = 'gray';} + else if (seriesrunning === 'True'){color = '#FEC34D';} + else {color = 'green';} + {intpl_color = color;} if (objResponse['items'][item]['depend']['item'] == null) { depend_text = '{{ _('Item existiert nicht!') }}'; color = 'red'; + intpl_color = 'red'; } else { depend_text = objResponse['items'][item]['depend']['item'] + ' (' + objResponse['items'][item]['interpolation']['itemtype'] + ')'; } - if (objResponse['items'][item]['interpolation'] == undefined) { + if (objResponse['items'][item]['interpolation'] === undefined) { intpl_text = "{{ _('fehlt!') }}"; bit_text = "{{ _('fehlt!') }}"; color = 'red'; + intpl_color = 'red'; } else { - if (objResponse['items'][item]['interpolation']['itemtype'] == 'bool') { - if (planned == 1) {planned = 'True';} - else if (planned == 0) {planned = 'False';} - if (lastvalue == 1) {lastvalue = 'True';} - else if (lastvalue == 0) {lastvalue = 'False';} + if (objResponse['items'][item]['interpolation']['itemtype'] === 'bool') { + if (planned === 1) {planned = 'True';} + else if (planned === 0) {planned = 'False';} + if (lastvalue === 1) {lastvalue = 'True';} + else if (lastvalue === 0) {lastvalue = 'False';} } if (objResponse['items'][item]['interpolation']['initage'] === '') { bit_text = '0'; @@ -70,8 +72,10 @@ else { bit_text = objResponse['items'][item]['interpolation']['initage']; } - if (objResponse['items'][item]['interpolation']['interval'] == '' || objResponse['items'][item]['interpolation']['interval'] == undefined) { - if (objResponse['items'][item]['interpolation']['type'] == 'none') { + if (objResponse['items'][item]['interpolationrunning'] !== 'True') + intpl_color = 'gray'; + if (objResponse['items'][item]['interpolation']['interval'] === '' || objResponse['items'][item]['interpolation']['interval'] === undefined) { + if (objResponse['items'][item]['interpolation']['type'] === 'none') { interval = ''; } else { @@ -79,15 +83,21 @@ } } else { - if (objResponse['items'][item]['interpolation']['type'] == 'none') { + if (objResponse['items'][item]['interpolation']['type'] === 'none') { interval = ''; } else { - interval = ' (' + objResponse['items'][item]['interpolation']['interval'] + ')'; + if (objResponse['items'][item]['interpolation']['perday'] === true) { + perday = ', per day'; + } + else { + perday = ''; + } + interval = ' (' + objResponse['items'][item]['interpolation']['interval'] + perday + ')'; } } intpl_text = objResponse['items'][item]['interpolation']['type'] + interval; - if (objResponse['items'][item]['sun'] == undefined) { + if (objResponse['items'][item]['sun'] === undefined) { sunrise_text = ''; sunset_text = ''; } @@ -112,15 +122,16 @@ } } $('#' + $.escapeSelector(item)).css({'color': color}); + $('#' + $.escapeSelector(item + '_interpolation')).css({'color': intpl_color}); shngInsertText (item+'_interpolation', intpl_text, 'maintable', 10); shngInsertText (item+'_backintime', bit_text, 'maintable', 10); shngInsertText (item+'_dict', objResponse['items'][item]['dict'], 'maintable'); shngInsertText (item+'_dependvalue', objResponse['items'][item]['depend']['value'], 'maintable', 10); shngInsertText (item+'_nextvalue', planned, 'maintable', 10); - if (objResponse['items'][item]['planned']['time'] == '-') + if (objResponse['items'][item]['planned']['time'] === '-') shngInsertText (item+'_nexttime', '-', 'maintable', 10); else - shngInsertText (item+'_nexttime', '‪'+objResponse['items'][item]['planned']['time']+'‬', 'maintable', 10); + shngInsertText (item+'_nexttime', '‪'+objResponse['items'][item]['planned']['time']+'‬', 'maintable', 10); shngInsertText (item+'_dependitem', depend_text, 'maintable'); shngInsertText (item+'_lastvalue', lastvalue, 'maintable', 10); shngInsertText ('sunrise', sunrise_text, null, 2); @@ -204,6 +215,7 @@ {% else %} {% set color = 'green' %} {% endif %} + {% set intpl_color = color %} {{ item }} @@ -221,25 +233,35 @@ {% endif %} - {{ p._webdata['items'][item]['planned']['value'] }} + {{ p._webdata['items'][item]['planned']['value'] | replace('.0', '') }} {% if p._webdata['items'][item]['planned']['time'] == '-' %} - {% else %} - ‪{{ p._webdata['items'][item]['planned']['time'] }}‬ + ‪{{ p._webdata['items'][item]['planned']['time'] }}‬ {% endif %} {{ p._webdata['items'][item]['lastvalue'] }} - + {% if p._webdata['items'][item]['interpolation'] is not defined %} + {% set intpl_color = 'red' %} + {% elif p._webdata['items'][item]['interpolationrunning'] != 'True' %} + {% set intpl_color = 'gray' %} + {% endif %} + {% if p._webdata['items'][item]['interpolation'] is not defined %} {{ _('fehlt!') }} {% elif p._webdata['items'][item]['interpolation']['type'] %} {{ p._webdata['items'][item]['interpolation']['type'] }} {% if p._webdata['items'][item]['interpolation']['type'] != 'none' and p._webdata['items'][item]['interpolation']['interval'] %} - ({{ p._webdata['items'][item]['interpolation']['interval'] }}) + {% if p._webdata['items'][item]['interpolation']['perday'] == true %} + {% set perday = ', per day' %} + {% else %} + {% set perday = '' %} + {% endif %} + ({{ p._webdata['items'][item]['interpolation']['interval'] }}{{ perday }}) {% endif %} {% else %}- {% endif %} From 95f386b0513a47923faf17078cee1eae7f81b513 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Dec 2024 13:58:37 +0100 Subject: [PATCH 20/34] uzsu: update docu --- uzsu/user_doc.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/uzsu/user_doc.rst b/uzsu/user_doc.rst index 98a3b5b58..0155ffb91 100755 --- a/uzsu/user_doc.rst +++ b/uzsu/user_doc.rst @@ -121,11 +121,16 @@ Interpolation .. important:: - Wenn die Interpolation aktiviert ist, wird das UZSU Item im gegebenen Intervall aktualisiert, auch wenn der nächste UZSU Eintrag über die Tagesgrenze hinaus geht. Gibt es beispielsweise heute um 23:00 einen Eintrag mit dem Wert 100 und morgen um 1:00 einen Eintrag mit dem Wert 0, wird zwischen den beiden Zeitpunkten der Wert kontinuierlich abnehmen. Bei linearer Interpolation wird um Mitternacht der Wert 50 geschrieben. + Wenn die Interpolation aktiviert und der "per day" Parameter auf False gesetzt ist, wird das UZSU Item im gegebenen Intervall + aktualisiert, auch wenn der nächste UZSU Eintrag über die Tagesgrenze hinaus geht. Gibt es beispielsweise heute um 23:00 einen + Eintrag mit dem Wert 100 und morgen um 1:00 einen Eintrag mit dem Wert 0, wird zwischen den beiden Zeitpunkten der Wert + kontinuierlich abnehmen. Bei linearer Interpolation wird um Mitternacht der Wert 50 geschrieben. + Dieses Verhalten kann durch Setzen von ``perday`` auf True insofern geändert werden, dass dann nur die Einträge des aktuellen + Tages für die Interpolation herangezogen werden. Vor dem ersten und nach dem letzten Tageseintrag wird nicht interpoliert. Interpolation ist ein eigenes Dict innerhalb des UZSU Dictionary mit folgenden Einträgen: -- **type**: string, setzt die mathematische Interpolationsfunktion cubic, linear oder none. Ist der Wert cubic oder linear gesetzt, wird der für die aktuelle Zeit interpolierte Wert sowohl beim Pluginstart als auch im entsprechenden Intervall gesetzt. +- **type**: string (Standard 'none'), setzt die mathematische Interpolationsfunktion cubic, linear oder none. Ist der Wert cubic oder linear gesetzt, wird der für die aktuelle Zeit interpolierte Wert sowohl beim Pluginstart als auch im entsprechenden Intervall gesetzt. - **interval**: integer, setzt den zeitlichen Abstand (in Sekunden) der automatischen UZSU Auslösungen @@ -133,7 +138,9 @@ Interpolation ist ein eigenes Dict innerhalb des UZSU Dictionary mit folgenden E - **itemtype**: Der Item-Typ des uzsu_item, das durch die UZSU gesetzt werden soll. Dieser Wert wird beim Pluginstart automatisch ermittelt und sollte nicht verändert werden. -- **initizialized**: bool, wird beim Pluginstart automatisch gesetzt, sobald ein gültiger Eintrag innerhalb der initage Zeit gefunden wurde und diese Initialisierung tatsächlich ausgeführt wurde. +- **initizialized**: bool, wird beim Pluginstart automatisch gesetzt, sobald ein gültiger Eintrag innerhalb der initage Zeit gefunden und diese Initialisierung tatsächlich ausgeführt wurde. + +- **perday**: bool (Standard False), bestimmt, ob die Interpolation nur Schaltpunkte des aktuellen Tages berücksichtigen soll (True) oder sämtliche Einträge, die unter Umständen über die ganze Woche verteilt sind (False). Ist die Interpolation und die Einmal-Funktion (global oder für einen Eintrag) aktiviert, so werden so viele interpolierte Schaltvorgänge durchgeführt, bis der tatsächlich hinterlegte Wert erreich ist. Erst dann wird der Eintrag oder die UZSU deaktiviert. @@ -143,6 +150,7 @@ Pluginfunktionen Detaillierte Informationen zu den Funktionen des Plugins sind unter :doc:`/plugins_doc/config/uzsu` zu finden. Sämtliche Pluginfunktionen funktionieren auch als Itemfunktionen für UZSU Items. Dabei muss beim Funktionsaufruf das Item nicht angegeben werden. Beispiel: Die Pluginfunktion sh.uzsu.activate(True, sh.test.uzsu) ist identisch mit dem Aufruf sh.test.uzsu.activate(True) +Es wird allerdings empfohlen, den ersten Ansatz zu wählen, um etwaige Überschneidungen mit anderen Itemfunktionen zu vermeiden. Web Interface @@ -188,10 +196,11 @@ Folgender Python Aufruf bzw. Dictionary Eintrag schaltet das Licht jeden zweiten sh.eg.wohnen.leuchte.uzsu({'active':True, 'list':[ {'value':100, 'active':True, 'rrule':'FREQ=DAILY;INTERVAL=2', 'time': '16:30'}, {'value':0, 'active':True, 'rrule':'FREQ=DAILY;INTERVAL=2', 'time': '17:30'}], - 'interpolation': {'interval': 5, 'type': 'cubic', 'initialized': False, 'itemtype': 'num', 'initage': 0}, 'sunrise': '07:45', 'sunset': '17:23', 'SunCalculated': {'sunrise': + 'interpolation': {'interval': 5, 'type': 'cubic', 'initialized': False, 'itemtype': 'num', 'initage': 0, 'perday': False}, + 'sunrise': '07:45', 'sunset': '17:23', 'SunCalculated': {'sunrise': {'TU': '07:36', 'WE': '07:38', 'TH': '07:34', 'FR': '07:32', 'SA': '07:30', 'SU': '07:28', 'MO': '07:26'}, - 'sunset': {'TU': '17:16', 'WE': '17:18', 'TH': '17:20', 'FR': '17:22', 'SA': '17:23', 'SU': '17:25', 'MO': '17:27'}}, - 'plugin_version': '1.6.1'}) + 'sunset': {'TU': '17:16', 'WE': '17:18', 'TH': '17:20', 'FR': '17:22', 'SA': '17:23', 'SU': '17:25', 'MO': '17:27'}} + }) Datenformat From d0155d88fd7bfcd7e8d5ea724a34c25a477cae21 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 20 Dec 2024 16:50:07 +0100 Subject: [PATCH 21/34] uzsu: fix extra-long debug messages --- uzsu/__init__.py | 65 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 2686f00b7..4188610e8 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -242,7 +242,8 @@ def _update_sun(self, item, caller=None): _sunset = _sunset.astimezone(self._timezone) self._items[item]['sunrise'] = f'{_sunrise.hour:02}:{_sunrise.minute:02}' self._items[item]['sunset'] = f'{_sunset.hour:02}:{_sunset.minute:02}' - self.logger.debug(f'Updated sun entries for item {item}, triggered by {caller}. sunrise: {self._items[item]["sunrise"]}, sunset: {self._items[item]["sunset"]}') + self.logger.debug(f'Updated sun entries for item {item}, triggered by {caller}. ' + f'sunrise: {self._items[item]["sunrise"]}, sunset: {self._items[item]["sunset"]}') success = True except Exception as e: success = f'Not updated sun entries for item {item}. Error {e}' @@ -265,7 +266,8 @@ def _update_suncalc(self, item, entry, entryindex, entryvalue): self._update_item(item, 'UZSU Plugin', 'update_sun') self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') elif update is True and not entry.get('calculated') == entryvalue: - self.logger.debug(f'Updated calculated time for item {item} entry {self._items[item]["list"][entryindex]} with value {entryvalue}.') + self.logger.debug(f'Updated calculated time for item {item} entry ' + f'{self._items[item]["list"][entryindex]} with value {entryvalue}.') self._items[item]['list'][entryindex]['calculated'] = entryvalue self._update_item(item, 'UZSU Plugin', 'update_sun') self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') @@ -369,7 +371,8 @@ def interpolation(self, intpl_type=None, interval=5, backintime=0, perday=False, self._items[item]['interpolation']['interval'] = abs(int(interval)) self._items[item]['interpolation']['initage'] = abs(int(backintime)) self._items[item]['interpolation']['perday'] = bool(perday) - self.logger.info(f'Item {item} interpolation is set via logic to: type={intpl_type}, interval={abs(interval)}, backintime={backintime}, perday={perday}') + self.logger.info(f'Item {item} interpolation is set via logic to: ' + f'type={intpl_type}, interval={abs(interval)}, backintime={backintime}, perday={perday}') self._update_item(item, 'UZSU Plugin', 'logic') self._write_dict_to_item(item, 'UZSU Plugin', 'logic') return self._items[item].get('interpolation') @@ -536,7 +539,8 @@ def _remove_dupes(self, item): self._items[item]['list'][self._items[item]['list'].index(entry)].update({'active': False}) time = entry['time'] oldvalue, newvalue = entry['value'], new['value'] - self.logger.warning(f'Set old entry for item "{item}" at {time} with value {oldvalue} to inactive because newer active entry with value {newvalue} found.') + self.logger.warning(f'Set old entry for item "{item}" at {time} with value {oldvalue} ' + f'to inactive because newer active entry with value {newvalue} found.') def _check_rruleandplanned(self, item): if self._items[item].get('list'): @@ -621,12 +625,14 @@ def _update_item(self, item, caller="", comment=""): if success is True: self.logger.debug(f'Updated seriesCalculated for item {item} caller: {caller} comment: {comment}') else: - self.logger.debug(f'Issues with updating seriesCalculated for item {item} caller: {caller} comment: {comment}, issue: {success}') + self.logger.debug(f'Issues with updating seriesCalculated for item {item} ' + f'caller: {caller} comment: {comment}, issue: {success}') success = self._update_sun(item, caller="_update_item") if success is True: self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') else: - self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} caller: {caller} comment: {comment}, issue: {success}') + self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} ' + f'caller: {caller} comment: {comment}, issue: {success}') #item(self._items[item], caller, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) if self._webdata['items'][item.property.path].get('interpolationrunning') is None: @@ -722,29 +728,34 @@ def _schedule(self, item, caller=None): cond_preparing = series_finished is False and not series_started series_status = "preparing" if cond_preparing else "running" if cond_running else "finished" if cond_finished else "-" self._series[item][i] = series_status - self.logger.debug(f"{item}, i {i} series_finished {series_finished} series_started {series_started} cond_running {cond_running} series_status {series_status}") + self.logger.debug(f"{item}, i {i} series_finished {series_finished} series_started {series_started} " + f"cond_running {cond_running} series_status {series_status}") cond1 = next is None and previous is not None cond2 = previous is not None and next is not None and previous < next if cond1 or cond2: next = previous value = previousvalue if next is not None: - self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, value {value} and tzinfo {next.tzinfo}. Series {self._series[item]}') + self.logger.debug(f'uzsu active entry for item {item} with datetime {next}, ' + f'value {value} and tzinfo {next.tzinfo}. Series {self._series[item]}') if _next is None: _next = next _value = value _entryindex = i elif next and next < _next: - self.logger.debug(f'uzsu active entry for item {item} using now {next}, value {value} and tzinfo {next.tzinfo}') + self.logger.debug(f'uzsu active entry for item {item} using now {next}, ' + f'value {value} and tzinfo {next.tzinfo}') _next = next _value = value _entryindex = i else: - self.logger.debug(f'uzsu active entry for item {item} keep {_next}, value {_value} and tzinfo {_next.tzinfo}') + self.logger.debug(f'uzsu active entry for item {item} keep {_next}, ' + f'value {_value} and tzinfo {_next.tzinfo}') if self._items[item]["list"][i].get("series"): #series_status = self._series.get(i) if self._items[item]["list"][i].get("once") and series_status == "finished": - self.logger.debug(f'Deactivating list entry {i} for item {item} because series is finished and set to once') + self.logger.debug(f'Deactivating list entry {i} for item {item} ' + f'because series is finished and set to once') self._items[item]["list"][i]["active"] = False _next = None self._series[item][i] = "waiting" @@ -823,7 +834,8 @@ def _schedule(self, item, caller=None): _value_now = self._interpolate(self._itpl[item], entry_now, _interpolation.lower() == 'linear') if _caller != "dry_run" and _interpolated and _value: self._set(item=item, value=_value_now, caller=_caller, interpolated=_interpolated) - self.logger.info(f'Updated: {item}, {_interpolation.lower()} interpolation value: {_value_now}, based on dict: {self._itpl[item]}. Next: {_next}, value: {_value}') + self.logger.info(f'Updated: {item}, {_interpolation.lower()} interpolation value: {_value_now}, ' + f'based on dict: {self._itpl[item]}. Next: {_next}, value: {_value}') if _value is None: _value = _oldvalue _next = _oldnext @@ -839,7 +851,8 @@ def _schedule(self, item, caller=None): update = 'init' if update == 'init' else 'reset_interpolation' self._update_item(item, 'UZSU Plugin', 'reset_interpolation') if _caller != "dry_run": - self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') + self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and ' + f'tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%d.%m.%Y %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) @@ -1012,7 +1025,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, False self._itpl[item][next.timestamp() * 1000.0] = value rstr = str(entry['rrule']).replace('\n', ';') - self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') + self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} ' + f'with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') cond_today = False if next is None else next.date() == today.date() cond_yesterday = False if next is None else next.date() - timedelta(days=1) == yesterday.date() @@ -1095,7 +1109,8 @@ def _series_calculate(self, item, caller=None, source=None): if int(daycount) * interval >= 1440: org_daycount = daycount daycount = int(1439 / interval) - self.logger.warning(f'Cut your SerieCount to {daycount} - because interval {interval} x SerieCount {org_daycount} is more than 24h') + self.logger.warning(f'Cut your SerieCount to {daycount} - because interval {interval} ' + f'x SerieCount {org_daycount} is more than 24h') if 'sun' not in mydict['series']['timeSeriesMin']: starttime = datetime.strptime(mydict['series']['timeSeriesMin'], "%H:%M") @@ -1133,7 +1148,8 @@ def _series_calculate(self, item, caller=None, source=None): else: new_daycount = int((timediff.total_seconds() // 60) // interval + 1) if int(daycount) > new_daycount: - self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} x SerieCount {daycount} is not possible between {starttime} and {endtime}') + self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} ' + f'x SerieCount {daycount} is not possible between {starttime} and {endtime}') daycount = new_daycount ##################### @@ -1170,7 +1186,11 @@ def _series_calculate(self, item, caller=None, source=None): except Exception: max_interval = endtime - starttime if exceptions == 0: - self.logger.info(f'Item {item}: Between starttime {datetime.strftime(starttime, "%H:%M")} and endtime {datetime.strftime(endtime, "%H:%M")} is a maximum valid interval of {max_interval.seconds // 3600:02d}:{max_interval.seconds % 3600//60:02d}. {mydict["series"]["timeSeriesIntervall"]} is set too high for a continuous series trigger. The UZSU will only be scheduled for the start time.') + self.logger.info(f'Item {item}: Between starttime {datetime.strftime(starttime, "%H:%M")} ' + f'and endtime {datetime.strftime(endtime, "%H:%M")} is a maximum ' + f'valid interval of {max_interval.seconds // 3600:02d}:{max_interval.seconds % 3600//60:02d}. ' + f'{mydict["series"]["timeSeriesIntervall"]} is set too high for a continuous series trigger. ' + f'The UZSU will only be scheduled for the start time.') exceptions += 1 max_interval = int(max_interval.total_seconds() / 60) myrulenext = f'FREQ=MINUTELY;COUNT=1;INTERVAL={max_interval}' @@ -1210,14 +1230,16 @@ def _series_calculate(self, item, caller=None, source=None): mytpl['seriesMax'] = f'{endtime.hour:02d}:{endtime.minute:02d}' mytpl['maxCountCalculated'] = count if exceptions == 0 else 0 mytpl['seriesDay'] = actday - self.logger.debug(f'Mytpl for last time of day: {mytpl}, count {count} daycount {original_daycount}, interval {interval}') + self.logger.debug(f'Mytpl for last time of day: {mytpl}, count {count} daycount ' + f'{original_daycount}, interval {interval}') mynewlist.append(mytpl) if mynewlist: self._items[item]['list'][i]['seriesCalculated'] = mynewlist self.logger.debug(f'Series for item {item} calculated: {self._items[item]["list"][i]["seriesCalculated"]}') except Exception as e: - self.logger.warning(f'Error: {e}. Series entry {mydict} for item {item} could not be calculated. Skipping series calculation') + self.logger.warning(f'Error: {e}. Series entry {mydict} for item {item} could not be calculated. ' + f'Skipping series calculation') continue return True @@ -1323,7 +1345,10 @@ def _series_get_time(self, mydict, timescan=''): else: new_daycount = int((timediff.total_seconds() // 60) // interval + 1) if int(daycount) > new_daycount: - self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} x SerieCount {daycount} is not possible between {datetime.strftime(starttime, "%H:%M")} and {datetime.strftime(endtime, "%H:%M")}') + self.logger.warning(f'Cut your SerieCount to {new_daycount} - because interval {interval} ' + f'x SerieCount {daycount} is not possible between ' + f'{datetime.strftime(starttime, "%H:%M")} and ' + f'{datetime.strftime(endtime, "%H:%M")}') daycount = new_daycount mylist = OrderedDict() actrrule = mydict['rrule'] + ';COUNT=9' From 38a188f9f4bed60290222cdab67079cc06cb621e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 00:40:10 +0100 Subject: [PATCH 22/34] uzsu: code improvements, fix lastvalue handling, update activeToday on first run to False if time is in the future, etc. --- uzsu/__init__.py | 193 ++++++++++++++++++-------------- uzsu/plugin.yaml | 5 - uzsu/webif/templates/index.html | 8 +- 3 files changed, 116 insertions(+), 90 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 4188610e8..e7a02c68d 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -91,7 +91,7 @@ from .webif import WebInterface ITEM_TAG = ['uzsu_item'] - +PLUGIN_TAG = 'UZSU Plugin' class UZSU(SmartPlugin): """ @@ -154,7 +154,7 @@ def run(self): self._series.update({item: {}}) self._lastvalues[item] = None self._webdata['items'][item.property.path].update({'lastvalue': '-'}) - self._update_item(item, 'UZSU Plugin', 'run') + self._update_item(item, 'run') update = 'run' cond1 = self._items[item].get('active') and self._items[item]['active'] cond2 = self._items[item].get('list') @@ -174,31 +174,38 @@ def run(self): self.logger.debug(f'Item "{item}": removed lastvalue dict entry as it is deprecated.') except Exception: pass + # set activeToday to false if entry is in the future (and wasn't reset correctly at midnight) + for i, entry in enumerate(self._items[item].get('list')): + next, _, _ = self._get_time(entry, 'next', item, i, 'dry_run') + self.logger.debug(f'this entry {entry} next {next}') + if next and next.time() > datetime.now().time() and entry.get('activeToday'): + entry['activeToday'] = False + update = 'activeToday' + self.logger.debug(f"Item '{item}' activeToday of entry {entry} set to False because it is in the future") self._check_rruleandplanned(item) if cond1 and cond2: + if update: + self._write_dict_to_item(item, update) self._schedule(item, caller='run') elif cond1 and not cond2: - self.logger.warning(f'Item "{item}" is active but has no entries.') + self.logger.warning(f"Item '{item}' is active but has no entries.") self._planned.update({item: None}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) + self._write_dict_to_item(item, update) # only if not at end else: + if update: + self._write_dict_to_item(item, update) self.logger.debug(f'Not scheduling item {item}, cond1 {cond1}, cond2 {cond2}') self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries') self._schedule(item, caller='dry_run') - self._write_dict_to_item(item, 'UZSU Plugin', update) + #self._write_dict_to_item(item, update) def stop(self): """ Stop method for the plugin """ self.logger.debug("stop method called") - self.scheduler_remove('uzsu_sunupdate') - for item in self._items: - try: - self.scheduler_remove(item.property.path) - self.logger.debug(f'Removing scheduler for item {item.property.path}') - except Exception as err: - self.logger.debug(f'Scheduler for item {item.property.path} not removed. Problem: {err}') + self.scheduler_remove_all() self.alive = False def _reset_activetoday(self): @@ -208,7 +215,7 @@ def _reset_activetoday(self): for item in self._items: for entry in self._items[item].get('list'): entry['activeToday'] = False - self._write_dict_to_item(item, 'UZSU Plugin', 'reset_activetoday') + self._write_dict_to_item(item, 'reset_activetoday') def _update_all_suns(self, caller=None): """ @@ -219,9 +226,9 @@ def _update_all_suns(self, caller=None): for item in self._items: success = self._update_sun(item, caller="update_all_suns") if success: - self._update_item(item, 'UZSU Plugin', 'update_all_suns') + self._update_item(item, 'update_all_suns') self.logger.debug(f'Updated sun info for item {item}. Caller: {caller}') - self._write_dict_to_item(item, 'UZSU Plugin', 'update_all_suns') + self._write_dict_to_item(item, 'update_all_suns') def _update_sun(self, item, caller=None): """ @@ -263,14 +270,14 @@ def _update_suncalc(self, item, entry, entryindex, entryvalue): if entry.get('calculated') and entryvalue is None: self.logger.debug(f'No sunset/rise in time for current entry {entry}. Removing calculated value.') self._items[item]['list'][entryindex].pop('calculated') - self._update_item(item, 'UZSU Plugin', 'update_sun') - self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') + self._update_item(item, 'update_sun') + self._write_dict_to_item(item, 'update_sun') elif update is True and not entry.get('calculated') == entryvalue: self.logger.debug(f'Updated calculated time for item {item} entry ' f'{self._items[item]["list"][entryindex]} with value {entryvalue}.') self._items[item]['list'][entryindex]['calculated'] = entryvalue - self._update_item(item, 'UZSU Plugin', 'update_sun') - self._write_dict_to_item(item, 'UZSU Plugin', 'update_sun') + self._update_item(item, 'update_sun') + self._write_dict_to_item(item, 'update_sun') elif entry.get('calculated'): self.logger.debug(f'Sun calculation {entryvalue} entry not updated for item {item} with value {entry["calculated"]}') @@ -302,7 +309,7 @@ def _get_type(self, item): self.logger.warning(f'Item to be set by uzsu "{_itemforuzsu}" does not have a type attribute. Error: {err}') return _itemtype - def lastvalue(self, by=None, item=None): + def lastvalue(self, by=None, item=None, write=False): if self._items.get(item): lastvalue = self._lastvalues.get(item) itempath = item.property.path @@ -311,6 +318,16 @@ def lastvalue(self, by=None, item=None): itempath = None by_test = f' Queried by {by}' if by else "" self.logger.debug(f'Last value of item {itempath} is: {lastvalue}.{by_test}') + last_item = None + if write is True: + try: + for child in item.return_children(): + if child.property.path.endswith('.last'): + last_item = child + except Exception as e: + self.logger.warning(f"Item '{item}' has issues setting last value: {e}") + if last_item: + last_item(lastvalue, PLUGIN_TAG, by) return lastvalue def resume(self, activevalue=True, item=None): @@ -321,7 +338,7 @@ def resume(self, activevalue=True, item=None): self.logger.warning(f'Item {item} does not exist!') return None self.activate(True, item) - lastvalue = self.lastvalue(item) + lastvalue = self.lastvalue(by='logic', item=item) self._set(item=item, value=lastvalue, caller='logic') self.logger.info(f'Resuming item {item}: Activated and value set to {lastvalue}. Active value: {activevalue}') return lastvalue @@ -345,8 +362,8 @@ def activate(self, activevalue=None, item=None, caller='logic'): self._items[item]['active'] = activevalue _activeinfo = "deactivated" if activevalue is False else "activated" self.logger.info(f'Item {item} is set via {caller} to: {_activeinfo}') - self._update_item(item, 'UZSU Plugin', caller) - self._write_dict_to_item(item, 'UZSU Plugin', caller) + self._update_item(item, caller) + self._write_dict_to_item(item, caller) return activevalue if activevalue is None: return self._items[item].get('active') @@ -373,8 +390,8 @@ def interpolation(self, intpl_type=None, interval=5, backintime=0, perday=False, self._items[item]['interpolation']['perday'] = bool(perday) self.logger.info(f'Item {item} interpolation is set via logic to: ' f'type={intpl_type}, interval={abs(interval)}, backintime={backintime}, perday={perday}') - self._update_item(item, 'UZSU Plugin', 'logic') - self._write_dict_to_item(item, 'UZSU Plugin', 'logic') + self._update_item(item, 'logic') + self._write_dict_to_item(item, 'logic') return self._items[item].get('interpolation') def clear(self, clear=False, item=None): @@ -393,8 +410,8 @@ def clear(self, clear=False, item=None): self._items[item].clear() self._items[item] = {'interpolation': {}, 'active': False} self.logger.info(f'UZSU settings for item "{item}" are cleared') - self._update_item(item, 'UZSU Plugin', 'clear') - self._write_dict_to_item(item, 'UZSU Plugin', 'clear') + self._update_item(item, 'clear') + self._write_dict_to_item(item, 'clear') return True else: return False @@ -417,29 +434,37 @@ def itpl(self, clear=False, item=None): self.logger.info(f'UZSU interpolation dict for item "{item}" is: {self._itpl[item]}') return self._itpl[item] - def planned(self, item=None): + def planned(self, item=None, write=False): if self._items.get(item) is None: try: self.logger.warning(f'Item {item.property.path} is no valid UZSU item!') except: self.logger.warning(f'Item {item} does not exist!') return None + next_item = None + if write is True: + try: + for child in item.return_children(): + if child.property.path.endswith('.next'): + next_item = child + except Exception as e: + self.logger.warning(f"Item '{item}' has issues setting planned value: {e}") + _planned_value = {'value': '-', 'next': '-'} if self._planned.get(item) not in [None, {}, 'notinit'] and self._items[item].get('active') is True: - self.logger.info(f"Item '{item}' is going to be set to {self._planned[item]['value']} at {self._planned[item]['next']}") - self._webdata['items'][item.property.path].update( - {'planned': {'value': self._planned[item]['value'], 'time': self._planned[item]['next']}}) - return self._planned[item] + self.logger.info(f"Item '{item}' is going to be set to {self._planned[item]['value']} " + f"at {self._planned[item]['next']}") + _planned_value = {'value': self._planned[item]['value'], 'next': self._planned[item]['next']} elif self._planned.get(item) == 'notinit' and self._items[item].get('active') is True: self.logger.info(f'Item "{item}" is active but not fully initialized yet.') - return None elif not self._planned.get(item) and self._items[item].get('active') is True: self.logger.warning(f'Item "{item}" is active but has no (active) entries.') self._planned.update({item: None}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) - return None else: self.logger.info(f'Nothing planned for item "{item}": {self._planned.get(item)}.') - return None + self._webdata['items'][item.property.path].update({'planned': _planned_value}) + if next_item: + next_item(_planned_value, PLUGIN_TAG, 'planned') + return None if _planned_value == {'value': '-', 'next': '-'} else self._planned[item] def _add_dicts(self, item): """ @@ -509,10 +534,10 @@ def parse_item(self, item): self._items[item]['list'][entry].pop('delayedExec', None) self._webdata['items'].update({item.property.path: {}}) - self._update_item(item, 'UZSU Plugin', 'init') - self._write_dict_to_item(item, 'UZSU Plugin', 'init') + self._update_item(item, 'init') + self._write_dict_to_item(item, 'init') self._planned.update({item: 'notinit'}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) return self.update_item @@ -558,11 +583,11 @@ def _check_rruleandplanned(self, item): self.logger.warning(f'Error creating rrule: {err}') if count > 0: self.logger.debug(f'Updated {count} rrule entries for item: {item}') - self._update_item(item, 'UZSU Plugin', 'create_rrule') - self._write_dict_to_item(item, 'UZSU Plugin', 'create_rrule') + self._update_item(item, 'create_rrule') + self._write_dict_to_item(item, 'create_rrule') if _inactive >= len(self._items[item]['list']): self._planned.update({item: None}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) def update_item(self, item, caller=None, source='', dest=None): """ @@ -574,7 +599,7 @@ def update_item(self, item, caller=None, source='', dest=None): :param dest: if given it represents the dest """ itemtype = self._get_type(item) - cond = itemtype is not None and ((not caller == 'UZSU Plugin') or source == 'logic') + cond = itemtype is not None and ((not caller == PLUGIN_TAG) or source == 'logic') self.logger.debug(f'Update Item {item}, Caller {caller}, Source {source}, Dest {dest}. Will update: {cond}') if not source == 'create_rrule': self._check_rruleandplanned(item) @@ -588,7 +613,7 @@ def update_item(self, item, caller=None, source='', dest=None): if cond and self._items[item].get('active') is False and not source == 'update_sun': self._lastvalues[item] = None self._webdata['items'][item.property.path].update({'lastvalue': '-'}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) self.logger.debug(f'lastvalue for item {item} set to None because UZSU is deactivated') if cond: self._schedule(item, caller='update') @@ -602,38 +627,38 @@ def update_item(self, item, caller=None, source='', dest=None): except: current_value = None if cond and self._items[item] != current_value: - self._update_item(item, 'UZSU Plugin', 'update') - self._write_dict_to_item(item, 'UZSU Plugin', 'update') + self._update_item(item, 'update') + #self._write_dict_to_item(item, 'update') - def _write_dict_to_item(self, item, caller="", comment=""): + def _write_dict_to_item(self, item, comment=""): try: current_value = item.property.value #current_value = self.itemsApi.return_item(str(item)).property.value except: current_value = None if self._items[item] != current_value: - self.logger.debug(f"Writing dict to item {item} due to caller {caller}, comment {comment}") - item(self._items[item], caller, comment) + self.logger.debug(f"Writing dict to item {item} due to {comment}") + item(self._items[item], PLUGIN_TAG, comment) - def _update_item(self, item, caller="", comment=""): + def _update_item(self, item, comment=""): success = self._get_sun4week(item, caller="_update_item") if success: - self.logger.debug(f'Updated weekly sun info for item {item} caller: {caller} comment: {comment}') + self.logger.debug(f'Updated weekly sun info for item {item} comment: {comment}') else: - self.logger.debug(f'Issues with updating weekly sun info for item {item} caller: {caller} comment: {comment}') - success = self._series_calculate(item, caller, comment) + self.logger.debug(f'Issues with updating weekly sun info for item {item} comment: {comment}') + success = self._series_calculate(item, PLUGIN_TAG, comment) if success is True: - self.logger.debug(f'Updated seriesCalculated for item {item} caller: {caller} comment: {comment}') + self.logger.debug(f'Updated seriesCalculated for item {item} comment: {comment}') else: self.logger.debug(f'Issues with updating seriesCalculated for item {item} ' - f'caller: {caller} comment: {comment}, issue: {success}') + f'comment: {comment}, issue: {success}') success = self._update_sun(item, caller="_update_item") if success is True: - self.logger.debug(f'Updated sunset/rise calculations for item {item} caller: {caller} comment: {comment}') + self.logger.debug(f'Updated sunset/rise calculations for item {item} comment: {comment}') else: self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} ' - f'caller: {caller} comment: {comment}, issue: {success}') - #item(self._items[item], caller, comment) + f'comment: {comment}, issue: {success}') + #item(self._items[item], PLUGIN_TAG, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) if self._webdata['items'][item.property.path].get('interpolationrunning') is None: self._webdata['items'][item.property.path].update({'interpolationrunning': 'False'}) @@ -760,12 +785,12 @@ def _schedule(self, item, caller=None): _next = None self._series[item][i] = "waiting" update = 'once' - self._update_item(item, 'UZSU Plugin', 'once') + self._update_item(item, 'once') elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') self._planned.update({item: None}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) if _next and _value is not None and (cond_activetoday or self._items[item].get('active') is True or _caller == "dry_run"): _reset_interpolation = False _interpolated = False @@ -775,7 +800,7 @@ def _schedule(self, item, caller=None): _interval = abs(int(_interval)) self._items[item]['interpolation']['interval'] = _interval update = 'intervalchange' - self._update_item(item, 'UZSU Plugin', 'intervalchange') + self._update_item(item, 'intervalchange') _interpolation = self._items[item]['interpolation'].get('type') _interpolation = self._interpolation_type if not _interpolation else _interpolation _initage = self._items[item]['interpolation'].get('initage') @@ -813,7 +838,7 @@ def _schedule(self, item, caller=None): self.logger.info(f'Looking if there was a value set after {_timediff} for item {item}') self._items[item]['interpolation']['initialized'] = True update = 'init' - self._update_item(item, 'UZSU Plugin', 'init') + self._update_item(item, 'init') if cond1 and not cond2 and cond3 and cond6: self._set(item=item, value=_initvalue, caller=_caller) self.logger.info(f'Updated item {item} on startup with value {_initvalue} from time {datetime.fromtimestamp(_inittime/1000.0)}') @@ -849,29 +874,31 @@ def _schedule(self, item, caller=None): if _reset_interpolation is True: self._items[item]['interpolation']['type'] = 'none' update = 'init' if update == 'init' else 'reset_interpolation' - self._update_item(item, 'UZSU Plugin', 'reset_interpolation') + self._update_item(item, 'reset_interpolation') if _caller != "dry_run": self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and ' f'tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') self._planned.update({item: {'value': _value, 'next': _next.strftime('%d.%m.%Y %H:%M:%S')}}) - self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'time': _next.strftime('%d.%m.%Y %H:%M:%S')}}) + self._webdata['items'][item.property.path].update({'planned': {'value': _value, 'next': _next.strftime('%d.%m.%Y %H:%M:%S')}}) self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) self._webdata['items'][item.property.path].update({'interpolationrunning': str(_interpolated)}) self._update_count['done'] = self._update_count.get('done', 0) + 1 update = 'init' if update == 'init' else 'add_scheduler' - self._update_item(item, 'UZSU Plugin', 'add_scheduler') + self._update_item(item, 'add_scheduler') self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated, 'seriesstatus': self._series[item].get(_entryindex)}, next=_next) if self._update_count.get('done') == self._update_count.get('todo'): - self.scheduler_trigger('uzsu_sunupdate', by='UZSU Plugin') + self.scheduler_trigger('uzsu_sunupdate', by=PLUGIN_TAG) self._update_count = {'done': 0, 'todo': 0} elif self._items[item].get('active') is True and self._items[item].get('list'): self._planned.update({item: None}) - self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'time': '-'}}) + self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) if update is not None: - self._write_dict_to_item(item, caller, update) + self.planned(item, True) + self.lastvalue(by=update, item=item, write=True) + self._write_dict_to_item(item, update) def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated=False, seriesstatus="-"): """ @@ -883,7 +910,7 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated :param interpolated: True if value is set due to interpolation, False if uzsu trigger equals exact entry in list """ _uzsuitem, _itemvalue = self._get_dependant(item) - _uzsuitem(value, 'UZSU Plugin', 'set') + _uzsuitem(value, PLUGIN_TAG, 'set') update = None self.logger.debug(f'Setting {item} entryindex: {entryindex}, seriesstatus {seriesstatus}, interpolation {interpolated} caller {caller}') self._webdata['items'][item.property.path].update({'depend': {'item': _uzsuitem.property.path, 'value': str(_itemvalue)}}) @@ -903,8 +930,8 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated update = 'globalonce' self.logger.debug(f'Deactivating UZSU for item {item} as it has "once" set to True') if update is not None: - self._update_item(item, 'UZSU Plugin', update) - self._write_dict_to_item(item, 'UZSU Plugin', update) + self._update_item(item, update) + self._write_dict_to_item(item, update) if not caller or caller == "Scheduler": self._schedule(item, caller='set') @@ -958,9 +985,9 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): rrule = rrulestr(entry['rrule'], dtstart=datetime.combine( weekbefore, parser.parse(time.strip()).time())) rstr = str(rrule).replace('\n', ';') - self.logger.debug(f"{item}: Created rrule: '{rstr}'' for time:'{time}'") + self.logger.debug(f"{item}: Created rrule: '{rstr}' for time: '{time}'") except ValueError: - self.logger.debug(f"{item}: Could not create rrule from rrule: '{entry['rrule']}' and time:'{time}'") + self.logger.debug(f"{item}: Could not create rrule from rrule: '{entry['rrule']}' and time: '{time}'") if 'sun' in time: rrule = rrulestr(entry['rrule'], dtstart=datetime.combine( weekbefore, self._sun(datetime.combine(weekbefore.date(), @@ -988,7 +1015,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): next = datetime.combine(dt.date(), parser.parse(time.strip()).time()).replace(tzinfo=self._timezone) self._update_suncalc(item, entry, entryindex, None) if next and next.date() == dt.date(): - if not self._items[item]['interpolation'].get('perday') or next.date() == datetime.now().date(): + cond_istoday = next.date() == datetime.now().date() + if caller != "dry_run" and (not self._items[item]['interpolation'].get('perday') or cond_istoday): self._itpl[item][next.timestamp() * 1000.0] = value if next - timedelta(seconds=1) > datetime.now().replace(tzinfo=self._timezone): self.logger.debug(f'{item}: Return from rrule {timescan}: {next}, value {value}.') @@ -1003,7 +1031,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) else: - if not self._items[item]['interpolation'].get('perday'): + if caller != "dry_run" and not self._items[item]['interpolation'].get('perday'): self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Include previous today (sun): {next}, value {value} for interpolation.') if entryindex is not None: @@ -1014,7 +1042,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): elif 'series' not in time: next = datetime.combine(today, parser.parse(time.strip()).time()).replace(tzinfo=self._timezone) cond_future = next > datetime.now(self._timezone) - if not cond_future: + if caller != "dry_run" and not cond_future: self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Include {timescan} today: {next}, value {value} for interpolation.') next = datetime.combine(tomorrow, parser.parse(time.strip()).time()).replace(tzinfo=self._timezone) @@ -1023,7 +1051,8 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): next = self._series_get_time(entry, timescan) if next is None: return None, None, False - self._itpl[item][next.timestamp() * 1000.0] = value + if caller != "dry_run": + self._itpl[item][next.timestamp() * 1000.0] = value rstr = str(entry['rrule']).replace('\n', ';') self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} ' f'with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') @@ -1035,11 +1064,12 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): cond_previous_today = False if next is None else next - timedelta(seconds=1) < datetime.now(self._timezone) cond_previous_yesterday = False if next is None else next - timedelta(days=1) < datetime.now(self._timezone) if next and cond_today and cond_next: - self._itpl[item][next.timestamp() * 1000.0] = value + if caller != "dry_run": + self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next today: {next}, value {value}') return next, value, False if 'series' in time else None if next and cond_tomorrow and cond_next: - if not self._items[item]['interpolation'].get('perday'): + if caller != "dry_run" and not self._items[item]['interpolation'].get('perday'): self._itpl[item][next.timestamp() * 1000.0] = value self.logger.debug(f'{item}: Return next tomorrow: {next}, value {value}') return next, value, True if 'series' in time else None @@ -1047,11 +1077,12 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): self.logger.debug(f'{item}: Return next for series: {next}, value {value}') return next, value, True if next and cond_today and cond_previous_today: - self._itpl[item][(next - timedelta(seconds=1)).timestamp() * 1000.0] = value + if caller != "dry_run": + self._itpl[item][(next - timedelta(seconds=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous today {next} because it‘s in the past.') return None, None, True if next and cond_yesterday and cond_previous_yesterday: - if not self._items[item]['interpolation'].get('perday'): + if caller != "dry_run" and not self._items[item]['interpolation'].get('perday'): self._itpl[item][(next - timedelta(days=1)).timestamp() * 1000.0] = value self.logger.debug(f'{item}: Not returning previous yesterday {next} because it‘s in the past.') return None, None, False diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index ec0b7d3e7..ee5a9faec 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -125,9 +125,6 @@ item_structs: remark: The last set value if UZSU is active type: foo visu_acl: ro - eval: sh...lastvalue('uzsu_dict_updated') - crontab: init = None - eval_trigger: .. active: remark: Use this item to easily turn on or off your UZSU @@ -145,8 +142,6 @@ item_structs: remark: The next value and time type: dict visu_acl: ro - eval: sh...planned() - eval_trigger: .. initial_value: "{'value': '-', 'next': '-'}" value: diff --git a/uzsu/webif/templates/index.html b/uzsu/webif/templates/index.html index f202066e3..8b2bf7a72 100755 --- a/uzsu/webif/templates/index.html +++ b/uzsu/webif/templates/index.html @@ -128,10 +128,10 @@ shngInsertText (item+'_dict', objResponse['items'][item]['dict'], 'maintable'); shngInsertText (item+'_dependvalue', objResponse['items'][item]['depend']['value'], 'maintable', 10); shngInsertText (item+'_nextvalue', planned, 'maintable', 10); - if (objResponse['items'][item]['planned']['time'] === '-') + if (objResponse['items'][item]['planned']['next'] === '-') shngInsertText (item+'_nexttime', '-', 'maintable', 10); else - shngInsertText (item+'_nexttime', '‪'+objResponse['items'][item]['planned']['time']+'‬', 'maintable', 10); + shngInsertText (item+'_nexttime', '‪'+objResponse['items'][item]['planned']['next']+'‬', 'maintable', 10); shngInsertText (item+'_dependitem', depend_text, 'maintable'); shngInsertText (item+'_lastvalue', lastvalue, 'maintable', 10); shngInsertText ('sunrise', sunrise_text, null, 2); @@ -236,10 +236,10 @@ {{ p._webdata['items'][item]['planned']['value'] | replace('.0', '') }} - {% if p._webdata['items'][item]['planned']['time'] == '-' %} + {% if p._webdata['items'][item]['planned']['next'] == '-' %} - {% else %} - ‪{{ p._webdata['items'][item]['planned']['time'] }}‬ + ‪{{ p._webdata['items'][item]['planned']['next'] }}‬ {% endif %} From 972ac52151a2d462ec77e7745c655669b135f864 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 01:19:33 +0100 Subject: [PATCH 23/34] uzsu: further lastvalue improvements/fixes --- uzsu/__init__.py | 17 ++++++++++------- uzsu/plugin.yaml | 4 ++-- uzsu/user_doc.rst | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index e7a02c68d..230aff456 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -317,7 +317,6 @@ def lastvalue(self, by=None, item=None, write=False): lastvalue = None itempath = None by_test = f' Queried by {by}' if by else "" - self.logger.debug(f'Last value of item {itempath} is: {lastvalue}.{by_test}') last_item = None if write is True: try: @@ -326,7 +325,8 @@ def lastvalue(self, by=None, item=None, write=False): last_item = child except Exception as e: self.logger.warning(f"Item '{item}' has issues setting last value: {e}") - if last_item: + self.logger.debug(f'Last value of item {itempath} is: {lastvalue}.{by_test}, write {write} last_item {last_item}') + if last_item is not None: last_item(lastvalue, PLUGIN_TAG, by) return lastvalue @@ -337,10 +337,13 @@ def resume(self, activevalue=True, item=None): except: self.logger.warning(f'Item {item} does not exist!') return None - self.activate(True, item) - lastvalue = self.lastvalue(by='logic', item=item) - self._set(item=item, value=lastvalue, caller='logic') - self.logger.info(f'Resuming item {item}: Activated and value set to {lastvalue}. Active value: {activevalue}') + self.activate(activevalue, item) + if activevalue: + lastvalue = self.lastvalue(by='resume', item=item) + self._set(item=item, value=lastvalue, caller='logic') + self.logger.info(f'Resuming item {item}: Activated and value set to {lastvalue}.') + else: + lastvalue = None return lastvalue def activate(self, activevalue=None, item=None, caller='logic'): @@ -462,7 +465,7 @@ def planned(self, item=None, write=False): else: self.logger.info(f'Nothing planned for item "{item}": {self._planned.get(item)}.') self._webdata['items'][item.property.path].update({'planned': _planned_value}) - if next_item: + if next_item is not None: next_item(_planned_value, PLUGIN_TAG, 'planned') return None if _planned_value == {'value': '-', 'next': '-'} else self._planned[item] diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index ee5a9faec..d6b7c8356 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -202,8 +202,8 @@ plugin_functions: lastvalue: type: foo description: - de: 'Abfrage des zuletzt gesetzten Werts. Kann z.B. beim Aktivieren der UZSU genutzt werden, um sofort auf den gewünschten Wert zu schalten.' - en: 'Query the last value. Can be used to immediately set the correct value after activating an UZSU.' + de: 'Abfrage des zuletzt gesetzten/evaluierten Werts. Kann z.B. beim Aktivieren der UZSU genutzt werden, um sofort auf den gewünschten Wert zu schalten.' + en: 'Query the last calculated/set value. Can be used to immediately set the correct value after activating an UZSU.' parameters: by: type: str diff --git a/uzsu/user_doc.rst b/uzsu/user_doc.rst index 0155ffb91..cb732cb1b 100755 --- a/uzsu/user_doc.rst +++ b/uzsu/user_doc.rst @@ -170,7 +170,7 @@ Das Webinterface bietet folgende Informationen: - **Nächstes Update**: geplanter nächster Zeitpunkt der Schaltung -- **Letzter Wert**: zuletzt berechneter Wert (relevant bei Interpolation). Dies ist NICHT ident mit property.last_value! +- **Letzter Wert**: zuletzt berechneter Wert. Dies ist NICHT ident mit property.last_value! - **Interpolation**: Interpolationstyp und Intervall From e4109aff1e8042d3b5f7b44f25eed16104afa93b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 01:52:24 +0100 Subject: [PATCH 24/34] uzsu: improve documentation, add info on once feature --- uzsu/user_doc.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/uzsu/user_doc.rst b/uzsu/user_doc.rst index cb732cb1b..7405ab44b 100755 --- a/uzsu/user_doc.rst +++ b/uzsu/user_doc.rst @@ -98,6 +98,15 @@ Für die universelle Zeitschaltuhr können folgende Einstellungen vorgenommen we * Aktivieren: Eintrag aktivieren oder deaktivieren. * Einmal-Schaltung ("Once"): Sowohl auf globaler Ebene als auch pro Eintrag kann eingestellt werden, dass ein Schaltvorgang nur ein Mal ausgeführt wird. +Einmal-Schaltung +---------------- + +Für jeden Eintrag in der UZSU Liste kann eingestellt werden, ob der Eintrag nur ein einziges Mal aktiv sein soll, oder "normal" aktiv bleibt. +Bei aktiviertem "1x" bei einem Eintrag wird dieser sofort nach dem ersten Schaltvorgang deaktiviert, es sei denn, es handelt sich um eine Serie. +Diese wird komplett bis zum Ende abgearbeitet und erst dann deaktiviert. + +Die globale Einmal-Schaltung sorgt für eine Deaktivierung der UZSU direkt nach dem nächsten Schaltvorgang, unabhängig davon, ob dieser durch eine Serie oder einen +Standardeintrag ausgelöst wurde. Serien werden in dem Fall also nicht bis zum Ende abgearbeitet. Experteneinstellungen --------------------- @@ -214,7 +223,7 @@ Jedes USZU Item wird als dict-Typ gespeichert. Darin enthalten sind einige allge - **active**: ``True`` wenn die UZSU aktiviert ist, ``False`` wenn keine Aktualisierungen vorgenommen werden sollen. Dieser Wert kann über die Pluginfunktion activate gesteuert werden. -- **once**: ``True`` wenn die UZSU oder ein Eintrag nach einmaligem Schalten deaktiviert werden soll, ``False`` wenn ein Eintrag oder die UZSU öfters evaluiert werden soll. +- **once**: ``True`` wenn ein Eintrag nach einmaligem Schalten deaktiviert werden soll, ``False`` wenn ein Eintrag weiterhin evaluiert werden bzw. aktiv bleiben soll. Serien bleiben bis zur letzten Schaltung aktiv. - **time**: Zeit als String. Entweder eine direkte Zeitangabe wie ``17:00`` oder eine Kombination mit Sonnenauf- und Untergang wie bei einem crontab, z.B. ``17:008:00``, ``17:00 Date: Sat, 21 Dec 2024 01:52:58 +0100 Subject: [PATCH 25/34] uzsu: fix evaluation of upcoming setting when once feature is used --- uzsu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 230aff456..2aced9ff4 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -969,7 +969,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, None value = entry['value'] next = None - cond_activetoday = entry.get('activeToday') is True and self._items[item]['interpolation'].get('perday') + cond_activetoday = entry.get('activeToday') is True and timescan == 'previous' active = True if caller == "dry_run" or cond_activetoday else entry.get('active') today = datetime.today() tomorrow = today + timedelta(days=1) From 22ec0f18198cc7c86a490cbafeb50d0742793c8c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 01:53:27 +0100 Subject: [PATCH 26/34] uzsu: global once deactivates uzsu on first setting, no matter if it is a series or not. --- uzsu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 2aced9ff4..0d15fe707 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -928,7 +928,7 @@ def _set(self, item=None, value=None, caller=None, entryindex=None, interpolated if entryindex is not None and item: self._series[item][entryindex] = seriesstatus self.logger.debug(f"Updated series index {entryindex} for item {item} to {seriesstatus}. Series is now {self._series[item]}") - if self._items[item].get('once') and not interpolated and seriesstatus == "-": + if self._items[item].get('once') and not interpolated: self._items[item]['active'] = False update = 'globalonce' self.logger.debug(f'Deactivating UZSU for item {item} as it has "once" set to True') From 5553a3e6fc065bd3bc30c17ac2437a9c2555767c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 21:04:05 +0100 Subject: [PATCH 27/34] uzsu: introduce set_activetoday plugin function --- uzsu/__init__.py | 41 ++++++++++++++++++++++++++++++----------- uzsu/plugin.yaml | 25 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 0d15fe707..58c581564 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -141,7 +141,7 @@ def run(self): self.scheduler_add('uzsu_sunupdate', self._update_all_suns, value={'caller': 'Scheduler:UZSU'}, cron=self._suncalculation_cron) self.logger.info(f"Adding sun update schedule for {self._suncalculation_cron}") - self.scheduler_add('uzsu_resetactivetoday', self._reset_activetoday, cron='55 59 23 * * *') + self.scheduler_add('uzsu_resetactivetoday', self.set_activetoday, value={'set': False}, cron='55 59 23 * * *') _invaliditems = [] update = '' for item in self._items: @@ -177,7 +177,6 @@ def run(self): # set activeToday to false if entry is in the future (and wasn't reset correctly at midnight) for i, entry in enumerate(self._items[item].get('list')): next, _, _ = self._get_time(entry, 'next', item, i, 'dry_run') - self.logger.debug(f'this entry {entry} next {next}') if next and next.time() > datetime.now().time() and entry.get('activeToday'): entry['activeToday'] = False update = 'activeToday' @@ -208,15 +207,6 @@ def stop(self): self.scheduler_remove_all() self.alive = False - def _reset_activetoday(self): - """ - Set activeToday to False for all list entries (for smartVISU) - """ - for item in self._items: - for entry in self._items[item].get('list'): - entry['activeToday'] = False - self._write_dict_to_item(item, 'reset_activetoday') - def _update_all_suns(self, caller=None): """ Update sun information for all uzsu items @@ -397,6 +387,34 @@ def interpolation(self, intpl_type=None, interval=5, backintime=0, perday=False, self._write_dict_to_item(item, 'logic') return self._items[item].get('interpolation') + def set_activetoday(self, set=False, item=None): + """ + Set activeToday to False for all list entries (for smartVISU) + """ + if item is None: + items_to_change = self._items + elif self._items.get(item) is None: + try: + self.logger.warning(f'Item {item.property.path} is no valid UZSU item!') + except: + self.logger.warning(f'Item {item} does not exist!') + return None + else: + items_to_change = [item] + if isinstance(set, str): + if set.lower() in ['1', 'yes', 'true', 'on']: + reset = True + elif set.lower() in ['0', 'no', 'false', 'off']: + reset = False + else: + self.logger.warning(f'Value to reset activeToday of item "{item}" has to be boolean.') + if isinstance(set, bool): + for item in items_to_change: + for entry in self._items[item].get('list'): + entry['activeToday'] = set + self.logger.debug(f'Item "{item}": set all activeToday entries to {set}') + self._write_dict_to_item(item, 'set_activetoday') + def clear(self, clear=False, item=None): if self._items.get(item) is None: try: @@ -524,6 +542,7 @@ def parse_item(self, item): item.resume = functools.partial(self.resume, item=item) item.interpolation = functools.partial(self.interpolation, item=item) item.clear = functools.partial(self.clear, item=item) + item.set_activetoday = functools.partial(self.set_activetoday, item=item) item.planned = functools.partial(self.planned, item=item) item.itpl = functools.partial(self.itpl, item=item) diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index d6b7c8356..697bdcf3f 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -241,6 +241,31 @@ plugin_functions: description: de: "Das Item-Objekt" en: "An item object" + set_activetoday: + type: bool + description: + de: 'Alle activeToday Einträge, die durch Einmalausführung gesetzt worden sind, werden auf den entsprechenden Wert gesetzt.' + en: 'All activeToday entries that have been set due to once evaluations are set to the specified value.' + description_long: + de: 'Setzen der activeToday Einträge eines Items.\n + - False: Setzen der Einträge auf False, praktisch ein Reset\n + - True: Setzen der Einträge auf True - im Normalfall nicht hilfreich, lediglich für Debuggingzwecke\n + ' + en: 'Delete the activeToday entries of an item.\n + - False: Setting the entries to False, practically resetting them\n + - True: setting the entries to True - usually not helpful except for debugging purposes\n + ' + parameters: + set: + type: bool + description: + de: "True, um die activeToday Einträge zu aktivieren, False um sie zu deaktivieren/zurückzusetzen." + en: "True to activate the activeToday entries, False to deactivate/reset them." + item: + type: foo + description: + de: "Das Item-Objekt" + en: "An item object" itpl: type: dict description: From 4089b11e7e27c1aa46303eb13201f369c026156a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 21:06:27 +0100 Subject: [PATCH 28/34] uzsu: introduce ignore_once_entries parameter for (not) using once set entries for interpolation --- uzsu/__init__.py | 7 ++++--- uzsu/plugin.yaml | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 58c581564..c57de04c9 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -120,6 +120,7 @@ def __init__(self, smarthome): self._interpolation_precision = self.get_parameter_value('interpolation_precision') self._backintime = self.get_parameter_value('backintime') self._suncalculation_cron = self.get_parameter_value('suncalculation_cron') + self._ignore_once_entries = self.get_parameter_value('ignore_once_entries') self._sh = smarthome self._items = {} self._series = {} @@ -315,7 +316,7 @@ def lastvalue(self, by=None, item=None, write=False): last_item = child except Exception as e: self.logger.warning(f"Item '{item}' has issues setting last value: {e}") - self.logger.debug(f'Last value of item {itempath} is: {lastvalue}.{by_test}, write {write} last_item {last_item}') + self.logger.debug(f'Last value of item {itempath} is: {lastvalue}.{by_test}, write {write}') if last_item is not None: last_item(lastvalue, PLUGIN_TAG, by) return lastvalue @@ -753,7 +754,7 @@ def _schedule(self, item, caller=None): update = None self._update_sun(item, caller=_caller) self._add_dicts(item) - cond_activetoday = self._items[item].get('activeToday') is True and self._items[item]['interpolation'].get('perday') + cond_activetoday = self._ignore_once_entries is False and self._items[item].get('activeToday') is True if not self._items[item]['interpolation'].get('itemtype') or \ self._items[item]['interpolation']['itemtype'] == 'none': self._items[item]['interpolation']['itemtype'] = self._get_type(item) @@ -988,7 +989,7 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): return None, None, None value = entry['value'] next = None - cond_activetoday = entry.get('activeToday') is True and timescan == 'previous' + cond_activetoday = self._ignore_once_entries is False and entry.get('activeToday') is True and timescan == 'previous' active = True if caller == "dry_run" or cond_activetoday else entry.get('active') today = datetime.today() tomorrow = today + timedelta(days=1) diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index 697bdcf3f..930fc63fe 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -40,6 +40,13 @@ parameters: de: 'Falls True, werden Einträge mit exakt den selben Einstellungen, aber unterschiedlichem Wert durch einen neu getätigten Eintrag ersetzt' en: 'If True, existing entries with exactly the same settings except the value get replaced by the new entry' + ignore_once_entries: + type: bool + default: False + description: + de: 'Falls False, werden Einträge, die durch "Once" heute inaktiv geschaltet wurden, für eine etwaige Interpolation oder Berechnung des letzten Werts herangezogen.' + en: 'If False, entries that were deactivated today due to “Once” will be considered for any interpolation or calculation of the last value.' + suncalculation_cron: type: str default: '0 0 * *' From f2c5ee517b3ecf4f8c4f5bac30786e525a4d90b6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 21:42:18 +0100 Subject: [PATCH 29/34] uzsu: fix and improve perday interpolation --- uzsu/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index c57de04c9..f92be3b57 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -1049,14 +1049,15 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): if 'sun' in time and 'series' not in time: next = self._sun(datetime.combine(today, datetime.min.time()).replace(tzinfo=self._timezone), time, timescan) cond_future = next > datetime.now(self._timezone) + cond_istoday = next.date() == datetime.now().date() if cond_future: self.logger.debug(f'{item}: Result parsing time today (sun) {time}: {next}') if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) else: - if caller != "dry_run" and not self._items[item]['interpolation'].get('perday'): + if caller != "dry_run" and (not self._items[item]['interpolation'].get('perday') or cond_istoday): self._itpl[item][next.timestamp() * 1000.0] = value - self.logger.debug(f'{item}: Include previous today (sun): {next}, value {value} for interpolation.') + self.logger.debug(f'{item}: Include previous today (sun): {next}, value {value} for interpolation.') if entryindex is not None: self._update_suncalc(item, entry, entryindex, next.strftime("%H:%M")) next = self._sun(datetime.combine(tomorrow, datetime.min.time()).replace( @@ -1074,8 +1075,10 @@ def _get_time(self, entry, timescan, item=None, entryindex=None, caller=None): next = self._series_get_time(entry, timescan) if next is None: return None, None, False - if caller != "dry_run": + cond_istoday = next.date() == datetime.now().date() + if caller != "dry_run" and (not self._items[item]['interpolation'].get('perday') or cond_istoday): self._itpl[item][next.timestamp() * 1000.0] = value + self.logger.debug(f'{item}: Include {timescan} of series: {next} for interpolation.') rstr = str(entry['rrule']).replace('\n', ';') self.logger.debug(f'{item}: Looking for {timescan} series-related time. Found rrule: {rstr} ' f'with start-time {entry["series"]["timeSeriesMin"]}. Next: {next}') From 486b350d023e725968dc43516b2e5dc609d8212c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 22:21:39 +0100 Subject: [PATCH 30/34] uzsu: avoid wrong value when 2 entries set a value at the same time and interpolation is activated --- uzsu/__init__.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index f92be3b57..61182958e 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -718,11 +718,9 @@ def _interpolate(self, data: dict, time: float, linear=True, use_precision=True) d_next = float(data[ts_next]) if linear: - # linear interpolation value = d_last + ((d_next - d_last) / (ts_next - ts_last)) * (time - ts_last) else: - # cubic interpolation with m0/m1 = 0 t = (time - ts_last) / (ts_next - ts_last) value = (2 * t ** 3 - 3 * t ** 2 + 1) * d_last + (-2 * t ** 3 + 3 * t ** 2) * d_next @@ -799,16 +797,16 @@ def _schedule(self, item, caller=None): else: self.logger.debug(f'uzsu active entry for item {item} keep {_next}, ' f'value {_value} and tzinfo {_next.tzinfo}') - if self._items[item]["list"][i].get("series"): - #series_status = self._series.get(i) - if self._items[item]["list"][i].get("once") and series_status == "finished": - self.logger.debug(f'Deactivating list entry {i} for item {item} ' - f'because series is finished and set to once') - self._items[item]["list"][i]["active"] = False - _next = None - self._series[item][i] = "waiting" - update = 'once' - self._update_item(item, 'once') + cond_once = self._items[item]["list"][i].get("once") and series_status == "finished" + if self._items[item]["list"][i].get("series") and cond_once: + self.logger.debug(f'Deactivating list entry {i} for item {item} ' + f'because series is finished and set to once') + self._items[item]["list"][i]["active"] = False + _next = None + self._series[item][i] = "waiting" + update = 'once' + self._update_item(item, 'once') + self.logger.debug(f'uzsu for item {item} final next {_next}, value {_value} and tzinfo {_next.tzinfo}') elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') @@ -884,7 +882,7 @@ def _schedule(self, item, caller=None): self._set(item=item, value=_value_now, caller=_caller, interpolated=_interpolated) self.logger.info(f'Updated: {item}, {_interpolation.lower()} interpolation value: {_value_now}, ' f'based on dict: {self._itpl[item]}. Next: {_next}, value: {_value}') - if _value is None: + if _value is None or _interpolated is False: _value = _oldvalue _next = _oldnext _interpolated = False @@ -893,7 +891,7 @@ def _schedule(self, item, caller=None): except Exception as e: self.logger.error(f'Error {_interpolation.lower()} interpolation for item {item} with interpolation list {self._itpl[item]}: {e}') if cond5 and _value < 0: - self.logger.warning(f'value {_value} for item "{item}" is negative. This might be due to not enough values set in the UZSU.') + self.logger.info(f'value {_value} for item "{item}" is negative. This might be due to not enough values set in the UZSU.') if _reset_interpolation is True: self._items[item]['interpolation']['type'] = 'none' update = 'init' if update == 'init' else 'reset_interpolation' From 81478c521838fbd6b990e05f0c5ea0ad0bf73685 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 21 Dec 2024 22:25:34 +0100 Subject: [PATCH 31/34] uzsu: update docu --- uzsu/user_doc.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/uzsu/user_doc.rst b/uzsu/user_doc.rst index 7405ab44b..ec7e2b47b 100755 --- a/uzsu/user_doc.rst +++ b/uzsu/user_doc.rst @@ -16,7 +16,7 @@ Dieses Plugin ermöglicht gezielte Schaltvorgänge von Items zu bestimmten Uhrze Sonnenstand. Die automatischen Schaltungen können dabei pro Wochentag separat definiert werden. Außerdem ermöglicht eine Interpolationsfunktion das Errechnen von Werten zwischen zwei manuell -angelegten Schaltzeiten, wodurch z.B. Lichtkurven über den Tagesverlauf umgesetzt werden können +angelegten Schaltzeiten, wodurch z.B. Lichtkurven über den Tagesverlauf umgesetzt werden können. Einführung @@ -25,6 +25,11 @@ Einführung Die Funktionsweise der universellen Zeitschaltuhr wird auf dem `SmarthomeNG Blog `_ beschrieben. Dort finden sich auch einige praktische Beispiele. +Setzen mehrere UZSU Einträge einen Wert zum selben Zeitpunkt, hat der früher definierte Listeneintrag Vorrang, +weitere später gereihte Schaltvorgänge werden nicht ausgeführt. +Beispiel: Erster Eintrag in der Liste setzt um 20:02 Uhr den Wert auf 0, der zweite Eintrag würde den Wert zur selben Zeit, +also 20:02 Uhr auf 100 setzen. De facto wird um 20:02 Uhr der Wert auf 0 gesetzt, der zweite Eintrag wird ignoriert. + Konfiguration ============= From 61878e53d07c07985fd681bb15635e68b1f63833 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 22 Dec 2024 11:38:24 +0100 Subject: [PATCH 32/34] uzsu: fix wrong logger entry --- uzsu/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 61182958e..3c49262a5 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -804,9 +804,10 @@ def _schedule(self, item, caller=None): self._items[item]["list"][i]["active"] = False _next = None self._series[item][i] = "waiting" - update = 'once' - self._update_item(item, 'once') - self.logger.debug(f'uzsu for item {item} final next {_next}, value {_value} and tzinfo {_next.tzinfo}') + update = 'schedule_once' + self._update_item(item, update) + tz_text = '' if _next is None else f' and tzinfo {_next.tzinfo}' + self.logger.debug(f'uzsu for item {item} final next {_next}, value {_value}{tz_text}') elif not self._items[item].get('list') and self._items[item].get('active') is True: self.logger.warning(f'item "{item}" is active but has no entries.') From 2b12069b3316af9538e2a1980b3cdd8143ce6289 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 22 Dec 2024 11:41:15 +0100 Subject: [PATCH 33/34] uzsu: improve item writing, fix issue when uzsu has no active entries --- uzsu/__init__.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 3c49262a5..210085bd4 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -645,13 +645,6 @@ def update_item(self, item, caller=None, source='', dest=None): else: self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries. Source: {source}') self._schedule(item, caller='dry_run') - try: - current_value = self.itemsApi.return_item(str(item)).property.value - except: - current_value = None - if cond and self._items[item] != current_value: - self._update_item(item, 'update') - #self._write_dict_to_item(item, 'update') def _write_dict_to_item(self, item, comment=""): try: @@ -681,7 +674,6 @@ def _update_item(self, item, comment=""): else: self.logger.debug(f'Issues with updating sunset/rise calculations for item {item} ' f'comment: {comment}, issue: {success}') - #item(self._items[item], PLUGIN_TAG, comment) self._webdata['items'][item.property.path].update({'interpolation': self._items[item].get('interpolation')}) if self._webdata['items'][item.property.path].get('interpolationrunning') is None: self._webdata['items'][item.property.path].update({'interpolationrunning': 'False'}) @@ -694,6 +686,8 @@ def _update_item(self, item, comment=""): _uzsuitem, _itemvalue = self._get_dependant(item) item_id = None if _uzsuitem is None else _uzsuitem.property.path self._webdata['items'][item.property.path].update({'depend': {'item': item_id, 'value': str(_itemvalue)}}) + if not comment.startswith('schedule_'): + self._write_dict_to_item(item, comment) def _interpolate(self, data: dict, time: float, linear=True, use_precision=True): """ @@ -821,8 +815,8 @@ def _schedule(self, item, caller=None): if _interval < 0: _interval = abs(int(_interval)) self._items[item]['interpolation']['interval'] = _interval - update = 'intervalchange' - self._update_item(item, 'intervalchange') + update = 'schedule_intervalchange' + self._update_item(item, update) _interpolation = self._items[item]['interpolation'].get('type') _interpolation = self._interpolation_type if not _interpolation else _interpolation _initage = self._items[item]['interpolation'].get('initage') @@ -859,8 +853,8 @@ def _schedule(self, item, caller=None): if not cond2 and cond3 and cond4: self.logger.info(f'Looking if there was a value set after {_timediff} for item {item}') self._items[item]['interpolation']['initialized'] = True - update = 'init' - self._update_item(item, 'init') + update = 'schedule_init' + self._update_item(item, update) if cond1 and not cond2 and cond3 and cond6: self._set(item=item, value=_initvalue, caller=_caller) self.logger.info(f'Updated item {item} on startup with value {_initvalue} from time {datetime.fromtimestamp(_inittime/1000.0)}') @@ -895,8 +889,8 @@ def _schedule(self, item, caller=None): self.logger.info(f'value {_value} for item "{item}" is negative. This might be due to not enough values set in the UZSU.') if _reset_interpolation is True: self._items[item]['interpolation']['type'] = 'none' - update = 'init' if update == 'init' else 'reset_interpolation' - self._update_item(item, 'reset_interpolation') + update = 'schedule_reset_interpolation' + self._update_item(item, update) if _caller != "dry_run": self.logger.debug(f'will add scheduler named uzsu_{item.property.path} with datetime {_next} and ' f'tzinfo {_next.tzinfo} and value {_value} based on list index {_entryindex}') @@ -905,8 +899,8 @@ def _schedule(self, item, caller=None): self._webdata['items'][item.property.path].update({'seriesrunning': 'True' if self._series[item].get(_entryindex) == "running" else 'False'}) self._webdata['items'][item.property.path].update({'interpolationrunning': str(_interpolated)}) self._update_count['done'] = self._update_count.get('done', 0) + 1 - update = 'init' if update == 'init' else 'add_scheduler' - self._update_item(item, 'add_scheduler') + update = 'schedule_add_scheduler' + self._update_item(item, update) self.scheduler_add(item.property.path, self._set, value={'item': item, 'value': _value, 'caller': 'Scheduler', 'entryindex': _entryindex, 'interpolated': _interpolated, @@ -917,7 +911,10 @@ def _schedule(self, item, caller=None): elif self._items[item].get('active') is True and self._items[item].get('list'): self._planned.update({item: None}) self._webdata['items'][item.property.path].update({'planned': {'value': '-', 'next': '-'}}) - if update is not None: + if update is None: + self.logger.debug("Updating item...") + self._update_item(item, 'schedule') + else: self.planned(item, True) self.lastvalue(by=update, item=item, write=True) self._write_dict_to_item(item, update) From c3c1348fb142c53a032679137e4d9adb57df9648 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 22 Dec 2024 14:05:44 +0100 Subject: [PATCH 34/34] uzsu: avoid cycle of schedule and item update --- uzsu/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uzsu/__init__.py b/uzsu/__init__.py index 210085bd4..f0e557b9e 100755 --- a/uzsu/__init__.py +++ b/uzsu/__init__.py @@ -642,14 +642,14 @@ def update_item(self, item, caller=None, source='', dest=None): self._schedule(item, caller='update') elif 'sun' in source: self.logger.info(f'Not running dry run of scheduler calculation for item {item} because of {source} source') - else: - self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries. Source: {source}') + elif source != 'schedule': + self.logger.info(f'Dry run of scheduler calculation for item {item} to get calculated sunset/rise entries. ' + f'Caller: {caller}, Source: {source}') self._schedule(item, caller='dry_run') def _write_dict_to_item(self, item, comment=""): try: current_value = item.property.value - #current_value = self.itemsApi.return_item(str(item)).property.value except: current_value = None if self._items[item] != current_value: