An alarm clock adjusting to Magister iCAL calendar

You might know a teenager who a) likes to sleep in as late as possible and b) uses Magister to check his school calendar. In this project I created an alarm that automatically adjusts itself to a Magister or other iCAL calendar.

magister_logo

Magister is an app that is used by a lot of schools in the Netherlands. But the same principles can be applied to any iCAL calendar that offers a subscription link.

Requirements:

  • Calendar sharing has to be enabled in the school’s Magister instance (if not, the school needs to ask the provider to enable this).
  • Add-on iCal Sensor support for Home Assistant (it can be installed through HACS).
  • A media player that can be used to play the alarm’s song. I am using a Google Home (Nest) Mini.
  • The Time & Date integration configured so sensor.time and sensor.date are available.
  • Some skills to add it to your dashboard.

Steps:

  • Get the calendar URL. You need to log into Magister online using your kid’s credentials. Then find the Settings page (find it by clicking on the portrait picture in the bottom left corner). There should be a widget called ‘Calendar sharing’ that displays the webcal:// url you need.

  • Install the iCal Sensor add-on through HACS. Follow the instructions to add it to your integrations. Configure it to look a maximum of 14 days into the future and for 50 events (=50 sensors). Name it ‘ical_magister’ to make it work with the following templates.

  • Create some helpers:

    1. An input_boolean ‘alarm_magister’ for toggling the alarm.
    2. An input_number ‘alarm_magister_offset’ min 1 max 180, for storing the offset of how many minutes before the first lesson the kid needs to wake up.
  • Create a template sensor that contains the date and time for the next ‘first lesson of the day’. This one will be used as the trigger for the alarm automation.

template:
  - trigger:
      - platform: state
        entity_id: sensor.date
      - platform: state
        entity_id:
          - sensor.ical_magister_event_0
          - sensor.ical_magister_event_1
          - sensor.ical_magister_event_2
          - sensor.ical_magister_event_3
          - sensor.ical_magister_event_4
          - sensor.ical_magister_event_5
          - sensor.ical_magister_event_6
          - sensor.ical_magister_event_7
          - sensor.ical_magister_event_8
          - sensor.ical_magister_event_9
          - sensor.ical_magister_event_10
          - sensor.ical_magister_event_11
          - sensor.ical_magister_event_12
          - sensor.ical_magister_event_13
          - sensor.ical_magister_event_14
          - sensor.ical_magister_event_15
          - sensor.ical_magister_event_16
          - sensor.ical_magister_event_17
          - sensor.ical_magister_event_18
          - sensor.ical_magister_event_19
          - sensor.ical_magister_event_20
          - sensor.ical_magister_event_21
          - sensor.ical_magister_event_22
          - sensor.ical_magister_event_23
          - sensor.ical_magister_event_24
          - sensor.ical_magister_event_25
          - sensor.ical_magister_event_26
          - sensor.ical_magister_event_27
          - sensor.ical_magister_event_28
          - sensor.ical_magister_event_29
          - sensor.ical_magister_event_30
          - sensor.ical_magister_event_31
          - sensor.ical_magister_event_32
          - sensor.ical_magister_event_33
          - sensor.ical_magister_event_34
          - sensor.ical_magister_event_35
          - sensor.ical_magister_event_36
          - sensor.ical_magister_event_37
          - sensor.ical_magister_event_38
          - sensor.ical_magister_event_39
          - sensor.ical_magister_event_40
          - sensor.ical_magister_event_41
          - sensor.ical_magister_event_42
          - sensor.ical_magister_event_43
          - sensor.ical_magister_event_44
          - sensor.ical_magister_event_45
          - sensor.ical_magister_event_46
          - sensor.ical_magister_event_47
          - sensor.ical_magister_event_48
          - sensor.ical_magister_event_49
    sensor:
      - name: "Nextup lesson Magister"
        # device_class: timestamp
        unique_id: nextup_lesson_magister
        state: >-
          {% if now().hour < 12 %}
          {% set x = states.sensor
            | selectattr('object_id', 'match', 'ical_magister')
            | selectattr('attributes.start', 'defined') 
            | rejectattr('attributes.location', 'in', ('','verborgen'))
            | rejectattr('attributes.all_day', 'eq', true)
            | sort(attribute='attributes.start')
            | map(attribute='attributes.start') | list %}
          {{ x|first if x != [] else '' }}
          {% else %}
          {% set x = states.sensor
            | selectattr('object_id', 'match', 'ical_magister')
            | selectattr('attributes.start', 'defined') 
            | rejectattr('attributes.start', 'search', as_timestamp(now()) | timestamp_custom ('%Y-%m-%d') )
            | rejectattr('attributes.location', 'in', ('','verborgen'))
            | rejectattr('attributes.all_day', 'eq', true)
            | sort(attribute='attributes.start')
            | map(attribute='attributes.start') | list %}
          {{ x|first if x != [] else '' }}
          {% endif %}
  • Create a template sensor that contains a nice displayable description of the alarm time. E.g. “Tomorrow at 6.55”. This one you can use in your Lovelace dashboard, for display purposes.
template:
  - trigger:
      - platform: state
        entity_id: sensor.date
      - platform: state
        entity_id: sensor.nextup_lesson_magister
      - platform: state
        entity_id: input_number.magister_offset
    sensor:
    - name: Description of next up lesson
      unique_id: description_of_nextup_lesson
      state: >-
        {% set midnight = now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp() %}
        {% set event = states('sensor.nextup_lesson_magister') | as_timestamp(default=0) %}
        {% set delta = ((event - midnight) / 86400) | int %}
        {% if is_state('sensor.nextup_lesson_magister','') %} Onbekend
        {% elif delta < 0 %} Onbekend
        {% elif delta == 0 %} Vandaag {{ (states('sensor.nextup_lesson_magister')|as_timestamp - (states('input_number.magister_offset')|int(default=90) * 60)) |timestamp_custom('%H:%M', true)}}
        {% elif delta == 1 %} Morgen {{ (states('sensor.nextup_lesson_magister')|as_timestamp -(states('input_number.magister_offset')|int(default=90) * 60)) |timestamp_custom('%H:%M', true)}}
        {% else %} Over {{ delta }} dagen {{ (states('sensor.nextup_lesson_magister')|as_timestamp - (states('input_number.magister_offset')|int(default=90) * 60)) |timestamp_custom('%H:%M', true)}}
        {% endif %}
  • Create an automation for triggering the alarm.
- alias: "Alarm Magister"
  id: alarm_magister
  trigger:
    - platform: template
      value_template: "{{ (as_timestamp(states('sensor.nextup_lesson_magister')) - (states('input_number.magister_offset')|int(default=90) * 60) )|timestamp_custom('%H:%M', true) == states('sensor.time') }}"
  condition:
    - condition: state
      entity_id: input_boolean.alarm_magister
      state: 'on'
    - condition: template
      value_template: "{{ (as_timestamp(states('sensor.nextup_lesson_magister')) - (states('input_number.magister_offset')|int(default=90) * 60) )|timestamp_custom('%Y-%m-%d', true) == states('sensor.date') }}"
  action:
    - service: media_player.play_media
      entity_id: media_player.googlehomemini
      data:
        media_content_id: "{{ <your base url here> + '/local/audio/dont_worry_be_happy.mp3' }}"
        media_content_type: "audio/mp3"
  • Add it to your dashboard:

    1. Something to toggle the input_boolean, for toggling the alarm.
    2. Something to set the offset (input_number).
    3. Something to display the description for next ‘first lesson of the day’ (template sensor).

I’ve been fine tuning this setup for a couple of months now. The alarm works for the way our school uses the calendar. Of course other schools might use the calendar differently, I imagine the template might need to be tuned for that.

If you have any questions let me know. I’m hoping some more teenagers get to sleep in once in a while using these examples :slight_smile:

image
image

Edit 1: excluded all-day calendar events (these start at midnight, usually holiday reminders).
Edit 2: good to know: at the beginning of each school year, calendar sharing is disabled by Magister. It gets enabled when all schools in the Netherlands have started again.
Edit 3: a little housekeeping, fixed a wrong reference in the second template sensor.
Edit 4: added the check suggested below, to check if a start attribute exists. Preventing an error when a calendar item is empty / without a starting time.

3 Likes

Does this still work? I try to add the webcal url to the sensor but it doesnt seem to find any sensors.

Indeed it is doesn’t work for me anymore in this new school year. It seems something broke at Magister, the ics file contains no appointments.

Only thing you can try: replace webcal:// with https://

But for me, that does not do anything. I reported the bug at our school.

Thank you. I also tried both options but received zero appointments.

Also tried two different child accounts on two different schools.

Also filed a feedback ticket

Great idea, I also sent a feedback mail to Magister. Our school has not responded yet, they are probably busy with other stuff at the start of the school year.

1 Like

The calendar is not active in the first two weeks of the school year apparently. I asked the local admin and he responded:

De iCal synchronisatie staat in het begin van het schooljaar altijd uit, totdat alle scholen in Nederland weer begonnen zijn.

I am now setting this up. The iCal events do show up. Hope this all works :slight_smile:

1 Like

Thanks for letting us know. It works now!

It’s working! Thanks!

1 Like

hi roelof
nice work
i copy past this in mine configuration yaml but i cant get it to work
can u help me?
have u got a example of the yaml for the dashboard

Hi @fjjfrank, I tried to describe all necessary steps, it is definitely more than just copying the yaml. The dashboard is quite specific for my setup, that’s why I didn´t include it in the post.

For a basic dashboard, I guess I would add an Entities card. And then add to that the input_boolean for toggling the alarm (input_boolean.alarm_magister) and the input_number (input_number.magister_offset). You could also add sensor.description_of_nextup_lesson.

There it is going wrong for me I think
I have copied it to my configuration yaml
And I get no sensor for the next lesson
I really don’t no what I am doing wrong

have u got the files in your configuration yaml or in a sensor yaml?

have u got a example of u config? so i can get to the right direction

I’m sorry my full configuration is not easily shareable. The template sensors are in configuration.yaml (in my setup that is).

If your template sensor is not working: did you name the iCal Sensor ‘ical_magister’? That is the name I am using in the templates.

Did you fill the other prerequisites? Time & Date integration, helpers?

When you check in Developer Tools > States > do you see the 50 sensors for ical_magister?

oh yes now i see it i have called it jaylie school and not magister

After some changes in ical I got this error:
Template variable warning: ‘homeassistant.util.read_only_dict.ReadOnlyDict object’ has no attribute ‘start’

adding the line below to the template to first check if the start attribute exists, fixed the issue.
| selectattr('attributes.start', 'defined')

below the full template

template:
  - trigger:
      - platform: state
        entity_id: sensor.date
      - platform: state
        entity_id:
          - sensor.ical_magister_event_0
          - sensor.ical_magister_event_1
          - sensor.ical_magister_event_2
          - sensor.ical_magister_event_3
          - sensor.ical_magister_event_4
          - sensor.ical_magister_event_5
          - sensor.ical_magister_event_6
          - sensor.ical_magister_event_7
          - sensor.ical_magister_event_8
          - sensor.ical_magister_event_9
          - sensor.ical_magister_event_10
          - sensor.ical_magister_event_11
          - sensor.ical_magister_event_12
          - sensor.ical_magister_event_13
          - sensor.ical_magister_event_14
          - sensor.ical_magister_event_15
          - sensor.ical_magister_event_16
          - sensor.ical_magister_event_17
          - sensor.ical_magister_event_18
          - sensor.ical_magister_event_19
          - sensor.ical_magister_event_20
          - sensor.ical_magister_event_21
          - sensor.ical_magister_event_22
          - sensor.ical_magister_event_23
          - sensor.ical_magister_event_24
          - sensor.ical_magister_event_25
          - sensor.ical_magister_event_26
          - sensor.ical_magister_event_27
          - sensor.ical_magister_event_28
          - sensor.ical_magister_event_29
          - sensor.ical_magister_event_30
          - sensor.ical_magister_event_31
          - sensor.ical_magister_event_32
          - sensor.ical_magister_event_33
          - sensor.ical_magister_event_34
          - sensor.ical_magister_event_35
          - sensor.ical_magister_event_36
          - sensor.ical_magister_event_37
          - sensor.ical_magister_event_38
          - sensor.ical_magister_event_39
          - sensor.ical_magister_event_40
          - sensor.ical_magister_event_41
          - sensor.ical_magister_event_42
          - sensor.ical_magister_event_43
          - sensor.ical_magister_event_44
          - sensor.ical_magister_event_45
          - sensor.ical_magister_event_46
          - sensor.ical_magister_event_47
          - sensor.ical_magister_event_48
          - sensor.ical_magister_event_49
    sensor:
      - name: "Nextup lesson Magister"
        # device_class: timestamp
        unique_id: nextup_lesson_magister
        state: >-
          {% if now().hour < 12 %}
          {% set x = states.sensor
            | selectattr('object_id', 'match', 'ical_magister')
            | selectattr('attributes.start', 'defined') 										  
            | rejectattr('attributes.location', 'in', ('','verborgen'))
            | rejectattr('attributes.all_day', 'eq', true)
            | sort(attribute='attributes.start')
            | map(attribute='attributes.start') | list %}
          {{ x|first if x != [] else '' }}
          {% else %}
          {% set x = states.sensor
            | selectattr('object_id', 'match', 'ical_magister')
            | selectattr('attributes.start', 'defined') 										  
            | rejectattr('attributes.start', 'search', as_timestamp(now()) | timestamp_custom ('%Y-%m-%d') )
            | rejectattr('attributes.location', 'in', ('','verborgen'))
            | rejectattr('attributes.all_day', 'eq', true)
            | sort(attribute='attributes.start')
            | map(attribute='attributes.start') | list %}
          {{ x|first if x != [] else '' }}
          {% endif %}

Wow, I was wondering if this would be possible in an automation, when I bumped into this thread. Appearantly it is already fully functional… Thanks for your efforts, ik ga er binnenkort even voor zitten.

Hi, i’ve used your configuration, and i’m almost there i think,
except the result i get from the sensor is:
sensor:
- name: “Nextup lesson Magister”
# device_class: timestamp
unique_id: nextup_lesson_magister
state: >-

      2024-01-19 08:30:00+01:00

and i think the +01:00 at the end is preventing my automation to run, how can i fix that?

I have the same state for my sensor, the strings ends with +01:00. And it still does trigger properly. So I think there might be a different issue for you. I think I would check the logs.

Perhaps you can check the template with the Developer Tools, to see if it produces a valid result:

{{ (as_timestamp(states('sensor.nextup_lesson_magister')) - (states('input_number.magister_offset')|int(default=90) * 60) )|timestamp_custom('%H:%M', true) == states('sensor.time') }}