Rest sensor for school vacations openholidaysapi

Hello everyone,

I am quite new to Home Assistant and am currently tearing my hair out regarding the correct configuration of a REST sensor to read out the current or next school vacations for Germany from www.openholidaysapi.org.

What I have so far:

  • in sensors.yaml
- platform: rest
  scan_interval: 3600
  name: schulferien_bw_next_json
  unique_id: sensor.schulferien_bw_next_json
  icon: mdi:calendar-cursor
  resource_template: >-
    https://openholidaysapi.org/SchoolHolidays?countryIsoCode=DE&languageIsoCode=DE&subdivisionCode=DE-BW&validFrom={{(now()).strftime('%Y-%m-%d')}}&validTo={{(now()+timedelta(days=120)).strftime('%Y-%m-%d')}}
  value_template: >-
    {{value_json[0]|to_json|string}}

This fills sensor.schulferien_bw_next_json with the following:

{"id":"7c71dc62-dfe0-43e0-9d56-4599da11f959","startDate":"2024-03-23","endDate":"2024-04-05","type":"School","name":[{"language":"DE","text":"Osterferien"}],"nationwide":false,"subdivisions":[{"code":"DE-BW","shortName":"BW"}]}

or prettified:

{
    "id": "7c71dc62-dfe0-43e0-9d56-4599da11f959",
    "startDate": "2024-03-23",
    "endDate": "2024-04-05",
    "type": "School",
    "name": [
        {
            "language": "DE",
            "text": "Osterferien"
        }
    ],
    "nationwide": false,
    "subdivisions": [
        {
            "code": "DE-BW",
            "shortName": "BW"
        }
    ]
}

As you can see, one of the most interessting entries, the name of the vacation - here “Osterferien” - is nested in name[0].text, while other important entries such as start and end are one level above.

What I want to achieve is one entity, which is a boolean:

  • true, if we have vacation actually
  • false, if not
  • all interesting entries as well as some calculated entries should be added as attributes to this sensor:
    • name
    • duration
    • begins: Start datetime of the vacation
    • days_until_start: Count of days until start. Zero if we have vacation actually
    • end: End datetime of the vacation
    • days_remaining: Count of days until end, if we have vacation actually. Zero, if we have no vacation actually.
    • updated: datetime of last update

I tried to achieve this with the following template in templates.yaml:

- trigger:
    - platform: state
      entity_id:
        - sensor.schulferien_bw_next_json
  action: []
  binary_sensor:
    # Schulferien?
    name: schulferien_bw_next
    unique_id: binary_sensor.schulferien_bw_next
    icon: >-
      {% set status = states('binary_sensor.schulferien_bw_next') %}
      {% if status in ['off', 'unknown', 'unavailable'] %}
        mdi:school
      {% else %}
        mdi:calendar-star
      {% endif %}
    state: >-
      {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
      {% if ( (as_timestamp(strptime(value_json.startDate, "%Y-%m-%d"))) <= (as_timestamp(now())) ) %}
        true
      {% else %}
        false
      {% endif %}
    attributes:
      name: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {{ value_json.name[0].text }}
      duration: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {{ ( ( (as_timestamp(strptime(value_json.endDate, "%Y-%m-%d"))) - (as_timestamp(strptime(value_json.startDate, "%Y-%m-%d"))) ) | timestamp_custom("%d") ) | float() | int }}
      begins: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {{ strptime(value_json.startDate, "%Y-%m-%d").isoformat()}}
      days_until_start: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {% if ( (as_timestamp(strptime(value_json.startDate, "%Y-%m-%d"))) > (as_timestamp(now())) ) %}
          {{ ( ( ( (as_timestamp(strptime(value_json.startDate, "%Y-%m-%d"))) - (as_timestamp(now())) ) ) | timestamp_custom("%d") ) | float() | int }}
        {% else %}
          0
        {% endif %}
      ends: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {{ (((strptime(value_json.endDate, "%Y-%m-%d"))) + timedelta( hours = 23, minutes = 59, seconds = 59)).isoformat() }}
      days_remaining: >-
        {% set value_json = states('sensor.schulferien_bw_next_json')|from_json %}
        {% if ( ( (as_timestamp(now())) > (as_timestamp(strptime(value_json.startDate, "%Y-%m-%d"))) ) and ( (as_timestamp(strptime(value_json.endDate, "%Y-%m-%d"))) > (as_timestamp(now())) )) %}
          {{ ( ( (as_timestamp(strptime(value_json.endDate, "%Y-%m-%d"))) - (as_timestamp(now())) ) | timestamp_custom("%d") ) | float() | int}}
        {% else %}
          0
        {% endif %}
      updated: >-
        {{ now().strftime('%Y-%m-%dT%H:%M:%S')|string }}

Just for the sake of completeness: as additional binary_sensors I have the following (for automation purposes):

    - name: "Ist heute Schulfrei?"
      unique_id: binary_sensor.is_schulfrei_today
      icon: mdi:school-outline
      state: >-
        {{
          is_state('binary_sensor.schulferien_bw_next', 'on')
          or is_state('binary_sensor.is_feiertag_today', 'on')
          or is_state('binary_sensor.is_wochenende_today', 'on')
          or is_state('input_boolean.schulfrei', 'on')
        }}
      availability: >
        {{ expand('binary_sensor.schulferien_bw_next', 'binary_sensor.is_feiertag_today', 'binary_sensor.is_wochenende_today')
          | rejectattr('state', 'in', ['unknown','unavailable']) | list | count == 3 }}

    - name: "Ist morgen Schulfrei?"
      unique_id: binary_sensor.is_schulfrei_tomorrow
      icon: mdi:school-outline
      state: >-
        {% set days_remaining = state_attr('binary_sensor.schulferien_bw_next', 'days_remaining') %}
        {{
          is_state_attr('binary_sensor.schulferien_bw_next', 'days_remaining', 1)
          or days_remaining >= 1
          or is_state('binary_sensor.is_feiertag_tomorrow', 'on')
          or is_state('binary_sensor.is_wochenende_tomorrow', 'on')
        }}
      availability: >-
        {{ expand('binary_sensor.schulferien_bw_next', 'binary_sensor.is_feiertag_tomorrow', 'binary_sensor.is_wochenende_tomorrow')
          | rejectattr('state', 'in', ['unknown','unavailable']) | list | count == 3 }}

If I use the button to reload the REST ENTITIES AND NOTFY SERVICES in the Developer tools the sensor is updated and filled as expected.

friendly_name: Schulferien?
name: Osterferien
duration: 14
begins: "2024-03-23T00:00:00"
days_until_start: 0
ends: "2024-04-05T23:59:59"
days_remaining: 10
updated: "2024-03-26T01:46:35"
icon: mdi:school

I thought I’d use this approach for two reasons:

  1. nested entries: as far as I’ve read, rest sensors don’t support them in the context of json_attributes
  2. i want to have one binary sensor with attributes instead of having everything distributed on different sensors.

So far so good, but now to my problems:

  1. If i reload the Rest entities I get the following Error in the home-assistant.log

    2024-03-26 01:46:35.802 ERROR (MainThread) [homeassistant.helpers.binary_sensor] Error rendering state template for binary_sensor.schulferien_bw_next: JSONDecodeError: unexpected character: line 1 column 1 (char 0)
    

    How can I solve this? I have walidated the json, but can’t find any error in it.

  2. The Template does not update automatically. I thought with the scan_interval: 3600 the Rest-Sensor sensor.schulferien_bw_next_json will update once a hour, and with the trigger template the binary_sensor.schulferien_bw_next will be updated too, but unfortunately it does not seem to work. I haven’t yet found out how to do this properly.

It would be great if someone could help me set this up correctly and cleanly.

I can well imagine that some people might be interested in this, as www.openholidaysapi.org also provides school vacations and public holidays for 23 other countries.

Best regards and Thanks in advance,
Stefan

Too much for my short availability here but the error may be related that you try to set the icon of the bw entity by referrirng to itself in the template.
I would also not load a large(r) json into the state and there are 2 options I know of

  1. Use a curl to get the data, map the output to an attribute and load the attribute… via the command_line sensor
    OLD example:
  - platform: command_line
    name: testevents
    scan_interval: 1500
    command: >
         echo "{\"events\":" $(
         curl
         -s
         'https://api.fingrid.fi/v1/variable/336/events/json?start_time={{ (now()-timedelta(hours=4)).strftime('%Y-%m-%dT%H:%M:%SZ') }}&end_time={{ (now()+timedelta(hours=12)).strftime('%Y-%m-%dT%H:%M:%SZ') }}'
         ) "}"
    value_template: >
        {{ value_json.events | length }}
    json_attributes:
        - events
  1. you could pipe the output to jq and use that to ‘flatten’ the json
... | jq ' {attribs: . | {start: .startDate, end: .endDate, name: .name[0].text}}'

gives you this

{
  "attribs": {
    "start": "2024-03-23",
    "end": "2024-04-05",
    "name": "Osterferien"
  }
}

Alternative… I am using the French and Dutch ical to load all in my calendars which makes it more visually attractive (my opinion of course)…assume BadenW has the same

Thank you @vingerha for your suggestions.
I don’t think the error comes from the icon, but I will test this.
The size of the json shouldn’t be an issue as long it stays below 255 characters as far as i know. And this should not be the case: the compressed json above has 178 characters for Osterferien, that should fit.
In my opinion, using the command line would be a last resort, as I think it would be too much of a burden.
Ical is an option, true, but has the downside, that I have to update it once a year. I would like to have a solution that works with the “standard methods” provided by Home Assistant for Rest-Calls.

Do you have a clue, why the update doesn’t happen while using the scan_interval: 3600?

Are there any others with hints to solve the problems 1 and 2?

It is just not proper to add this in the state but of course fully your choice :slight_smile:
jq is easy/simple/clean and for my ical…they update every year so I donot need to switch
Add the code in your dev.tools > template and see what it does
scan interval is working for me …alternative is to use automation and service call

It’s simply not proper to not support nested attributes through Home Assistant :wink:

Dev Tools → Template is my friend already, no worries :wink:

Besides: I made some tests:

  • Problem 1 (the error message):

    • the Icon is not the issue for problem.
    • It just happens when I use the REST ENTITIES AND NOTFY SERVICES button. I believe it’s just a glitch as the value is empty for a millisecond during doing this. The error is not raising during normal duty.
  • Problem 2 (the automatic update):

    • I think the trigger

      - trigger:
          - platform: state
            entity_id:
              - sensor.schulferien_bw_next_json
         action: []
      ...
      

      does not update the binary_sensor.schulferien_bw_next, because the json-value of sensor.schulferien_bw_next_json does not change.

    • So I added an additional time trigger

      - trigger:    
          - platform: state
            entity_id:
              - sensor.schulferien_bw_next_json
          - platform: time
            at: "00:01:00"
         action: []
      ...
      

      now it works as expected.

Thanks @vingerha anyways. :slight_smile:

Regards
Stefan

I am not sure how much nested stuff you have seen but it is not easily (!) possible to standardise all situations without making an integration similarly complex to configure. e.g. think of situations where the key itself is a variable having a nested list.
Again… state should be a single value imo and not a large piece of text to be used/decomposed elsewhere (and yes… I do admit I have 1 of those myself).
I cannot comment on what you perceive as a possible cause but it is a series of actions… from your initial post I understood you could not set it up at all.

On the update…there are many posts and I have no clue anymore what/where a solution may be… I just implemented an automation to trigger them and this is fine since 1y+ now

I agree in principle, it’s not pretty and it’s not 100% “clean”. But if you don’t want to descend into the depths of jq and shell scripting, my way is a solution that works.
The main advantage for me here is that I am using a REST API and have no hassle with calendars that only contain one year of data.

By the way, this approach also works for public holidays, you just have to adapt the URL: simply replace https://openholidaysapi.org/SchoolHolidays with https://openholidaysapi.org/PublicHolidays. This means I don’t need any automation, which in my view is also just a workaround for the “only having one year of data” problem.

Agree… it took some time to pick up on jq and if you donot touch on it for a few months,…relearning starts. It is indeed not a solution for the regular user. My commandline suggestion is not that bad…it just puts the output-as-a-whole in the attributes. So many people so many solutions :slight_smile:

Hi. Thanks for this idea @stkr. I reused data source and sensor for myself.
As for single school holiday today I made a template boolean helper with following template:

{% set holiday_data = states('sensor.school_holidays_lithuania') | from_json %}
  {% set start_date = holiday_data.startDate | as_datetime %}
  {% set end_date = holiday_data.endDate | as_datetime %}
  {% set current_date = now() %}
  
  
  {% set start_date = start_date | as_local %}
  {% set end_date = end_date | as_local %}
  
  {% if current_date >= start_date and current_date <= end_date %}
    True
  {% else %}
    False
  {% endif %}

Looks like it’s working as of now. Not sure about future :slight_smile: