Skip to content

Instantly share code, notes, and snippets.

@jlongman
Last active December 15, 2024 20:09
Show Gist options
  • Save jlongman/90492d3deb7cc06e4f1d3dc6ea9c1723 to your computer and use it in GitHub Desktop.
Save jlongman/90492d3deb7cc06e4f1d3dc6ea9c1723 to your computer and use it in GitHub Desktop.
HomeAssistant and Montreal Rink Conditions

Adding YOUR Montreal hockey rink

Ability to see the state and condition of ice rinks in Montreal.

Screenshot 2024-12-15 at 2 22 46 PM

The left is a typical rink widget, the right has an additional row for debugging.

Setup

  • If you don't already have one, create a directory custom_templates at the same level as your configuration.yaml file.
  • copy rinks.ninja into the custom_templates directory

Find your rink(s)

There are other tools to find nearby rinks but if you know the name you can find it in the XML file:

http://www2.ville.montreal.qc.ca/services_citoyens/pdf_transfert/L29_PATINOIRE.xml

Different rinks at the same facility will have distinctions, like PPL and PSE for patin libre and sports, respectively. PPL will not have boards, PSE is for hockey with boards. Two rinks at the same parc may not be in the same condition.

Set up the rest api

This selects which rinks will be imported from the XML file.

In configuration.yaml if you don't already have a rest stanza add one as following. If you already have one, append it to the existing entries as new list item under rest. A scan interval of 90000 (seconds) is about 25 hours - which is a long, very-safe/generous time interval. There's probably not much benefit to checking too often and I wouldn't go below 1800s/30 minutes.

rest:
  - scan_interval: 90000
    resource: http://www2.ville.montreal.qc.ca/services_citoyens/pdf_transfert/L29_PATINOIRE.xml
    sensor:

Now in the sensor add for each rink a stanza that looks like:

      - name: "rinks_mtl_ndg_gsp_pse"
        value_template: >
          {% from 'rinks.jinja' import compose_rink_conditions %}
          {% set rink_name = '.*Georges-Saint-Pierre.*PSE.*' %}
          {{ compose_rink_conditions( value_json.patinoires.patinoire | selectattr('nom', 'match', rink_name) | list | first ) }}

Each name must be unique and you'll use it later for the break out sensors.

Customize the rink_name with an expression that selects your rink. The value is a regular expression where .* is a wildcard for any character. Only the first rink will be selected if your wildcards grab more than one.

If you restart HA now and your configuration above is correct you will be able to see the composite entity which is decomposed into each sensor that represents the state of the rink. You won't yet be able to add these to a card.

Set up the template sensors for each rink

This translates the composite value imported into the separate sensors which are visualizable. At least originally when this was written it could not be done in one step.

Now for each rink you must add 6 template sensors. In the template section of configuration.yaml this can be added as a template:

template:
  - sensor:
      - unique_id: "rink-???-???-m"
        name: "Rink ??? ??? Mise-a-Jour"
        state: >
          {% from 'rinks.jinja' import decompose_rink_last_update %}
          {{ decompose_rink_last_update('sensor.rinks_mtl_???') }}
        device_class: "timestamp"

  - sensor:
      - unique_id: "rink-???-???-c"
        name: "Rink ??? ??? Condition"
        state: >
          {% from 'rinks.jinja' import decompose_rink_conditions %}
          {{ decompose_rink_conditions('sensor.rinks_mtl_???') }}
  - binary_sensor:
      - unique_id: "rink-???-???-o"
        name: "Rink ??? ??? Ouvert"
        state: >
          {% from 'rinks.jinja' import decompose_rink_open %}
          {{ decompose_rink_open('sensor.rinks_mtl_???') }}
        icon: >
          {% from 'rinks.jinja' import decompose_rink_open_icon %}
          {{ decompose_rink_open_icon(states(this.entity_id)) }}

  - binary_sensor:
      - unique_id: "rink-???-???-a"
        name: "Rink ??? ??? Arrose"
        state: >
          {% from 'rinks.jinja' import decompose_rink_flooded %}
          {{ decompose_rink_flooded('sensor.rinks_mtl_???') }}
        icon: >
          {% from 'rinks.jinja' import decompose_rink_flooded_icon %}
          {{ decompose_rink_flooded_icon(states(this.entity_id)) }}

  - binary_sensor:
      - unique_id: "rink-???-???-d"
        name: "Rink ??? ??? Deblaye"
        state: >
          {% from 'rinks.jinja' import decompose_rink_cleared %}
          {{ decompose_rink_cleared('sensor.rinks_mtl_???') }}
        icon: >
          {% from 'rinks.jinja' import decompose_rink_cleared_icon %}
          {{ decompose_rink_cleared_icon(states(this.entity_id)) }}
  - binary_sensor:
      - unique_id: "rink-???-???-r"
        name: "Rink ??? ??? Resurface"
        state: >
          {% from 'rinks.jinja' import decompose_rink_resurface %}
          {{ decompose_rink_resurface('sensor.rinks_mtl_???') }}
        icon: >
          {% from 'rinks.jinja' import decompose_rink_resurface_icon %}
          {{ decompose_rink_resurface_icon(states(this.entity_id)) }}

Note: each unique_id and name must be unique! The names don't matter but I found something semi-structured like above help me keep order.

Replace the question marks with values that make sense. Being consistent will make changes later easier.

I'd recommend using search and replace to replace the following terms:

  • for the unique_id field
    • search Rink ??? ???
    • replace Rink YOURVALUEHERE with your designation
  • for the name field:
    • search rink-???-???-
    • replace rink-YOURVALUEHERE-
  • the original rest sensor name in the decompose_ line
    • note this value must come from the rest section
    • search sensor.rinks_mtl_???
    • replace with the name value from rest section.

It might help to do each template for each rink in a separate text editor and then paste it in complete into the configuration.yaml. (There might be a clever way to compose files?)

Finishing the sensors

Once these are all entered you can restart HA. I haven't found a faster way to iterate here.

Once restarted you can check the individual entities in the appropriate HA Settings page. Searching by rink usually does the trick.

The rest integration sensors should look like some variation on 0|N/A|0|0|0|2024-12-15 14:17:14-05:00, the names are the ones you set for them. The template sensors should be broken out appropriately and have icons shown in the settings list.

You're now ready to add the UI elements.

Adding the UI

Edit the dashboard you want to add the card to and click Add Card and the at the bottom of the choices choose Manual.

Delete the existing lines and paste this template into the text box:

type: entities
entities:
  - entity: sensor.???
    name: Condition
    secondary_info: last-changed
  - entity: sensor.rink_???_???_mise_a_jour
    name: " City Updated"
  - entity: binary_sensor.rink_???_???_ouvert
    name: Open
  - entity: binary_sensor.rink_???_???_deblaye
    name: Cleared
  - entity: binary_sensor.rink_???_???_resurface
    name: Resurfaced
  - entity: binary_sensor.rink_???_???_arrose
    name: Flooded
title: ??? 
state_color: true

and then similarly replace the question marks with values from the template section.

rest:
- scan_interval: 90000
resource: http://www2.ville.montreal.qc.ca/services_citoyens/pdf_transfert/L29_PATINOIRE.xml
sensor:
- name: "rinks_mtl_ndg_gsp_pse"
value_template: >
{% from 'rinks.jinja' import compose_rink_conditions %}
{% set rink_name = '.*Georges-Saint-Pierre.*PSE.*' %}
{{ compose_rink_conditions( value_json.patinoires.patinoire | selectattr('nom', 'match', rink_name) | list | first ) }}
- name: "rinks_mtl_ndg_gsp_ppl"
value_template: >
{% from 'rinks.jinja' import compose_rink_conditions %}
{% set rink_name = '.*Georges-Saint-Pierre.*PPL.*' %}
{{ compose_rink_conditions( value_json.patinoires.patinoire | selectattr('nom', 'match', rink_name) | list | first ) }}
template:
- sensor:
- unique_id: "rink-gsp-pse-m"
name: "Rink GSP PSE Mise-a-Jour"
state: >
{% from 'rinks.jinja' import decompose_rink_last_update %}
{{ decompose_rink_last_update('sensor.rinks_mtl_ndg_gsp_pse') }}
device_class: "timestamp"
- sensor:
- unique_id: "rink-gsp-pse-c"
name: "Rink GSP PSE Condition"
state: >
{% from 'rinks.jinja' import decompose_rink_conditions %}
{{ decompose_rink_conditions('sensor.rinks_mtl_ndg_gsp_pse') }}
- binary_sensor:
- unique_id: "rink-gsp-pse-o"
name: "Rink GSP PSE Ouvert"
state: >
{% from 'rinks.jinja' import decompose_rink_open %}
{{ decompose_rink_open('sensor.rinks_mtl_ndg_gsp_pse') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_open_icon %}
{{ decompose_rink_open_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-pse-a"
name: "Rink GSP PSE Arrose"
state: >
{% from 'rinks.jinja' import decompose_rink_flooded %}
{{ decompose_rink_flooded('sensor.rinks_mtl_ndg_gsp_pse') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_flooded_icon %}
{{ decompose_rink_flooded_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-pse-d"
name: "Rink GSP PSE Deblaye"
state: >
{% from 'rinks.jinja' import decompose_rink_cleared %}
{{ decompose_rink_cleared('sensor.rinks_mtl_ndg_gsp_pse') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_cleared_icon %}
{{ decompose_rink_cleared_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-pse-r"
name: "Rink GSP PSE Resurface"
state: >
{% from 'rinks.jinja' import decompose_rink_resurface %}
{{ decompose_rink_resurface('sensor.rinks_mtl_ndg_gsp_pse') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_resurface_icon %}
{{ decompose_rink_resurface_icon(states(this.entity_id)) }}
- sensor:
- unique_id: "rink-gsp-ppl-m"
name: "Rink GSP PPL Mise-a-Jour"
state: >
{% from 'rinks.jinja' import decompose_rink_last_update %}"
{{ decompose_rink_last_update('sensor.rinks_mtl_ndg_gsp_ppl') }}
device_class: "timestamp"
- sensor:
- unique_id: "rink-gsp-ppl-c"
name: "Rink GSP PPL Condition"
state: >
{% from 'rinks.jinja' import decompose_rink_conditions %}
{{ decompose_rink_conditions('sensor.rinks_mtl_ndg_gsp_ppl') }}
- binary_sensor:
- unique_id: "rink-gsp-ppl-a"
name: "Rink GSP PPL Arrose"
state: >
{% from 'rinks.jinja' import decompose_rink_flooded %}
{{ decompose_rink_flooded('sensor.rinks_mtl_ndg_gsp_ppl') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_flooded_icon %}
{{ decompose_rink_flooded_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-ppl-o"
name: "Rink GSP PPL Ouvert"
state: >
{% from 'rinks.jinja' import decompose_rink_open %}
{{ decompose_rink_open('sensor.rinks_mtl_ndg_gsp_ppl') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_open_icon %}
{{ decompose_rink_open_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-ppl-d"
name: "Rink GSP PPL Deblaye"
state: >
{% from 'rinks.jinja' import decompose_rink_cleared %}
{{ decompose_rink_cleared('sensor.rinks_mtl_ndg_gsp_ppl') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_cleared_icon %}
{{ decompose_rink_cleared_icon(states(this.entity_id)) }}
- binary_sensor:
- unique_id: "rink-gsp-ppl-r"
name: "Rink GSP PPL Resurface"
state: >
{% from 'rinks.jinja' import decompose_rink_resurface %}
{{ decompose_rink_resurface('sensor.rinks_mtl_ndg_gsp_ppl') }}
icon: >
{% from 'rinks.jinja' import decompose_rink_resurface_icon %}
{{ decompose_rink_resurface_icon(states(this.entity_id)) }}
# This needs to be in a folder, peer level to `configuration.yaml`, called `custom_templates`.
# It may need to be created.
{% macro compose_rink_conditions(items) %}
{% set arr = items.arrondissement %}
{% set arr_maj = arr.date_maj + "-05:00" %}
{% set attributes = ['arrose', 'condition', 'deblaye', 'ouvert', 'resurface'] %}
{% set interim = items.items() | selectattr(0, 'in', attributes) | sort(attribute=0) | map(attribute=1) | join('|') %}
{{ [interim, arr_maj] | join('|') }}
{% endmacro %}
{% macro decompose_rink_last_update(entity_id) %}
{{ states(entity_id).split('|')[5] }}
{% endmacro %}
{% macro decompose_rink_conditions(entity_id) %}
{{ states(entity_id).split('|')[1] }}
{% endmacro %}
{% macro decompose_rink_open(entity_id) %}
{{ states(entity_id).split('|')[3] == '1' }}
{% endmacro %}
{% macro decompose_rink_open_icon(entity_id) %}
{% if entity_id == "on" %}
mdi:skate
{% else %}
mdi:skate-off
{% endif %}
{% endmacro %}
{% macro decompose_rink_flooded(entity_id) %}
{{ states(entity_id).split('|')[0] == '1' }}
{% endmacro %}
{% macro decompose_rink_flooded_icon(entity_id) %}
{% if entity_id == "on" %}
mdi:water
{% else %}
mdi:water-off
{% endif %}
{% endmacro %}
{% macro decompose_rink_cleared(entity_id) %}
{{ states(entity_id).split('|')[2] == '1' }}
{% endmacro %}
{% macro decompose_rink_cleared_icon(entity_id) %}
{% if entity_id == "on" %}
mdi:snowflake
{% else %}
mdi:snowflake-off
{% endif %}
{% endmacro %}
{% macro decompose_rink_resurface(entity_id) %}
{{ states(entity_id).split('|')[4] == '1' }}"
{% endmacro %}
{% macro decompose_rink_resurface_icon(entity_id) %}
{% if entity_id == "on" %}
mdi:layers
{% else %}
mdi:layers-off
{% endif %}
{% endmacro %}
type: entities
entities:
- entity: sensor.rink_gsp_ppl_condition
name: Condition
secondary_info: last-changed
- entity: sensor.rink_gsp_ppl_mise_a_jour
name: " City Updated"
- entity: binary_sensor.rink_gsp_ppl_ouvert
name: Open
- entity: binary_sensor.rink_gsp_ppl_deblaye
name: Cleared
- entity: binary_sensor.rink_gsp_ppl_resurface
name: Resurfaced
- entity: binary_sensor.rink_gsp_ppl_arrose
name: Flooded
title: GSP Free Skate
state_color: true
type: entities
entities:
- entity: sensor.rink_gsp_pse_condition
name: Condition
secondary_info: last-changed
- entity: sensor.rink_gsp_pse_mise_a_jour
name: City Updated
- entity: binary_sensor.rink_gsp_pse_ouvert
name: Open
- entity: binary_sensor.rink_gsp_pse_deblaye
name: Cleared
- entity: binary_sensor.rink_gsp_pse_resurface
name: Resurfaced
- entity: binary_sensor.rink_gsp_pse_arrose
name: Flooded
title: GSP Hockey rink
state_color: true
@jlongman
Copy link
Author

Screenshot 2024-12-15 at 2 22 46 PM
  • GSP shows the card as above in the UI YAML
  • Toussaint has an extra line which helps me debug (and is currently in better condition than the GSP rink)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment