How to list multiple calendar event in a template

I ended up figuring out how to get the Node Red integration to work and use the ical-events integration. The hard part was getting the right URL for the “iCloud - Secure” option to work since it’s not the standard URL you get from the calendar itself.

{{ entities[0].attributes.message }}

1 Like

For anyone that comes across this in the future, here is my solution which was created based on the code provided here: Google calendar get more than one event - #11 by der-optimist

This will create and populate a new sensor, the attributes will contain all the upcoming events within X number of days and the state will be the next event date.

  1. Install the AppDaemon addon and add the packages requests, datetime and humanize required in the python section of the configuration

  2. Create a config/appdaemon/apps/calendar.py file with the following code

import appdaemon.plugins.hass.hassapi as hass
from requests import get
import json
import datetime
import humanize

class calendar(hass.Hass):

    def initialize(self):
        # --- define variables ---
        self.ha_url = self.args["url"]
        self.token = self.args["token"]
        self.calendar_name = self.args["calendar"]
        self.days_to_display = self.args["days_to_display"]
        self.sensor_name = self.args["sensor_name"]

        self.run_hourly(self.check_calendar_events, datetime.time(hour=0, minute=1, second=0))

        # --- do all the stuff at restarts ---
        self.listen_event(self.startup, "plugin_started")
        self.listen_event(self.startup, "appd_started")

        # --- initialize ---
        self.check_calendar_events(None)
        
    def startup(self, event_name, data, kwargs):
        self.log("Startup detected")
        self.check_calendar_events(None)
      
    def check_calendar_events(self, kwargs):
        utc_offset = self.utc_offset(None)
        start_dt = (datetime.datetime.now() - utc_offset).strftime("%Y-%m-%dT%H:%M:%S") # results in UTC time => "Z" in url
        end_dt = (datetime.datetime.now() + datetime.timedelta(days=self.days_to_display) - utc_offset).strftime("%Y-%m-%dT%H:%M:%S") # results in UTC time => "Z" in url

        _list = self.load_calendar(self.calendar_name,start_dt,end_dt)
        # self.log(_list)

        if _list == "error":
            self.log("received http error - will retry later")
            self.run_in(self.check_calendar_events, 600)
        else:
            _nextEvent:datetime = None
            _events = []

            for element in _list:

                _summary = ""
                _start_date = ""
                _end_date = ""
                _all_day_event = "dateTime" not in element["start"]

                if "summary" not in element:
                    self.log("No summary in event, ignore")
                    continue

                _summary = element["summary"]

                if(_all_day_event):
                    _start = datetime.datetime.strptime(element["start"]["date"], "%Y-%m-%d")
                    _end = datetime.datetime.strptime(element["end"]["date"], "%Y-%m-%d")
                    _start_date = datetime.datetime.combine(_start, datetime.datetime.min.time()).astimezone()
                    _end_date = datetime.datetime.combine(_end, datetime.datetime.min.time()).astimezone()
                else: 
                    _start_date = datetime.datetime.strptime(element["start"]["dateTime"],"%Y-%m-%dT%H:%M:%S%z")
                    _end_date = datetime.datetime.strptime(element["end"]["dateTime"], "%Y-%m-%dT%H:%M:%S%z")

                _diff = _end_date - _start_date
                _duration = humanize.precisedelta(_diff)
                friendly_start = humanize.naturalday(_start_date)

                _events.append({
                    "summary": _summary,
                    "start_date": _start_date,
                    "end_date": _end_date,
                    "all_day_event": _all_day_event,
                    "duration": _duration,
                    "friendly_start": friendly_start,
                })

                if _nextEvent is None:
                    _nextEvent = _start_date
                else:
                    if _start_date < _nextEvent:
                        _nextEvent = _start_date
                
            # self.log(_events)

            self.set_state(self.sensor_name,state=_nextEvent,attributes= {"events" : _events})

         
    def load_calendar(self,calendar,start_dt,end_dt):
        headers = {'Authorization': "Bearer {}".format(self.token)}
        # self.log("Try to load calendar events")
        apiurl = "{}/api/calendars/{}?start={}Z&end={}Z".format(self.ha_url,calendar,start_dt,end_dt)
        # self.log("ha_config: url is {}".format(apiurl))
        try:

            r = get(apiurl, headers=headers, verify=False, timeout=10)
        except:
            self.log("Error while loading calendar {}. Maybe connection problem".format(calendar))
            return "error"
        # self.log(r)
        # self.log(r.text)

        if r.status_code == 200:
            if "summary" in r.text:
                resp = json.loads(r.text) # List
            else:
                resp = []
        else:
            self.log("loading calendar {} failed. http error {}".format(calendar,r.status_code))
            resp = "error"
        return resp        

    def utc_offset(self, kwargs):
        now_utc_naive = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
        now_loc_naive = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
        utc_offset_dt = datetime.datetime.strptime(now_loc_naive, "%Y-%m-%dT%H:%M:%S") - datetime.datetime.strptime(now_utc_naive, "%Y-%m-%dT%H:%M:%S")
        #self.log("utc offset: {}d {}sec".format(utc_offset_dt.days, utc_offset_dt.seconds))
        return utc_offset_dt

 
  1. Update config/appdaemon/apps/apps.yaml and add the following code
calendar:
  module: calendar
  class: calendar
  token: !secret calendar_token
  url: http://192.168.1.3:8123
  sensor_name: 'sensor.calendar_events'
  calendar: 'calendar.family'
  days_to_display: 60
1 Like

For future reference, you can now get a list of upcoming events using calendar.list_events.

This new service call was added in the 2023.7.0 release (Services can now respond) and is one of the first that’s able to provide a response.

3 Likes

Thanks Tara, would you know how to call this service (calendar.list.events)i n a Jinja template to use for a TTS announcement?

Sure Gar, here’s the example from the link I posted above.

service: calendar.list_events
target:
  entity_id: calendar.school
data:
  duration:
    hours: 24
response_variable: agenda

The variable agenda will contain a list of all events scheduled in the next 24 hours. That’s just one way to use the service call. The documentation explains the service call’s options.

Sorry for my ignorance, yep understand that however I am trying to construct a script using macros, I am trying to construct a TTS response using a number of macros, I have worked out 3/4 of it, however the service call to agenda and retrieving the info eludes me.
Macros for “greetings”, “weather”, hopefully “Calanda 1 agenda”, “Calanda 2 agenda” and so on here is an example - see the ??? needs filling :slight_smile:

{%- macro getGreeting() -%}
{% if now().strftime(’%H’)|int < 12 %}
Good morning.
{% elif now().strftime(’%H’)|int >= 12 and now().strftime(’%H’)|int < 17 %}
Good afternoon.
{% else %}
Good evening.
{% endif %}

{% if is_state(‘binary_sensor.morning’,‘on’) %}
Today is {{ states.sensor.date_and_time.state }},
{% else %}
It is {{ now().strftime("%I:%M %p") }}.
{% endif %}

{%- endmacro -%}

{%- macro getWeatherToday() -%}
the Temperature in the living area is currently
{{states(‘sensor.dining_room_fan_temperature’)}} degrees, the Outside
Temperature is currently {{states(‘sensor.gw1100c_v2_1_3_temperature_1’)}}
degrees, We are expecting a high of
{{states(‘sensor.parkerville_temp_max_0’)}} degrees, with a low of
{{states(‘sensor.parkerville_temp_min_0’)}} degrees, the chance of it
raining today is around {{states(‘sensor.parkerville_rain_chance_0’)}}
percent.
{%- endmacro -%}

{%- macro getGarysAgendaToday() -%}
Garys agenda for today is Today is ???
{%- endmacro -%}

Thanks for sharing this!

I’m trying to read google calendar multiple ‘all day’ events and want automations on them (to use as birthday reminders).

I followed your scripts, but sensor.calendar_events isn’t being shown.

What did I do:

  1. Install AppDaemon via hassio/dashboard Add-ons.
  2. SSH’d to config > appdaemon and added these lines to appdaemon.yaml:
python_packages:
  - requests
  - datetime
  - humanize
  1. I went into the apps folder and edited apps.yaml with the following:
calendar:
  module: calendar
  class: calendar
  token: !secret calendar_token
  url: http://<myinternalIP>:8123
  sensor_name: 'sensor.calendar_events'
  calendar: 'calendar.verjaardagen_2'
  days_to_display: 60

Question: Does this need to be the internal IP or my external (I’ve set a subdomain on my own domain to enter HA).

  1. Created calendar.py with your code.

What am I doing wrong?

Does the appdaemon log show anything useful, eg. The HomeAssistant API failing?

The URL is used to make a call to the API to get the calendar events, the API may not be accessible externally so I use the internal url

Thanks for the quick reply!

I’m can visit the AppDaemon. I don’t see any logs (no data available) on main_log, error_log. Also ‘Dashboards’ doesn’t show ‘calendar’.

The AppDaemon Dashboard doesn’t show the calendar for me either. main_log only seems to show something for me when I have the page open and then save the calendar.py file, then the logs will appear.

Also the Token should be a long-lived access token, this can be created in the profile section of HA.

My appdaemon.yaml does not contain python_packages, I added my packages in the front end.

Ok thanks. So I need to do something with the access token?

How can I check if the python packages are installed correctly? Or if the calendar.py is even loaded by appDaemon? I don’t see it when viewing State > Apps …

You need to generate a long-lived access token, add it to you secrets.yaml with the name calendar_token.

The app should be visible in State > Apps, the fact that its not makes me think something is erroring before it completes, go to hassio/addon select AppDaemon and click the info tab to see if there are any errors

Okay, few step closer:

  1. I was manually adding the packages via SSH. Thanks for hinting towards the frontend. There I found the Log tab as well. It hinted on the token couldn’t be found. So:

  2. Created the long-lived access token and placed it into my secrets.yaml.

Now when I watch the log, all I get is:

2023-08-21 13:57:01.707038 INFO AppDaemon: Loading app hello_world using class HelloWorld from module hello
2023-08-21 13:57:01.710298 INFO AppDaemon: Loading app calendar using class calendar from module calendar
2023-08-21 13:57:01.712695 WARNING calendar: ------------------------------------------------------------
2023-08-21 13:57:01.713168 WARNING calendar: Unexpected error initializing app: calendar:
2023-08-21 13:57:01.714275 WARNING calendar: ------------------------------------------------------------
2023-08-21 13:57:01.718571 WARNING calendar: Traceback (most recent call last):
  File "/usr/lib/python3.11/site-packages/appdaemon/app_management.py", line 1035, in check_app_updates
    await self.init_object(app)
  File "/usr/lib/python3.11/site-packages/appdaemon/app_management.py", line 338, in init_object
    "object": app_class(
              ^^^^^^^^^^
TypeError: TextCalendar.formatyear() takes from 2 to 6 positional arguments but 8 were given
2023-08-21 13:57:01.718979 WARNING calendar: ------------------------------------------------------------
2023-08-21 13:57:01.834220 WARNING AppDaemon: Unable to find module calendar - initialize() skipped

The log began with downloading the resources which is good. Also it found the calendar as ‘all’ so, also good. Now I only get this issue.

I have not seen that error before, try uncommenting some of the self.log lines in calendar.py and then we can start to work out where it is getting up to

I uncommented line 36 self.log(_list) ; line 93 and 95 but It’s not logging.

ChatGPT to the rescue? It suggested to rename the module to my_calendar because calendar is also a build-in library.

So, that helped with the earlier issues. Now I’m getting the following error:

Error while loading calendar calendar.verjaardagen_2. Maybe connection problem

Do you have a google calendar setup with that name under Devices and Services in HA?

@gjohnson94 … we’ve got a success!

I did had to change my calendar to my subdomain and now it works. I found the calendar_events sensor and it’s giving multiple all_day_events .

Question now remains: How do I show a a list of the events summary on a dashboard?

So in summary:

  1. Create a long-live access token via profile (scroll all the way down)
  2. Place the access token in your secrets.yaml
  3. Change your apps.yaml file and name the module ‘my_calendar’ and check if the URL is correct (this will result eventually in an error in the AppDaemon log if it’s not correct).

EDIT

With the advent of Home Assistant version 2023.09.0, it’s now even easier to use calendar.list_events in a Trigger-based Template Sensor because it now supports an action section.

template:
  - trigger:
      - platform: time_pattern
        minutes: /30
    action:
      - service: calendar.list_events
         target:
          entity_id: calendar.gary
        data:
          duration:
            hours: 24
        response_variable: scheduled_events
    sensor:
      - name: Gary Scheduled Events
        unique_id: gary_scheduled_events
        state: "{{ scheduled_events.events | count() }}"
        attributes:
          scheduled_events: "{{ scheduled_events.events }}"
        icon: mdi:calendar

For historical reference, here’s the original post which used a separate automation for what now can all be done in a Trigger-based Template Sensor.

Original post

Here’s what you can do natively in Home Assistant using the recently added calendar.list_events service call.

Basically, an automation periodically gets upcoming events from your calendar and transfers them to a Template Sensor. Afterwards, you can access the upcoming events directly from the Template Sensor.


Automation

Create an automation that uses a Time Pattern Trigger to poll your calendar every 30 minutes, gets all upcoming calendar events for the next 24 hours, and posts them via a custom Home Assistant Event named gary_scheduled_events.

alias: Get Gary Scheduled Events
description: ""
trigger:
  - platform: time_pattern
    minutes: /30
condition: []
action:
  - service: calendar.list_events
     target:
      entity_id: calendar.gary
    data:
      duration:
        hours: 24
    response_variable: scheduled_events
  - event: gary_scheduled_events
    event_data:
      scheduled_events: "{{ scheduled_events }}"
mode: single

Trigger-based Template Sensor

Create a Trigger-based Template Sensor that uses an Event Trigger to receive your scheduled events, via gary_scheduled_events, and reports them in an attribute named scheduled_events. It also reports the number of scheduled events in its state property.

template:
  - trigger:
      - platform: event
        event_type: gary_scheduled_events
    sensor:
      - name: Gary Scheduled Events
        unique_id: gary_scheduled_events
        state: "{{ trigger.event.data.scheduled_events.events | count() }}"
        attributes:
          scheduled_events: "{{ trigger.event.data.scheduled_events.events }}"
        icon: mdi:calendar

For your TTS macro, it should iterate through the list of calendar events found in the scheduled_events attribute of sensor.gary_scheduled_events.

The template will look something like this:

{% for e in state_attr('sensor.gary_scheduled_events', 'scheduled_events') %}
{{ e.start}}: {{ e.summary }}
{% endfor %}

Reference

From the documentation for calendar.list_events

image


EDIT

Correction.

Changed this:

scheduled_events: "{{ trigger.event.data.scheduled_events }}"

to this:

scheduled_events: "{{ trigger.event.data.scheduled_events.events }}"
5 Likes

Wow thank you I will have a play and see what I can do, the other issue is the format of the time (reads out in military time) , I came across your post to convert the time however not sure where to add it, tried a few thing but failed :frowning: still leaning :slight_smile:
This is the post I found.

{%- if agenda.events %}
  {%- for event in agenda.events if event.start is defined -%}
    {%- set start = event.start | as_datetime | as_local -%}
    {%- set end = event.end | as_datetime | as_local -%}
    {%- if start > now() and start - now() < timedelta(days=1) %}     
      Am {{ tage[start.weekday()] }} den {{ start.strftime('%d/%m/%Y') }}
      {%- if end - start == timedelta(days=1) %} ganztägig
      {%- else %} um {{ start.strftime('%H:%M %p') }} bis {{ end.strftime('%H:%M %p') }}
      {%- endif %} ist {{ event.summary }}
      {%- if event.description is defined %}
      Hinweise, {{ event.description }}
      {%- endif -%}
      {%- if event.location is defined %}
      Ort, {{ event.location }}
      {% endif -%}
    {% endif -%}  
  {%- endfor %}
{%- endif -%}'