I couldn’t find a good way to display the information provided natively by my local transit authority’s integration, and after looking at a few examples from other people, I couldn’t find any I really liked. So I’ve spent a little time today working on my own. It’s still a WIP so I’ll solicit specific advice and feedback in the replies, but wanted to dedicate the first post to sharing what I have so far.
These cards require two existing custom components:
The data from PTV is quite messy and needs to be parsed to remove a bit of junk, and formatted into a form more suitable for humans. There are three bits of code I’m working on to accomplish this:
- A reusable Jinja template, to prevent duplicating several lines of code
- Template sensors, to create a sensor for each stop
- Button card code, to present it all nicely
Jinja Template
Saved in config/custom_templates/process_ptv.jinja
{%-macro process_ptv(attribute, stop_string) -%}
{%-set stop_string = "sensor." + stop_string + "_" -%}
{%- set ns = namespace (ids = [], times = [], next_service_time = "", next_service_id = "", following_service_time = "", following_service_id = "", skip = false) -%}
{%- for index in range(0,5) -%}
{%- if state_attr(stop_string + index | string, 'run_id' ) != -1 -%}
{%- set ns.ids = ns.ids + [state_attr( stop_string + index | string(), 'run_id' )] -%}
{%- else -%}
{%- set ns.ids = ns.ids + [state_attr( stop_string + index | string(), 'run_ref' )] -%}
{%- endif -%}
{%- endfor -%}
{%- for index in range(0,5) -%}
{%- if state_attr(stop_string + index | string, 'estimated_departure_utc' ) != none -%}
{%- set ns.times = ns.times + [strptime(state_attr(stop_string + index | string, 'estimated_departure_utc' ) + "+0000", "%Y-%m-%dT%H:%M:%SZ%z" )] -%}
{%- else -%}
{%- set ns.times = ns.times + [strptime(state_attr(stop_string + index | string, 'scheduled_departure_utc' ) + "+0000", "%Y-%m-%dT%H:%M:%SZ%z" )] -%}
{%- endif -%}
{%- endfor -%}
{%- set ns.skip = false -%}
{%- for index in range(0,5) -%}
{%- if ns.skip == false -%}
{%- if ns.times[index] > utcnow() -%}
{%- set ns.skip = true -%}
{%- endif -%}
{%- set ns.next_service_time = ns.times[index] -%}
{%- set ns.next_service_id = ns.ids[index] -%}
{%- endif -%}
{%- endfor -%}
{%- set ns.skip = false -%}
{%- for index in range(1,5) -%}
{%- if ns.skip == false -%}
{%- if ns.times[index] > ns.next_service_time and ns.ids[index] != ns.next_service_id -%}
{%- set ns.skip = true -%}
{%- endif -%}
{%- set ns.following_service_time = ns.times[index] -%}
{%- set ns.following_service_id = ns.ids[index] -%}
{%- endif -%}
{%- endfor -%}
{%- if attribute == "next_service_time" -%}
{{ ns.next_service_time }}
{%- elif attribute == "next_service_id" -%}
{{ ns.next_service_id }}
{%- elif attribute == "following_service_time" -%}
{{ ns.following_service_time }}
{%- elif attribute == "following_service_id" -%}
{{ ns.following_service_id }}
{%- else -%}
Unknown Attribute
{%-endif -%}
{%-endmacro -%}
Template Sensor
This sensor must be duplicated for each stop you want to display.
For PTV users, replace stop_string
with the sensor name for the stop added by the custom integration. Remove the sensor.
from the beginning, and the _0
/_1
/etc from the end. The template will iterate over the five individual sensors the integration creates for each stop. The integration will often continue to display services service has departed, and duplicates of a single service. The template will eliminate both of these to determine the next two true services.
For non-PTV users, you could still use the button card by creating your own sensor that has datetime strings for the attributes shown here.
template:
sensor:
- name: "My Service Name"
state: >
{% from 'process_ptv.jinja' import process_ptv %}
{{ process_ptv('next_service_time','stop_string')}}
attributes:
next_service_time: >
{% from 'process_ptv.jinja' import process_ptv %}
{{ process_ptv('next_service_time','stop_stringion')}}
next_service_id: >
{% from 'process_ptv.jinja' import process_ptv %}
{{ process_ptv('next_service_id','stop_string')}}
following_service_time: >
{% from 'process_ptv.jinja' import process_ptv %}
{{ process_ptv('following_service_time','stop_string')}}
following_service_id: >
{% from 'process_ptv.jinja' import process_ptv %}
{{ process_ptv('following_service_id','stop_string')}}
Button Card
Simply replace the two sensor IDs to show the correct information for each stop. Optionally, the icon, colour and text size can be adjusted too.
type: custom:button-card
icon: mdi:train
name: Route 1
styles:
card:
- background-color: '#028430'
- padding: 2% 3%
- color: ivory
- font-size: 12px
- font-weight: bold
grid:
- grid-template-areas: '"i n next_title following_title" "i n next_value following_value"'
- grid-template-columns: min-content 1fr min-content min-content
- grid-template-rows: min-content min-content
name:
- font-weight: bold
- font-size: 30px
- color: white
- align-self: middle
- justify-self: start
- padding: 0px
icon:
- color: white
- width: 50px
- padding: 0px 12px 0px 0px
custom_fields:
next_title:
- text-align: left
- justify-self: start
next_value:
- padding-bottom: 0px
- padding-right: 10px
- text-align: left
- justify-self: null
- padding-top: |
[[[
const service_time = new Date(states['sensor.test_transit_1'].attributes.next_service_time);
const now = new Date();
const difference = service_time - now;
const millisecondsInMinute = 1000 * 60;
const minutes = Math.ceil(difference / millisecondsInMinute);
if (minutes <=60) {
return '<0px';
} else {
return '5px';
}
]]]
following_title:
- text-align: left
following_value:
- text-align: left
- padding-top: |
[[[
const service_time = new Date(states['sensor.test_transit_1'].attributes.following_service_time);
const now = new Date();
const difference = service_time - now;
const millisecondsInMinute = 1000 * 60;
const minutes = Math.ceil(difference / millisecondsInMinute);
if (minutes <=60) {
return '<0px';
} else {
return '5px';
}
]]]
custom_fields:
next_title: 'Next:'
next_value: |
[[[
const service_time = new Date(states['sensor.test_transit_1'].attributes.next_service_time);
const now = new Date();
const difference = service_time - now;
const millisecondsInMinute = 1000 * 60;
const minutes = Math.ceil(difference / millisecondsInMinute);
if (minutes <=60) {
return '<span style="font-size:40px;">' + minutes + '</span><span style="font-size:14px;"> min</span>';
} else {
return '<span style="font-size:35px; padding-top: 5px">' + helpers.formatTime24h(service_time) + '</span>';
}
]]]
following_title: 'Following:'
following_value: |
[[[
const service_time = new Date(states['sensor.test_transit_1'].attributes.following_service_time);
const now = new Date();
const difference = service_time - now;
const millisecondsInMinute = 1000 * 60;
const minutes = Math.ceil(difference / millisecondsInMinute);
if (minutes <=60) {
return '<span style="font-size:40px;">' + minutes + '</span><span style="font-size:14px;"> min</span>';
} else {
return '<span style="font-size:35px; padding:115px 0px 0px 0px;">' + helpers.formatTime24h(service_time) + '</span>';
}
]]]