From 8e716350ba4c1fe83d88955af424d6035848a8f5 Mon Sep 17 00:00:00 2001 From: Martijn van der Pol Date: Thu, 5 Oct 2023 16:03:20 +0200 Subject: [PATCH] Allow non hourly data (#46) --- README.md | 4 +- cheapest_energy_hours.jinja | 273 ++++++++++++++++++++--------------- example_package/package.yaml | 59 +++++--- example_package/script.yaml | 12 +- example_package/sensor.yaml | 43 ++++-- 5 files changed, 225 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 599cd3a..68938b1 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ In case `hours` is provided, this will overrule the calculated number of hours, |name|type|default|example|description| |---|---|---|---|---| -|`no_weight_points`|integer|`1`|`4`|The most important hour in your hour range. Eg if hour device uses most energy in the 2nd hour, you can set this to `2` to give more weight to that energy price| +|`no_weight_points`|integer|`1`|`4`|The number of weight points per hour, eg set to `4` if each weight point represents 15 minutes. This should match with the datapoints per hour, meaning the number of minutes each list item in the sensor represents (normally 60, for dynamic prices per hour) shoudl be divisable by the number of minutes per weight point. So with hourly data you can use `4` or `12` but not `7`| |`weight`|list|`none`|`[25, 1, 4, 0]`|The list with weight factors to be used for the calculation| |`program`|string|none|`"Dryer Clothes"`| Description of data used in the energy plot sensor. Adds automatically the correct weight and number of weight points. @@ -189,9 +189,11 @@ The macro will display error messages as output in case of incorrect input. |No valid data in selected sensor|The provided sensor does not have valid data to work with, the sensor might be unavailable, or you need to provide a specific `attr_today` or `attr_tomorrow`| |Time key not found in data|The time key can not be found in the source data, you might need to proviee a `time_key` parameter| |Value key not found in data|The value key can not be found in the source data, you might need to provide a `value_key` parameter| +|Boolean input expected for {parameter}|The mentioned parameter expects a boolean input, but something else is provided. You can provice values like `0`, `false`, `"True"` but not `"banana"`| |Invalid mode selected|An invalid value for the `mode` parameter was provided, check your input| |Selected program is not available or has no data|The value provided for the `program` parameter can not be found in the sensor, or has no data| |Data plot for selected program not complete|The data plot for the value in the `program` parameter is incomplete| +|Invalid combination of data points per hour and number of weight points|The number of weight points does not match with the datapoints per hour provided by the sensor. If you eg have quarterly prices, you can't have 6 datapoints per hour, as 15 is not divisable by 10| |No(t enough) data within current selection|There is no, or not enough data to match your input. This can happen if you eg want a consecutive block of 4 hours, and you only use todays data for future hours, when it's already after 21:00| # Thanks to diff --git a/cheapest_energy_hours.jinja b/cheapest_energy_hours.jinja index f1b8d78..2f7c05b 100644 --- a/cheapest_energy_hours.jinja +++ b/cheapest_energy_hours.jinja @@ -1,43 +1,70 @@ -{%- macro cheapest_energy_hours(sensor, hours, start, end, attr_today, attr_tomorrow, time_key, value_key, include_today, include_tomorrow, lowest, mode, look_ahead, time_format, no_weight_points, weight, program, value_on_error) -%} +{%- macro _format_date(datetime, time_format) -%} + {%- if time_format is not none -%} + {%- set format = dict(time12='%I:%M %p', time24='%H:%M') -%} + {{- datetime.strftime(format[time_format] | default(time_format)) -}} + {%- else -%} + {{- datetime.isoformat() -}} + {%- endif -%} +{%- endmacro -%} + +{%- macro cheapest_energy_hours( + sensor, + start, + end, + value_on_error, + hours, + attr_today='raw_today', + attr_tomorrow='raw_tomorrow', + time_key='start', + value_key='value', + mode='start', + time_format=none, + include_today=true, + include_tomorrow=false, + lowest=true, + look_ahead=false, + no_weight_points=1, + weight=none, + program=none + ) +-%} {%- set modes = ['min', 'max', 'average', 'start', 'end', 'list', 'weighted_average','time_min','time_max', 'split'] %} {# Get data out of the selected entity #} - {%- set today = state_attr(sensor, attr_today | default('raw_today')) -%} - {%- set tomorrow = state_attr(sensor, attr_tomorrow | default('raw_tomorrow')) -%} - {%- set wp = no_weight_points | default(1) | int(1) -%} - {%- set h = hours | default(1) | int(1) -%} - {%- set w = weight | default(none) -%} - {%- set p = program | default(none) -%} - {%- set m = mode | default('start') -%} + {%- set today = state_attr(sensor, attr_today) -%} + {%- set tomorrow = state_attr(sensor, attr_tomorrow) -%} + {%- set h = hours | default(1) | float(1) -%} {%- set use_voe = value_on_error is defined -%} - {%- set tk, vk = time_key | default('start'), value_key | default('value') -%} - {%-if p is not none-%} - {%- set w = state_attr('sensor.energy_plots', 'energy_plots').get(p, {}).get('data') | default(none, true) -%} - {%- set wp = state_attr('sensor.energy_plots', 'energy_plots').get(p, {}).get('no_weight_points', 1) -%} - {%- set c = state_attr('sensor.energy_plots', 'energy_plots').get(p, {}).get('complete', false) -%} + {# set weight points based on energy plot sensor #} + {%-if program is not none-%} + {%- set weight = state_attr('sensor.energy_plots', 'energy_plots').get(program, {}).get('data') | default(none, true) -%} + {%- set no_weight_points = state_attr('sensor.energy_plots', 'energy_plots').get(program, {}).get('no_weight_points', 1) -%} {%- endif -%} - {%- set w = w | map('int', none) | reject('none') | list - if w is iterable and w is not string + {%- set w = weight | map('float', none) | reject('none') | list + if weight is iterable and weight is not string else none -%} {# set number of hours based on weight points in case hours setting is not provided #} - {%- set h_wp = (w | count / wp) | round(0, 'ceil') | int if w else h -%} + {%- set h_wp = (w | count / no_weight_points) | round(0, 'ceil') | int if w else h -%} {%- set h = h if hours is defined else h_wp -%} {# set weight points based on number of hours (either add zeros, or remove unneeded part) #} {%- if w is not none -%} {%- if h_wp <= h -%} - {%- set w = w + [0] * (h*wp - w | count) -%} + {%- set w = w + [0] * int(h*no_weight_points - w | count) -%} {%- else -%} - {%- set w = w[:h*wp] -%} + {%- set w = w[:int(h*no_weight_points)] -%} {%- endif -%} {%- endif -%} {# Return error messages for input #} {%- set errors = [ - 'No valid data in selected sensor' if not today and not tomorrow, - 'Time key not found in data' if today is not none and tk not in today[0], - 'Value key not found in data' if today is not none and vk not in today[0], - 'Invalid mode selected' if m not in modes, - 'Selected program is not available or has no data' if p is not none and w is none, - 'Data plot for selected program not complete' if p is not none and w is not none and not c + 'No valid data in selected sensor' if not today and not tomorrow, + 'Time key not found in data' if today is not none and time_key not in today[0], + 'Value key not found in data' if today is not none and value_key not in today[0], + 'Invalid mode selected' if mode not in modes, + 'Boolean input expected for include_today' if include_today | bool('') is not boolean, + 'Boolean input expected for include_tomorrow' if include_tomorrow | bool('') is not boolean, + 'Boolean input expected for look_ahead' if look_ahead | bool('') is not boolean, + 'Boolean input expected for lowest' if lowest | bool('') is not boolean, + 'Selected program is not available or has no data' if program is not none and w is none ] | select() | list -%} {%- if errors | count > 0 -%} @@ -45,113 +72,121 @@ {# no error - continue with macro #} {%- else -%} {# Set defaults for variables which are not provided #} - {%- set l = lowest | default(true) | bool(true) -%} - {%- set itd = include_today | default(true) | bool(true) -%} - {%- set it = include_tomorrow | default(false) | bool(false) -%} - {%- set la = look_ahead | default(false) |bool(false) -%} {%- set s = today_at(start) if start is defined else today_at() -%} - {%- set s = s + timedelta(days=1) if not itd else s -%} + {%- set s = s + timedelta(days=1) if not include_today else s -%} {%- set n = today_at(now().strftime('%H:00')) -%} - {%- set s = n if la and s < n else s -%} - {%- set e = (today_at(end) if end is defined else today_at() + timedelta(days=1)) + timedelta(days=1 if it else 0) -%} - {%- set e = e + timedelta(days=1) if not itd and end is defined else e -%} - {# Check if the dateteime in the sensor is a string, and convert start and end if needed #} - {%- set str = today[0][tk] is string -%} - {%- set s, e = s.isoformat() if str else s, e.isoformat() if str else e -%} + {%- set s = n if look_ahead and s < n else s -%} + {%- set e = (today_at(end) if end is defined else today_at() + timedelta(days=1)) + timedelta(days=1 if include_tomorrow else 0) -%} + {%- set e = e + timedelta(days=1) if not include_today and end is defined else e -%} + {# Rebuild data from sensor to generic format #} + {%- set rebuild = namespace(data=[]) -%} + {%- for item in (today if include_today else []) + (tomorrow if include_tomorrow else []) -%} + {%- set time = item[time_key] -%} + {%- set time = as_datetime(time) if time is string else time -%} + {%- set rebuild.data = rebuild.data + [dict(time=time, value=item[value_key])] -%} + {%- endfor -%} + {# determine how many data points per hour are used #} + {%- set dph = 3600 / (rebuild.data[1].time - rebuild.data[0].time).seconds -%} + {%- set dp = (h * dph) | round(0, 'ceil') | int -%} {# Perform selection based on start and end on the data #} - {%- set values = - ((today if itd else []) + (tomorrow if it else [])) - | selectattr(tk, '>=', s) - | selectattr(tk, '<', e) - | selectattr(vk, 'is_number') - | list + {%- set values = rebuild.data + | selectattr('time', '>=', s) + | selectattr('time', '<', e) + | selectattr('value', 'is_number') + | list -%} + {%- endif -%} + {# set variables to check if numbeer of wp/h match dp/h #} + {%- set dp_minutes, wp_minutes = 60 / dph, 60/no_weight_points -%} + {%- set check_list = [dp_minutes, wp_minutes] | sort %} + {%- set check = check_list[1] % check_list[0] == 0 %} {# Check if there is data, and find the right hour block #} - {%- if values | count >= h -%} - {%- if m == 'split' -%} - {# sort values and take out hours needed#} - {%- set values = (values | sort(attribute=vk))[:h] | sort(attribute=tk) -%} - {%- set ns = namespace(split=[], start=none, prices=[], hours=0) -%} - {%- for i in range(h) -%} - {%- set dt = values[loop.index0][tk] -%} - {%- set dt = as_datetime(dt) if str else dt -%} - {%- if i == 0 -%} - {%- set ns.start = dt -%} - {%- set ns.hours = 1 -%} - {%- set ns.prices = [values[0][vk]] -%} - {%- elif values[loop.index0 - 1][tk] != dt - timedelta(hours=1) -%} - {%- set ns.split = ns.split + [dict(start=ns.start.isoformat(), end=(ns.start + timedelta(hours=1)).isoformat(), hours=ns.hours, prices=ns.prices)] -%} - {%- set ns.start = dt -%} - {%- set ns.hours = 1 -%} - {%- set ns.prices = [values[loop.index0][vk]] -%} - {%- else -%} - {%- set ns.hours = ns.hours + 1 -%} - {%- set ns.prices = ns.prices + [values[loop.index0][vk]] -%} - {%- if loop.last -%} - {%- set ns.split = ns.split + [dict(start=ns.start.isoformat(), end=(dt + timedelta(hours=1)).isoformat(), hours=ns.hours, prices=ns.prices)] -%} - {%- endif -%} - {%- endif -%} + {%- if values | count >= dp and check-%} + {%- if mode == 'split' -%} + {# sort values and take out hours needed #} + {%- set values = (values | sort(attribute='value'))[:dp] | sort(attribute='time') -%} + {%- set split = namespace(split=[], start=none, prices=[], datapoints=0) -%} + {%- for i in range(dp) -%} + {%- set dt = values[loop.index0].time -%} + {%- if i == 0 -%} + {%- set split.start = dt -%} + {%- set split.datapoints = 1 -%} + {%- set split.prices = [values[0].value] -%} + {%- elif values[loop.index0 - 1].time != dt - timedelta(hours=1/dph) -%} + {%- set split.split = split.split + [dict(start=_format_date(split.start, time_format), end=_format_date(split.start + timedelta(hours=split.datapoints/dph), time_format), hours=split.datapoints/dph, prices=split.prices)] -%} + {%- set split.start = dt -%} + {%- set split.datapoints = 1 -%} + {%- set split.prices = [values[loop.index0].value] -%} + {%- else -%} + {%- set split.datapoints = split.datapoints + 1 -%} + {%- set split.prices = split.prices + [values[loop.index0].value] -%} + {%- if loop.last -%} + {%- set split.split = split.split + [dict(start=_format_date(split.start, time_format), end=_format_date(dt + timedelta(hours=1/dph), time_format), hours=split.datapoints/dph, prices=split.prices)] -%} + {%- endif -%} + {%- endif -%} {%- endfor -%} - {{ ns.split | to_json }} + {{ split.split | to_json }} {%- else -%} - {# Change datetimes in case multiple weight factors per hour are used #} - {%- set ns = namespace(values=[]) -%} - {%- if wp != 1 and w is not none -%} - {%- for v in (values * wp) | sort(attribute=tk) -%} - {%- set t = as_datetime(v[tk]) if str else v[tk] -%} - {%- set t = t + timedelta(minutes=((60/wp)*(loop.index0 % wp))) -%} - {%- set ns.values = ns.values + [ { tk: t, vk: v[vk] } ] -%} - {%- endfor -%} - {%- set values = ns.values -%} - {%- set str = false -%} - {%- endif -%} - {%- set ns = namespace(average=none, start=none, min=none, max=none, weighted_average=none, time_min=none, time_max=none) -%} - {%- for i in values[:values|length-(h*wp-1)] -%} - {%- set ix = loop.index0 -%} - {%- set list = values[ix:ix+h*wp] | map(attribute=vk) | list -%} - {# calculate weighted average #} - {%- if w is not none -%} - {%- set wa = namespace(sum=0,divide=0) -%} - {%- for i in list -%} - {%- set wa.sum = wa.sum + i * w[loop.index0] -%} - {%- set wa.divide = wa.divide + w[loop.index0] -%} + {# Change data based on weight input and datpoints per hour if needed #} + {%- if wp_minutes != dp_minutes and w is not none -%} + {%- if dp_minutes > wp_minutes %} + {%- set values_adjust = namespace(values=[]) -%} + {%- for v in (values * int(dp_minutes/wp_minutes)) | sort(attribute='time') -%} + {%- set t = v.time -%} + {%- set t = t + timedelta(minutes=(wp_minutes*(loop.index0 % int(dp_minutes/wp_minutes)))) -%} + {%- set values_adjust.values = values_adjust.values + [ dict(time=t, value=v.value) ] -%} {%- endfor -%} - {%- set a = wa.sum / wa.divide -%} - {%- else -%} - {%- set a = list | sum / h -%} - {%- endif -%} - {%- set b = ns.weighted_average -%} - {%- set min = list | min -%} - {%- set max = list | max -%} - {%- if ns.average is none or ((a < b) if l else (a > b)) -%} - {%- set ns.list = list -%} - {%- set ns.min = min -%} - {%- set ns.max = max -%} - {%- set ns.weighted_average = a -%} - {%- set ns.average = list | average -%} - {%- set ns.start = as_datetime(i[tk]) if str else i[tk] -%} - {%- set ns.end = ns.start + timedelta(hours=h) -%} - {%- set index_min = ns.list.index(ns.min) -%} - {%- set index_max = ns.list.index(max) -%} - {%- set ns.time = values[ix:ix+h*wp] | map(attribute=tk) | list -%} - {%- set ns.time_min = as_datetime(ns.time[index_min]) if str else ns.time[index_min] -%} - {%- set ns.time_max = as_datetime(ns.time[index_max]) if str else ns.time[index_max] -%} - {%- endif -%} - {%- endfor -%} - {# output date based on the selected mode #} - {%- if m in [ 'start', 'end','time_min','time_max'] -%} - {%- if time_format is defined -%} - {%- set format = dict(time12='%I:%M %p', time24='%H:%M') -%} - {{- ns[m].strftime(format[time_format] | default(time_format)) -}} + {%- set values = values_adjust.values -%} {%- else -%} - {{- ns[m].isoformat() -}} + {%- set weight_adjust = namespace(weight=[]) -%} + {%- for item in w -%} + {%- set weight_adjust.weight = weight_adjust.weight + [item] * int(wp_minutes/dp_minutes) -%} + {%- endfor -%} + {%- set w = weight_adjust.weight -%} {%- endif -%} - {%- else -%} - {{- ns[m] | round(5) if ns[m] | is_number else ns[m] -}} {%- endif -%} + {# create output for all modes #} + {%- set output = namespace(average=none, start=none, min=none, max=none, weighted_average=none, time_min=none, time_max=none, list=[]) -%} + {%- set last_values = int(h*no_weight_points) -%} + {%- for i in range(values|count-(last_values-1)) -%} + {%- set list = values[i:i+last_values] | map(attribute='value') | list -%} + {# calculate (weighted) average price #} + {%- if w is not none -%} + {%- set wa = namespace(sum=0,divide=0) -%} + {%- for i in list -%} + {%- set wa.sum = wa.sum + i * w[loop.index0] -%} + {%- set wa.divide = wa.divide + w[loop.index0] -%} + {%- endfor -%} + {%- set a = wa.sum / wa.divide -%} + {%- else -%} + {%- set a = list | sum / dp -%} + {%- endif -%} + {%- set b = output.weighted_average -%} + {%- set min, max = list | min, list | max -%} + {%- if output.average is none or ((a < b) if lowest else (a > b)) -%} + {%- set output.list = list -%} + {%- set output.min = min -%} + {%- set output.max = max -%} + {%- set output.weighted_average = a -%} + {%- set output.average = list | average -%} + {%- set output.start = _format_date(values[i].time, time_format | default(none)) -%} + {%- set output.end = _format_date(values[i].time + timedelta(hours=h), time_format | default(none)) -%} + {%- set index_min = output.list.index(output.min) -%} + {%- set index_max = output.list.index(max) -%} + {%- set output.time = values[i:i+last_values] | map(attribute='time') | list -%} + {%- set output.time_min = _format_date(output.time[index_min], time_format | default(none)) -%} + {%- set output.time_max = _format_date(output.time[index_max], time_format | default(none)) -%} + {%- endif -%} + {%- endfor -%} + {# output date based on the selected mode #} + {{- output[mode] | round(5) if output[mode] | is_number else output[mode] -}} {%- endif -%} {%- else -%} - {{- value_on_error if use_voe else 'No' ~ ('t enough' if values) ~ ' data within current selection' -}} + {%- set error_msg = + 'Invalid combination of data points per hour and number of weight points' + if not check else + 'No' ~ ('t enough' if values) ~ ' data within current selection' + -%} + {{- value_on_error if use_voe else error_msg -}} {%- endif -%} - {%- endif -%} {%- endmacro -%} diff --git a/example_package/package.yaml b/example_package/package.yaml index fa5b941..3e39d51 100644 --- a/example_package/package.yaml +++ b/example_package/package.yaml @@ -50,12 +50,12 @@ script: sequence: - variables: not_defined: > - {{ [ - 'energy sensor' if sensor is not defined or not sensor else none, - 'stop entity' if stop_entity is not defined or not stop_entity else none, - 'stop state' if stop_state is not defined else none - ] | reject('none') | list - }} + {{ [ + 'energy sensor' if sensor is not defined or not sensor else none, + 'stop entity' if stop_entity is not defined or not stop_entity else none, + 'stop state' if stop_state is not defined else none + ] | reject('none') | list + }} - if: "{{ not_defined | count > 0 }}" then: - stop: > @@ -94,6 +94,10 @@ template: - trigger: - platform: event event_type: update_energy_plot + id: plot_update + - platform: homeassistant + event: start + id: remove_incomplete sensor: - unique_id: 36c9491c-2e16-4fc3-bc9f-a6ada5fc88b7 name: Energy plots @@ -101,37 +105,44 @@ template: attributes: energy_plots: > {%- set c = this.attributes.get('energy_plots', {}) -%} - {%- if trigger.event.data is defined -%} + {%- if trigger.id == 'plot_update' and trigger.event.data is defined -%} {%- set st = trigger.event.data.status | default('unknown') -%} - {%- set d = trigger.event.data.description | default('unknown') -%} - {%- if st == 'remove' -%} + {%- set d = trigger.event.data.description | default('unknown') ~ ('(incomplete)' if not st in ['complete', 'remove'] else '') -%} + {%- if st == 'remove_all' -%} + {{ dict() }} + {%- elif st == 'remove' -%} {{ dict(c.items() | rejectattr('0', 'eq', d)) }} {%- else -%} {%- set s = trigger.event.data.state | default(0) -%} {%- set wp = trigger.event.data.no_weight_points | default(none) %} {%- set dt = now().replace(microsecond=0).isoformat() -%} {%- if st == 'first' -%} - {%- set p = {d: dict(data=[], state=s, start=s, no_weight_points=wp, last_update=dt, complete=false)} -%} - {%- elif st == 'complete' -%} - {%- set values = c.get(d, {}) -%} - {%- set data = values.get('data', []) -%} - {%- set u = (values.get('state', 0) - values.get('start', 0)) | round(3) -%} - {%- set no_zero = data | select() | list -%} - {%- set factor = 1 / no_zero | min if no_zero else 0 -%} - {%- set data = data | map('multiply', factor) | map('round', 3) | list -%} - {%- set p = {d: dict(data=data, kwh_used=u, no_weight_points=wp, last_update=dt, complete=true)} -%} + {%- set p = {d: dict(data=[], state=s, start=s, start_time=now().isoformat(), no_weight_points=wp, last_update=dt)} -%} {%- elif st == 'ongoing' -%} {%- set values = c.get(d, {}) -%} {%- set data = values.get('data', []) -%} {%- set u = s - values.get('state', 0) -%} {%- set start = values.get('start', 0) -%} + {%- set start_time = values.get('start_time', now().isoformat()) -%} {%- set data = data + [u | round(3)] -%} - {%- set p = {d: dict(data=data, state=s, start=start, no_weight_points=wp, last_update=dt, complete=false)} -%} - {%- else -%} - {%- set p = {} -%} + {%- set p = {d: dict(data=data, state=s, start=start, start_time=start_time, no_weight_points=wp, last_update=dt)} -%} + {%- elif st == 'complete' -%} + {%- set di = d~'(incomplete)' -%} + {%- set values = c.get(di, {}) -%} + {%- set data = values.get('data', []) -%} + {%- set u = (values.get('state', 0) - values.get('start', 0)) | round(3) -%} + {%- set start_time = values.get('start_time', now().isoformat()) -%} + {%- set duration = (now() - as_datetime(start_time)).total_seconds() | int %} + {%- set no_zero = data | select() | list -%} + {%- set factor = 1 / no_zero | min if no_zero else 0 -%} + {%- set data = data | map('multiply', factor) | map('round', 3) | list -%} + {%- set c = dict(c.items() | rejectattr('0', 'eq', di)) -%} + {%- set p = {d: dict(data=data, kwh_used=u, duration=duration, no_weight_points=wp, last_update=dt)} -%} {%- endif -%} - {{ dict(c, **p) }} + {{ dict(c, **p | default({})) }} {%- endif -%} + {%- elif trigger.id == 'remove_incomplete' -%} + {{ dict(c.items() | rejectattr('0', 'search', '\(incomplete\)$')) }} {%- else -%} {{ c }} {%- endif -%} @@ -139,10 +150,10 @@ template: ## AUTOMATION TO START THE SCRIPT ## automation: - id: 2794cd64-a5d0-48e3-9edb-08527ac231bc - alias: "F02 - Plot Wasmachine Energy" + alias: "F2L - Plot Wasmachine Energy" trigger: - platform: state - entity_id: sensor.wasmachine + entity_id: binary_sensor.wasmachine_door_lock from: "off" to: "on" action: diff --git a/example_package/script.yaml b/example_package/script.yaml index 7a2aa40..d5016c2 100644 --- a/example_package/script.yaml +++ b/example_package/script.yaml @@ -48,12 +48,12 @@ plot_energy_usage: sequence: - variables: not_defined: > - {{ [ - 'energy sensor' if sensor is not defined or not sensor else none, - 'stop entity' if stop_entity is not defined or not stop_entity else none, - 'stop state' if stop_state is not defined else none - ] | reject('none') | list - }} + {{ [ + 'energy sensor' if sensor is not defined or not sensor else none, + 'stop entity' if stop_entity is not defined or not stop_entity else none, + 'stop state' if stop_state is not defined else none + ] | reject('none') | list + }} - if: "{{ not_defined | count > 0 }}" then: - stop: > diff --git a/example_package/sensor.yaml b/example_package/sensor.yaml index bc6ab46..8a601c8 100644 --- a/example_package/sensor.yaml +++ b/example_package/sensor.yaml @@ -2,6 +2,10 @@ template: - trigger: - platform: event event_type: update_energy_plot + id: plot_update + - platform: homeassistant + event: start + id: remove_incomplete sensor: - unique_id: 36c9491c-2e16-4fc3-bc9f-a6ada5fc88b7 name: Energy plots @@ -9,37 +13,44 @@ template: attributes: energy_plots: > {%- set c = this.attributes.get('energy_plots', {}) -%} - {%- if trigger.event.data is defined -%} + {%- if trigger.id == 'plot_update' and trigger.event.data is defined -%} {%- set st = trigger.event.data.status | default('unknown') -%} - {%- set d = trigger.event.data.description | default('unknown') -%} - {%- if st == 'remove' -%} + {%- set d = trigger.event.data.description | default('unknown') ~ ('(incomplete)' if not st in ['complete', 'remove'] else '') -%} + {%- if st == 'remove_all' -%} + {{ dict() }} + {%- elif st == 'remove' -%} {{ dict(c.items() | rejectattr('0', 'eq', d)) }} {%- else -%} {%- set s = trigger.event.data.state | default(0) -%} {%- set wp = trigger.event.data.no_weight_points | default(none) %} {%- set dt = now().replace(microsecond=0).isoformat() -%} {%- if st == 'first' -%} - {%- set p = {d: dict(data=[], state=s, start=s, no_weight_points=wp, last_update=dt, complete=false)} -%} - {%- elif st == 'complete' -%} - {%- set values = c.get(d, {}) -%} - {%- set data = values.get('data', []) -%} - {%- set u = (values.get('state', 0) - values.get('start', 0)) | round(3) -%} - {%- set no_zero = data | select() | list -%} - {%- set factor = 1 / no_zero | min if no_zero else 0 -%} - {%- set data = data | map('multiply', factor) | map('round', 3) | list -%} - {%- set p = {d: dict(data=data, kwh_used=u, no_weight_points=wp, last_update=dt, complete=true)} -%} + {%- set p = {d: dict(data=[], state=s, start=s, start_time=now().isoformat(), no_weight_points=wp, last_update=dt)} -%} {%- elif st == 'ongoing' -%} {%- set values = c.get(d, {}) -%} {%- set data = values.get('data', []) -%} {%- set u = s - values.get('state', 0) -%} {%- set start = values.get('start', 0) -%} + {%- set start_time = values.get('start_time', now().isoformat()) -%} {%- set data = data + [u | round(3)] -%} - {%- set p = {d: dict(data=data, state=s, start=start, no_weight_points=wp, last_update=dt, complete=false)} -%} - {%- else -%} - {%- set p = {} -%} + {%- set p = {d: dict(data=data, state=s, start=start, start_time=start_time, no_weight_points=wp, last_update=dt)} -%} + {%- elif st == 'complete' -%} + {%- set di = d~'(incomplete)' -%} + {%- set values = c.get(di, {}) -%} + {%- set data = values.get('data', []) -%} + {%- set u = (values.get('state', 0) - values.get('start', 0)) | round(3) -%} + {%- set start_time = values.get('start_time', now().isoformat()) -%} + {%- set duration = (now() - as_datetime(start_time)).total_seconds() | int %} + {%- set no_zero = data | select() | list -%} + {%- set factor = 1 / no_zero | min if no_zero else 0 -%} + {%- set data = data | map('multiply', factor) | map('round', 3) | list -%} + {%- set c = dict(c.items() | rejectattr('0', 'eq', di)) -%} + {%- set p = {d: dict(data=data, kwh_used=u, duration=duration, no_weight_points=wp, last_update=dt)} -%} {%- endif -%} - {{ dict(c, **p) }} + {{ dict(c, **p | default({})) }} {%- endif -%} + {%- elif trigger.id == 'remove_incomplete' -%} + {{ dict(c.items() | rejectattr('0', 'search', '\(incomplete\)$')) }} {%- else -%} {{ c }} {%- endif -%} \ No newline at end of file