Calendar - new entitites or helper that show a list of calendar events within a certain scope

I am also waiting for this feature, in the mean time I have found a workaround. Using the REST API it is possible to get a list of events from a calendar:

curl -X GET -H "Authorization: Bearer ABCDEFGH" \
  -H "Content-Type: application/json" \
  http://localhost:8123/api/calendars/calendar.holidays?start=2022-05-01T07:00:00.000Z&end=2022-06-12T07:00:00.000Z

Integrating that REST API call into a python script allows us to do anything related to past/upcoming events.

The default Python Script integration does not allow using Python imports (so no cURL/requests to make REST API calls), but using the pyscript integration everything is possible!

Thanks for pointing this out! I suck at python. Do you have a code example of how to pull calendar events with pyscript?

Here’s a piece of code to get you going:

import aiohttp

calendar_id = "calendar.my_calendar"
events_filter = "start=2022-12-01&end=2023-04-01"
access_token = "xxx"
local_calendar_url = f"https://localhost:8123/api/calendars/{calendar_id}?{events_filter}"

headers = {
    'Authorization': f"Bearer {access_token}",
    'Content-Type': 'application/json'
}
async with aiohttp.ClientSession(headers=headers, connector=aiohttp.TCPConnector(ssl=False)) as session:
    async with session.get(local_calendar_url) as response:
        calendar_events = json.loads(response.text())
log.info('Calendar events:' + json.dumps(calendar_events))

You’ll have to adjust the variables for your situation and will need to get a long-lived access token.

1 Like

Excuse me, in which folder should I put your script?

This is just a snippet of code that can be used to retrieve the list of events in a calendar, it is not a fully functioning script.

To get going you would need to install the Pyscript integration, turn the snippet into a full script that does what you want and copy it into /config/pyscript.

@Mathieu - thanks for the pointer. I wrote a pyscript that

  1. for every calendar included/excluded (see code) adds a “calendar.name_ext” entity with 4 attributes
current_event_brief: 
upcoming_event_brief: 9:00 Justin and Alex 
start_time: 2023-02-27T09:00:00-05:00
till: 12hr
  1. adds calendar.all entity that aggegates included calendars
current_event_brief: 
upcoming_event_brief: 9:00 (Steve  / alex)
start_time: 2023-02-27T09:00:00-05:00
till: 12hr
import aiohttp
import datetime
from datetime import timezone
import json

from homeassistant.const import EVENT_STATE_CHANGED

ha_base_url = "http://192.168.XX.YY:8123"
access_token = "YOUR_TOKEN"

now = datetime.datetime.now()
start = now.isoformat()
end = now + datetime.timedelta(days=7)
events_filter = f"start={start}&end={end}"


headers = {
    'Authorization': f"Bearer {access_token}",
    'Content-Type': 'application/json'
}

include_calendars = []
exclude_calendars = ['calendar.birthdays', 'calendar.holidays_in_united_state']

if include_calendars and len(include_calendars) == 0:
    calendar_url = f"{ha_base_url}/api/calendars"
    async with aiohttp.ClientSession(headers=headers, connector=aiohttp.TCPConnector(ssl=False)) as session:
        async with session.get(calendar_url) as response:
            try:
                calendars = json.loads(response.text())
                include_calendars = [calendar['entity_id'] for calendar in calendars]
            finally:
                pass

all_calendar_events = []


def add_briefs(include_calendars, exclude_calendars):
    for calendar in include_calendars:
        if calendar in exclude_calendars:
            continue
        calendar_url = f"{ha_base_url}/api/calendars/{calendar}?{events_filter}"
        async with aiohttp.ClientSession(headers=headers, connector=aiohttp.TCPConnector(ssl=False)) as session:
            async with session.get(calendar_url) as response:
                calendar_events = json.loads(response.text())
                # add calendar name to each event
                for event in calendar_events:
                    event['calendar.entity_id'] = calendar
                    if 'dateTime' not in event['start']:
                        event['start']['dateTime'] = event['start']['date'] + 'T00:00:00'
                        event['all_day'] = True
                    else:
                        event['all_day'] = False

                    start_time = datetime.datetime.fromisoformat(event['start']['dateTime']).astimezone()

                    dow = start_time.strftime('%a')
                    hm = start_time.strftime('%-I:%M')
                    duration = start_time - datetime.datetime.now().astimezone()
                    hours = duration.seconds // 3600 + (duration.days * 24)
                    minutes = (duration.seconds // 60) % 60
                    if hours > 0:
                        duration_str = f"{hours}hr"
                    else:
                        duration_str = f"{minutes}m"
                    event['till'] = f"{duration_str}"

                    # remove common words from summary
                    summary = event['summary'].replace('Meeting with ', '')
                    summary = summary.replace('Meeting ', '')
                    summary = summary.replace('Call with ', '')
                    summary = summary.replace('Discussion', '')
                    summary = summary.replace('Introduction Call ', '')
                    summary = summary.replace('Call ', '')
                    summary = summary.replace(' with ', ' ')
                    summary = summary.replace(' - ', ' ')
                    summary = summary.replace(' @ ', ' ')
                    summary = summary.strip(' -@:')

                    if (hours > 24):
                        event['brief'] = f"{dow} {hm} {summary}"
                    else:
                        event['brief'] = f"{hm} {summary}"

                filtered_calendar_events = calendar_events
                filtered_calendar_events = [event for event in filtered_calendar_events if event['start']['dateTime'] > now.isoformat()]
                filtered_calendar_events = [event for event in filtered_calendar_events if not event['all_day']]
                filtered_calendar_events = [event for event in filtered_calendar_events if 'Personal Commitment' not in event['summary']]
                filtered_calendar_events = [event for event in filtered_calendar_events if 'summary' in event and len(event['summary'])>0]

                all_calendar_events.extend(filtered_calendar_events)

                # sort events by ascending start time
                sorted_calendar_events = sorted(filtered_calendar_events, key=lambda event: event['start']['dateTime'])
                current_events = [event for event in sorted_calendar_events if event['end']['dateTime'] < now.isoformat()]
                upcoming_events = [event for event in sorted_calendar_events if event['end']['dateTime'] > now.isoformat()]

                if len(current_events) > 0:
                    next_event = current_events[0]
                    state.set(f"{calendar}_ext",
                              current_event_brief=next_event['brief'],
                              end_time=next_event['end']['dateTime'])
                else:
                    state.set(f"{calendar}_ext", current_event_brief='')

                if len(upcoming_events) > 0:
                    next_event = upcoming_events[0]
                    state.set(f"{calendar}_ext",
                              upcoming_event_brief=next_event['brief'],
                              start_time=next_event['start']['dateTime'],
                              till=next_event['till'])
                else:
                    state.set(f"{calendar}_ext", upcoming_event_brief='')

    sorted_calendar_events = sorted(all_calendar_events, key=lambda event: event['start']['dateTime'])
    current_events = [event for event in sorted_calendar_events if event['end']['dateTime'] < now.isoformat()]
    upcoming_events = [event for event in sorted_calendar_events if event['end']['dateTime'] > now.isoformat()]

    if len(current_events) > 0:
        next_event = current_events[0]
        state.set("calendar.all", current_event_brief=next_event['brief'], end_time=next_event['end']['dateTime'])
    else:
        state.set("calendar.all", current_event_brief='')

    if len(upcoming_events) > 0:
        next_event = upcoming_events[0]
        state.set("calendar.all", next_event['till'],
                  upcoming_event_brief=next_event['brief'],
                  start_time=next_event['start']['dateTime'],
                  till=next_event['till'])
    else:
        state.set("calendar.all", upcoming_event_brief='')

@event_trigger(EVENT_STATE_CHANGED, "entity_id.startswith('calendar.') and not entity_id.endswith('_ext') and 'calendar.all' not in entity_id")
def monitor_calendar_change(entity_id=None, new_state=None, old_state=None):
    add_briefs(include_calendars, exclude_calendars)

add_briefs(include_calendars, exclude_calendars)
3 Likes

Thanks for the great explanation @Alex_Pupkin and @Mathieu. I adapted to create a list of upcoming events to show them on an epaper display.

Only thing I can’t get to work: The @event_trigger to work when the calendar is updated. :frowning:

I use the Google Calendar integration which does not to trigger a state_changed event (I checked manually the by listening to the event bus). I believe a state_changed event is triggered only if the very next event has changed, not if upcoming events have changed.

Question: Has anyone an idea if and which event the Google Calendar integration triggers when it updates (which it does every 15 minutes)? :thinking:

Here is my “trigger code”.

@event_trigger(EVENT_STATE_CHANGED, "entity_id=='calendar.mabjo'")
@time_trigger("cron(0 0 * * *)", "startup")
def monitor_calendar_change(entity_id=None, new_state=None, old_state=None):
    add_briefs(include_calendars, exclude_calendars)

What would really be amazing is being able to select event, relative to now, by a consistently numbered selector, such as:

states.calendar['my_calendar_name'].event-2 = two events previous to now
states.calendar['my_calendar_name'].event-1 = most recent previous event
states.calendar['my_calendar_name'].event_0 = next event
states.calendar['my_calendar_name'].event_1 = second furthest out states.calendar['my_calendar_name'].event_2 = third furthest out event

And then have the attribute properties again selectable below that, so that:

states.calendar['my_calendar_name'].event_0.start_time was retrievable

Please feel free to adjust my model to be valid python, this is definitely not my “native programming language”.

Thanks.

I was playing around with using the home assistant rest API and a rest template sensor and came up with a hacky way to do this:

- platform: rest
  resource: https://<your home assistant url>/api/calendars/calendar.personal
  name: Agenda
  method: GET
  scan_interval: 900
  headers:
    authorization: !secret rest_events_token
  params:
    start: >
      {{ utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z') }}
    end: >
      {{ (utcnow() + timedelta(days=5)).strftime('%Y-%m-%dT%H:%M:%S.000Z') }}
  value_template: >
    {% set events = value_json %}
    {% for event in events %}
      {% if event.start.dateTime %}
        {{ (event.start.dateTime | as_datetime).strftime('%Y-%m-%d %H:%M') }}: {{ event.summary }}
      {% else %}
        {{ event.start.date }}: {{ event.summary }}
      {% endif %}
    {% endfor %}

See RESTful Sensor - Home Assistant for more details. You’ll need to get a home assistant bearer token for this to work and enter as a secret as shown in the rest docs.

4 Likes

i dont know if there an event but you can create one easily if you have pyscript. add an attribute to calendar entity on timer - when google calendar integration refreshes data, it removes that attribute thus generating an event.

I also like to announce my appointments of the day with TTS in my morning routine

1 Like

Hello everyone,
I have installed “HASS Calendar Addon”, but I can’t see a sensor anywhere, what am I doing wrong.
Can someone help me with this?
Thanks for every response

What does the addon’s log say?

Thanks for you reaction

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
cont-init: info: running /etc/cont-init.d/00-banner.sh
-----------------------------------------------------------
 Add-on: Hass Calendar Addon
 Addon to consume caldav and google calendars and make calendar events available as sensordata in Home Assistant.
-----------------------------------------------------------
 Add-on version: 0.301
 You are running the latest version of this add-on.
 System: Home Assistant OS 10.0  (amd64 / qemux86-64)
 Home Assistant Core: 2023.4.6
 Home Assistant Supervisor: 2023.04.1
-----------------------------------------------------------
 Please, share the above information when looking for help
 or support in, e.g., GitHub, forums or the Discord chat.
-----------------------------------------------------------
cont-init: info: /etc/cont-init.d/00-banner.sh exited 0
cont-init: info: running /etc/cont-init.d/01-log-level.sh
cont-init: info: /etc/cont-init.d/01-log-level.sh exited 0
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started
added 80 packages, and audited 81 packages in 14s
23 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities
npm notice 
npm notice New minor version of npm available! 9.1.2 -> 9.6.5
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v9.6.5>
npm notice Run `npm install -g [email protected]` to update!
npm notice 
System locale: en-US
Luxon locale set to: nl-BE
System timeZone: Europe/Brussels
Luxon timezone set to: Europe/Brussels
Previously stored events posted to sensor(s) at: Sun Apr 23 2023 17:50:11 GMT+0200 (Central European Summer Time)
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')

There was an issue about that log error a couple months ago… related to an intermittent problem with empty calendars. I don’t know if it was fixed yet.

Make sure there is at least one event in your calendar(s) within the time frame of fetchDaysPast - fetchDays, restart the addon. Remember that the sensor only sends a fetch request based on your fetchCron time configuration variable. If you use the default, that means it will fetch on the hour and at half-past. Sometimes the first fetch after restart fails, so let it run uninterrupted through at least 2 fetch cycles.

Hi,

Thank you for your response.

In attachment you see my calendar to test and also my settings.

If I understand you correctly, the add-on will retrieve the data from the calendar every 30 minutes? Is that correct?


After waiting 30 minutes I get this in the log:

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
cont-init: info: running /etc/cont-init.d/00-banner.sh
-----------------------------------------------------------
 Add-on: Hass Calendar Addon
 Addon to consume caldav and google calendars and make calendar events available as sensordata in Home Assistant.
-----------------------------------------------------------
 Add-on version: 0.301
 You are running the latest version of this add-on.
 System: Home Assistant OS 10.0  (amd64 / qemux86-64)
 Home Assistant Core: 2023.4.6
 Home Assistant Supervisor: 2023.04.1
-----------------------------------------------------------
 Please, share the above information when looking for help
 or support in, e.g., GitHub, forums or the Discord chat.
-----------------------------------------------------------
cont-init: info: /etc/cont-init.d/00-banner.sh exited 0
cont-init: info: running /etc/cont-init.d/01-log-level.sh
cont-init: info: /etc/cont-init.d/01-log-level.sh exited 0
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started
added 80 packages, and audited 81 packages in 14s
23 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities
npm notice 
npm notice New minor version of npm available! 9.1.2 -> 9.6.5
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v9.6.5>
npm notice Run `npm install -g [email protected]` to update!
npm notice 
System locale: en-US
Luxon locale set to: nl-BE
System timeZone: Europe/Brussels
Luxon timezone set to: Europe/Brussels
Previously stored events posted to sensor(s) at: Mon Apr 24 2023 10:54:18 GMT+0200 (Central European Summer Time)
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
Calendar(s) queried at: Mon Apr 24 2023 11:30:01 GMT+0200 (Central European Summer Time)
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
postEventsAllCalendars error: TypeError: Cannot read properties of undefined (reading 'length')
Events posted to sensor(s) at: Mon Apr 24 2023 11:30:01 GMT+0200 (Central European Summer Time)
axios error: [object Object]Request failed with status code 401
axios error: [object Object]Request failed with status code 401
axios error: [object Object]Request failed with status code 401
axios error: [object Object]Request failed with status code 401

Hello everyone,

For me it works now.

But the “caldavUrl:” setting should not be
https://pXX-caldav.icloud.com/{DSid}/calendars/{pGUID}” or
https://pXX-caldav.icloud.com/{DSid}/calendars/{calendarName}
but
https://pXX-caldav.icloud.com/{DSid}/calendars/{GUID}”.

This way I receive all data very well.

However, if I
https://pXX-caldav.icloud.com/{DSid}/calendars/{pGUID} or
https://pXX-caldav.icloud.com/{DSid}/calendars/{calendarName}
I keep getting
´´´´
axios error: [object Object]Request failed with status code 404
´´´´
error message.

At least this way it works for me!!

Thank you to all the people who gave me a little feedback!!

I am trying to use your code and I am struggling to have the sensor populate with anything. On restart, I get the agenda sensor but its empty. Following the RESTful documentation, I used the same details and used cURL and that returned calendar events. Here is what I I have in my config

  - platform: rest
    resource: https://myurl.com/api/calendars/calendar.calendar_me
    name: Agenda
    unique_id: calendar_agenda
    method: GET
    scan_interval: 900
    headers:
      authorization: !secret rest_events_token
    params:
      start: >
        {{ utcnow().strftime('%Y-%m-%dT%H:%M:%S.000Z') }}
      end: >
        {{ (utcnow() + timedelta(days=5)).strftime('%Y-%m-%dT%H:%M:%S.000Z') }}
    value_template: >
      {% set events = value_json %}
      {% for event in events %}
        {% if event.start.dateTime %}
          {{ (event.start.dateTime | as_datetime).strftime('%Y-%m-%d %H:%M') }}: {{ event.summary }}
        {% else %}
          {{ event.start.date }}: {{ event.summary }}
        {% endif %}
      {% endfor %}

I have a calendar and it has events in the last 5 days and next 5 days. I’ve set up a long-term token in my secret file, I’ve checked the logs and there is nothing in there on this.
I used this command from the docs (modifying for my instance)

curl \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  "https://myurl.com/api/calendars/calendar.calendar_me?start=2022-05-01T07:00:00.000Z&end=2022-06-12T07:00:00.000Z"

and it returns events.

Have I missed a vital step? I’d really appreciate a steer.

Thanks in advance.

This looks very similar. I’m not sure what the problem is from spot checking, but maybe I can suggest a couple things.

  • turn up debug logging for the rest component homeassistant.components.rest and see if there are any details in the logs about failed requests
  • make the template return “foo” to confirm its running
  • set the return value to " {{ value_json }} " and you can dump the whole thing and make sure that is working

Perfect - thanks for the pointer