Skip to content

Commit

Permalink
Improve script and template sensor (#23)
Browse files Browse the repository at this point in the history
* Completed plots will be recalculated to the lowest consumption in the list as base value (1)
* Added README for script and sensor
* Updated README for macro with some additional information
* Created separate files with script and sensor to make it easier to import them in your configuration
  • Loading branch information
TheFes authored May 8, 2023
1 parent eca2af3 commit a3bb1f7
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 9 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)
![Version](https://img.shields.io/github/v/release/TheFes/cheapest-energy-hours)
[![Buy me a coffe](https://img.shields.io/static/v1.svg?label=%20&message=Buy%20me%20a%20coffee&color=6f4e37&logo=buy%20me%20a%20coffee&logoColor=white)](https://www.buymeacoffee.com/TheFes)

# Cheapest Energy Hours

Expand Down Expand Up @@ -70,7 +71,7 @@ To help getting the weight data from your device, I created a script and a templ
You can start the script manually, or automate it. The data will be stored in a template sensor called `sensor.energy_plots`.
It will survive reboots, so you can refer to the data directly in the template for the macro, but if you store a lot of data it will be ommited from saving in your database automatically. So it might be better to store the list in another entity (an input_text for example) or just copy it in use it directly in the macro.

You can find the script, sensor and an automation example [here](./example_package/package.yaml)
More information on how to use the script and template sensor can be found [here](./example_package/README.md)

### Basic examples

Expand Down Expand Up @@ -116,9 +117,9 @@ To list the prices of the most expesive 5 hour time block

### Advanced examples

For a device shows a higher power usage in the first hour, and last half hour
For a device shows a higher power usage in the first hour, and last half hour. Based on the number of weight points the `hours` will be set to 3 automatically.
```jinja
{{ cheapest_energy_hours('sensor.nordpool_kwh_nl_eur', hours=3, no_weight_points=2, weight=[2 , 4, 1, 1, 1, 5] }}
{{ cheapest_energy_hours('sensor.nordpool_kwh_nl_eur', no_weight_points=2, weight=[2 , 4, 1, 1, 1, 5] }}
```

# Thanks to
Expand Down
66 changes: 66 additions & 0 deletions example_package/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Why this package?

The macro supports providing weight factors in case your device doesn't have a stable energy usage pattern. With this script and template sensor you can plot the weight factors needed for the macro.

# How to install this

You need both the script and the template sensor. The script sends events which are used to store the data in the sensor. You can put it in your configuration as a package, or place the script in your scripts.yaml, and the template sensor in your configuration.yaml.
The [script.yaml](./script.yaml) can be placed in `script.yaml` directly, and [sensor.yaml](./sensor.yaml) in your `configuration.yaml`. In case you already have definded `template:` in `configuration.yaml` make sure to place it under that key, and remove the `template:` line from my code.

# How to use it

Start the script in either an automation, or manually from developer tools > services
You need to provide some details as script variables:
|variable|mandatory|type|default|example|description|
|---|---|---|---|---|---|
|description|no|string|`"unknown"`|`"washing machine"`|Description which will be used as key to for which device the data is plotted|
|sensor|yes|string|`none`|`"sensor.wasmachine_energy"`|The energy sensor used to track the usage of the device|
|no_weight_points|no|integer|`4`|`2`|The number of weight points per hour|
|stop_entity|yes|string|`none`|`sensor.washing_machine`|An entity which is used to define when the script should stop sending data. This can be provided by the device, but can also be a input_boolean or binary_sensor which is eg toggled based on the power usage of the device|
|stop_state|yes|string|`none`|`"off"`|The state the `stop_entity` should be on to stop the script|

Example of the total script service call (using `script.turn_on` in this case)
```yaml
service: script.turn_on
target:
entity_id: script.plot_energy_usage
data:
variables:
description: Washing Machine Cotton 1400rpm
sensor: sensor.washing_machine_energy
no_weight_points: 6
stop_entity: binary_sensor.washing_machine_power_usage
stop_state: "off"
```
# Important to know
* If Home Assistants restarts during the process or the script is stopped because of another reason, it won't be restarted. You'll have to start the process again.
* Plots with the same `description` will be overwritten
* There is a limited number of bytes which is allowed in an attribute. If you store too many plots, you could lose the data

# How to use the plot in the macro
Use the following in developer tools > template to get the data from the sensor. Copy paste that somewhere (or store it in an input_text) to use it later.
```jinja
{{ state_attr('sensor.energy_plots', 'energy_plots')['Your Description'].data }}
```
As long as the data is still in the sensor, you can directly refer to it.
```jinja
{% set w = state_attr('sensor.energy_plots', 'energy_plots')['Your Description'].data %}
{% set wp = state_attr('sensor.energy_plots', 'energy_plots')['Your Description'].no_weight_points %}
{{ cheapest_energy_hours('sensor.nordpool_kwh_nl_eur', no_weight_points=wp, weight=w }}
```

If you store the data in an input_text, or otherwise in a string instead of a list, you need to apply `| from_json` to convert it to a list again
```jinja
{% set w = states('input_text.energy_plot') | from_json %}
{{ cheapest_energy_hours('sensor.nordpool_kwh_nl_eur', no_weight_points=4, weight=w }}
```

# How to remove a plot from the sensor
In developer tools > events, you can send an event to remove a plot. The event type should be `update_energy_plot`
As event_data you need to send the description of the data you want to remove, and `status: remove`
```yaml
description: Washing Machine Cotton 1400rpm
status: remove
```

40 changes: 34 additions & 6 deletions example_package/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,45 @@ script:
selector:
text:
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
}}
- if: "{{ not_defined | count > 0 }}"
then:
- stop: >
{{ not_defined | join(', ') }} {{ 'is' if not_defined | count == 1 else 'are'}} not defined, script has stopped
error: true
- variables:
description: "{{ description | default('unknown') }}"
no_weight_points: "{{ no_weight_points | default(4) | int(4) }}"
- repeat:
until:
- condition: template
value_template: "{{ is_state(stop_entity, stop_state) }}"
sequence:
- variables:
sensor_state: "{{ states(sensor) | float('na') }}"
- if: "{{ not sensor_state | is_number }}"
then:
- stop: "Received non numeric state, script has stopped"
error: true
- event: update_energy_plot
event_data:
description: "{{ description }}"
status: "{{ 'first' if repeat.first else 'ongoing' }}"
state: "{{ states(sensor) | float('na') }}"
state: "{{ sensor_state }}"
no_weight_points: "{{ no_weight_points }}"
- delay:
minutes: "{{ 60 / no_weight_points | int }}"
minutes: "{{ 60 / no_weight_points }}"
- event: update_energy_plot
event_data:
description: "{{ description }}"
no_weight_points: "{{ no_weight_points }}"
status: complete

## TEMPLATE SENSOR TO STORE THE DATA ##
Expand All @@ -81,19 +105,23 @@ template:
{%- set d = trigger.event.data.description | default('unknown') -%}
{%- set s = trigger.event.data.state | default(0) -%}
{%- set st = trigger.event.data.status | default('unknown') -%}
{%- set wp = trigger.event.data.no_weight_points | default(none) %}
{%- if st == 'remove' -%}
{{ dict(c.items() | rejectattr('0', 'eq', d)) }}
{%- else -%}
{%- if st == 'first' -%}
{%- set p = {d: dict(data=[], complete=false, state=s)} -%}
{%- set p = {d: dict(data=[], state=s, no_weight_points=wp, complete=false)} -%}
{%- elif st == 'complete' -%}
{%- set data = c.get(d, {}).get('data', []) -%}
{%- set p = {d: dict(data=data, complete=true)} -%}
{%- 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, no_weight_points=wp, complete=true)} -%}
{%- elif st == 'ongoing' -%}
{%- set data = c.get(d, {}).get('data', []) -%}
{%- set u = s - c[d].state -%}
{%- set u = s - c.get(d, {}).get('state', 0) -%}
{%- set data = data + [u | round(3)] -%}
{%- set p = {d: dict(data=data, complete=false, state=s)} -%}
{%- set p = {d: dict(data=data, state=s, no_weight_points=wp, complete=false)} -%}
{%- else -%}
{%- set p = {} -%}
{%- endif -%}
Expand Down
88 changes: 88 additions & 0 deletions example_package/script.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
plot_energy_usage:
alias: "00 - Plot energy usage"
icon: mdi:brightness-percent
mode: parallel
max: 20
max_exceeded: silent
fields:
sensor:
description: "Select energy sensor"
name: Energy Sensor
example: sensor.dishwasher_energy
required: true
selector:
entity:
domain: sensor
device_class: energy
description:
description: "Description for the plot"
name: Description
example: 120
required: true
selector:
text:
no_weight_points:
description: "Number of weight points/hour"
name: Weight points
default: 1
example: 3
required: true
selector:
number:
min: 0
max: 12
step: 1
mode: slider
stop_entity:
description: "Entity to use to stop the plot"
example: input_boolean.dishwasher_off
required: true
selector:
entity:
stop_state:
description: "State the entity should have to stop the plot"
example: "off"
required: true
selector:
text:
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
}}
- if: "{{ not_defined | count > 0 }}"
then:
- stop: >
{{ not_defined | join(', ') }} {{ 'is' if not_defined | count == 1 else 'are'}} not defined, script has stopped
error: true
- variables:
description: "{{ description | default('unknown') }}"
no_weight_points: "{{ no_weight_points | default(4) | int(4) }}"
- repeat:
until:
- condition: template
value_template: "{{ is_state(stop_entity, stop_state) }}"
sequence:
- variables:
sensor_state: "{{ states(sensor) | float('na') }}"
- if: "{{ not sensor_state | is_number }}"
then:
- stop: "Received non numeric state, script has stopped"
error: true
- event: update_energy_plot
event_data:
description: "{{ description }}"
status: "{{ 'first' if repeat.first else 'ongoing' }}"
state: "{{ sensor_state }}"
no_weight_points: "{{ no_weight_points }}"
- delay:
minutes: "{{ 60 / no_weight_points }}"
- event: update_energy_plot
event_data:
description: "{{ description }}"
no_weight_points: "{{ no_weight_points }}"
status: complete
40 changes: 40 additions & 0 deletions example_package/sensor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
template:
- trigger:
- platform: event
event_type: update_energy_plot
sensor:
- unique_id: 36c9491c-2e16-4fc3-bc9f-a6ada5fc88b7
name: Energy plots
state: OK
attributes:
energy_plots: >
{%- set c = this.attributes.get('energy_plots', {}) -%}
{%- if trigger.event.data is defined -%}
{%- set d = trigger.event.data.description | default('unknown') -%}
{%- set s = trigger.event.data.state | default(0) -%}
{%- set st = trigger.event.data.status | default('unknown') -%}
{%- set wp = trigger.event.data.no_weight_points | default(none) %}
{%- if st == 'remove' -%}
{{ dict(c.items() | rejectattr('0', 'eq', d)) }}
{%- else -%}
{%- if st == 'first' -%}
{%- set p = {d: dict(data=[], state=s, no_weight_points=wp, complete=false)} -%}
{%- elif st == 'complete' -%}
{%- set data = c.get(d, {}).get('data', []) -%}
{%- 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, no_weight_points=wp, complete=true)} -%}
{%- elif st == 'ongoing' -%}
{%- set data = c.get(d, {}).get('data', []) -%}
{%- set u = s - c.get(d, {}).get('state', 0) -%}
{%- set data = data + [u | round(3)] -%}
{%- set p = {d: dict(data=data, state=s, no_weight_points=wp, complete=false)} -%}
{%- else -%}
{%- set p = {} -%}
{%- endif -%}
{{ dict(c, **p) }}
{%- endif -%}
{%- else -%}
{{ c }}
{%- endif -%}

0 comments on commit a3bb1f7

Please sign in to comment.