Sorry for the double post, but I managed to get this done by using ChatGPT 4.0. Here’s how I’ve set everything up now.
I have three EPEX Spot Sensors
- binary_sensor.30deg_batch_cheapest_window
- binary_sensor.60deg_batch_cheapest_window
- binary_sensor.dishwasher_cheapest_window
I created three Template Sensor in the Helpers section using the UI for calculating the duration, but also taking only the future time into account.
sensor.dishwasher_start_duration
{% set data = state_attr('binary_sensor.dishwasher_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('start_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set start_time = strptime(next_window['start_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{% set time_to_start = (start_time - now).total_seconds() %}
{% set hours = (time_to_start // 3600) | int %}
{% set minutes = ((time_to_start % 3600) // 60) | int %}
{% set time_str = '{:02}:{:02}'.format(hours, minutes) %}
{{ time_str }}
{% else %}
'No upcoming start'
{% endif %}
sensor.next_30deg_batch_end_duration
{% set data = state_attr('binary_sensor.30deg_batch_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('end_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set end_time = strptime(next_window['end_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{% set time_to_end = (end_time - now).total_seconds() %}
{% set hours = (time_to_end // 3600) | int %}
{% set minutes = ((time_to_end % 3600) // 60) | int %}
{% set time_str = '{:02}:{:02}'.format(hours, minutes) %}
{{ time_str }}
{% else %}
Waiting for new data.
{% endif %}
sensor.next_60deg_batch_end_duration
{% set data = state_attr('binary_sensor.60deg_batch_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('end_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set end_time = strptime(next_window['end_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{% set time_to_end = (end_time - now).total_seconds() %}
{% set hours = (time_to_end // 3600) | int %}
{% set minutes = ((time_to_end % 3600) // 60) | int %}
{% set time_str = '{:02}:{:02}'.format(hours, minutes) %}
{{ time_str }}
{% else %}
Waiting for new data.
{% endif %}
I then made three Template Sensor for reporting only the future times.
sensor.dishwasher_start_time
{% set data = state_attr('binary_sensor.dishwasher_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('start_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set start_time = strptime(next_window['start_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{{ start_time.strftime('%H:%M on %d/%m/%y') }}
{% else %}
Waiting for new data
{% endif %}
sensor.next_30deg_batch_end_time
{% set data = state_attr('binary_sensor.30deg_batch_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('end_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set end_time = strptime(next_window['end_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{{ end_time.strftime('%H:%M on %d/%m/%y') }}
{% else %}
Waiting for new data.
{% endif %}
sensor.next_60deg_batch_end_time
{% set data = state_attr('binary_sensor.60deg_batch_cheapest_window', 'data') %}
{% set now = now() %}
{% set future_windows = data | selectattr('end_time', '>', now.timestamp() | timestamp_local) | list %}
{% if future_windows %}
{% set next_window = future_windows | first %}
{% set end_time = strptime(next_window['end_time'], '%Y-%m-%dT%H:%M:%S%z') %}
{{ end_time.strftime('%H:%M on %d/%m/%y') }}
{% else %}
Waiting for new data.
{% endif %}
Next, I needed to install Card Tools & Secondary Info entity Row via HACS.
Finally, I created an Entites Card in the dashboard with this code
type: entities
entities:
- entity: sensor.dishwasher_start_duration
name: Dishwasher Start
icon: mdi:dishwasher
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.dishwasher_start_time ]]'
- entity: sensor.next_30deg_batch_end_duration
name: 30° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_30deg_batch_end_time ]]'
- entity: sensor.next_60deg_batch_end_duration
name: 60° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_60deg_batch_end_time ]]'
title: Appliance Schedules
state_color: true
show_header_toggle: false
And this is the final result
As a side-note, I am quite mind-blown right now with how well ChatGPT guided me, a complete HomeAssistant noob, through the process. I primarily gave it screenshots and it parsed everything VERY well. All the code you see here was written by ChatGPT, with very minor tweaks from me (eg changing variable names correcting capitalisation and other minor cosmetic changes).
EDIT: I found a Home Assistant Assistant GPT and used it to make this card conditional.
Originally, if the data for the following day hadn't been updated, then the entities would simply show "Waiting for new data":
I figured that it didn't make much sense to display the entity if there wasn't any data yet, so I asked GPT to hide it for me
This is what it coded
type: entities
title: Appliance Schedules
state_color: true
show_header_toggle: false
entities:
- type: conditional
conditions:
- entity: sensor.dishwasher_start_duration
state_not: "Waiting for new data"
row:
entity: sensor.dishwasher_start_duration
name: Dishwasher Start
icon: mdi:dishwasher
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.dishwasher_start_time ]]'
- type: conditional
conditions:
- entity: sensor.next_30deg_batch_end_duration
state_not: "Waiting for new data"
row:
entity: sensor.next_30deg_batch_end_duration
name: 30° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_30deg_batch_end_time ]]'
- type: conditional
conditions:
- entity: sensor.next_60deg_batch_end_duration
state_not: "Waiting for new data"
row:
entity: sensor.next_60deg_batch_end_duration
name: 60° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_60deg_batch_end_time ]]'
However, the result was (expectedly) an empty card with just the heading
I had no idea that state_not
was even a possibility.
I decided that if the entire card is empty, might as well just hide it
This is the final YAML for the card now:
type: vertical-stack
cards:
- type: conditional
conditions:
- entity: sensor.dishwasher_start_duration
state_not: "Waiting for new data"
- entity: sensor.next_30deg_batch_end_duration
state_not: "Waiting for new data"
- entity: sensor.next_60deg_batch_end_duration
state_not: "Waiting for new data"
card:
type: entities
title: Appliance Schedules
state_color: true
show_header_toggle: false
entities:
- type: conditional
conditions:
- entity: sensor.dishwasher_start_duration
state_not: "Waiting for new data"
row:
entity: sensor.dishwasher_start_duration
name: Dishwasher Start
icon: mdi:dishwasher
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.dishwasher_start_time ]]'
- type: conditional
conditions:
- entity: sensor.next_30deg_batch_end_duration
state_not: "Waiting for new data"
row:
entity: sensor.next_30deg_batch_end_duration
name: 30° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_30deg_batch_end_time ]]'
- type: conditional
conditions:
- entity: sensor.next_60deg_batch_end_duration
state_not: "Waiting for new data"
row:
entity: sensor.next_60deg_batch_end_duration
name: 60° Batch End
icon: mdi:washing-machine
type: custom:secondaryinfo-entity-row
secondary_info: '[[ sensor.next_60deg_batch_end_time ]]'
Big thanks to @webwude for the inspiration - I did not know that this was even a possibility till I saw his type : conditional
card!
I love how much of a help an LLM has been through this process - it has been great for learning and I think that it can definitely lower the barrier of entry for people like me who may not be complete coding noobs, but are absolutely not programmers, and don’t have enough time/resources to sit down and scour through loads of documentation. 🩵