Populate local Home Assistant calendar with school "specials" based on cycle days using Appdaemon
This app provides a way to keep track of school specials based on cycle days rather than days of the week. The app is based on a five cycle day system where school "specials" (art, music, library, etc.) run on the same cycle day but not the same day of the week. Each time there's a snow day or in-service day, etc., the cycle days stop, and then they start back up when school is back in session. This has become very difficult to manage.
This app relies heavily on Home Assistant (HA) created entities, and using the HA interface to trigger different tasks. Below are the helpers that need to be created. Because HA does not (yet) have a way to programmatically delete events, the delete events button deletes the physical calendar file (.ics), and then it's recreated when adding a new calendar entry.
Add the createDate.py in your apps folder, and modify apps.yaml as necessary. The yaml file I included has only this application, so you will need to add the text to any other apps you have already installed. This app is driven off of HA helper entities: input_datetime, input_text, and input_button. The intent was to have HA store the values so they were readable to the user and persisted after restarts.
If the app is not showing up on the HACS repository list, you will need to go into your HA settings and into the configuration for HACS. There, be sure the Enable AppDaemon apps discovery & tracking is enabled.
Each of these helper entities are explained in detail in the comments at the top of the application. You will need to create the following entities (the ones I created are listed):
- non_school_days: input_text.non_school_days
- added_date: input_datetime.add_non_school_day
- cycle_day_holidays: input_text.cycle_day_holidays
- start_date : input_datetime.cycle_start_day
- end_date : input_datetime.cycle_end_day
- cycle_day_1: input_text.cycle_day_1
- cycle_day_2: input_text.cycle_day_2
- cycle_day_3: input_text.cycle_day_3
- cycle_day_4: input_text.cycle_day_4
- cycle_day_5: input_text.cycle_day_5
- day_number: input_number.cycle_day_restart_day
- button_entity_for_adding_dates: input_button.rerun_calendar_cycle_days
- button_entity_to_list_holidays: input_button.cycle_day_list_holidays
- button_entity_to_add_non_school_day: input_button.add_non_school_day
- button_entity_to_clear_non_school_days: input_button.clear_non_school_days
- button_entity_to_delete_non_school_day: input_button.delete_non_school_day
- button_entity_to_delete_calendar_events: input_button.delete_calendar_events
- button_entity_to_delete_holidays: input_button.delete_holidays
- button_entity_to_add_dates_from_other_calendar: input_button.add_dates_from_other_calendar
- button_entity_to_refresh_calendar_list: input_button.refresh_calendar_list
- calendar_list: input_select.calendar_list
- include_holidays_in_calendar: input_boolean.include_holidays_in_calendar
- include_weekends_in_calendar: input_boolean.include_weekends_in_calendar
- delete_and_rerun_calendar_cycle_days: input_button.delete_and_rerun_calendar_cycle_days
- system_message: input_text.system_message
- current_calendar: input_text.current_calendar
If you change any of the names (the text above before the :), you'll need to replace them in the code. I did my best not to hard code anything, and instead use self.args["INPUT NAME"]. In addition, you will need to create a Bearer Token to access the REST API. Instructions for creation are provided here. As a warning, you must put the word Bearer in front of the created token to designate it as a bearer token.
I put the file path for the .ics file as part of my secrets.yaml file, but you can just add it directly into apps.yaml. The same is true for the Bearer token as described above.
You can add the code from school_cycle_days_dashboard.yml to make a separate dashboard for this app. This file closely follows the pictures below.
This is the main input/status screen. From here, you can add and delete non-school days, add and delete holidays, and finally, add those cycle days and their associated specials to your local HA calendar.
Once you add holidays and non-school days, this is the interface you can continue to add non-school days or delete entries you already added.
This is where you add non-school days (in-service, snow days, etc.). Any manually added days will be added to the holidays in your selected region. When running the calendar cycle days (to add the dates to your calendar), you can select the date range for it to run. This allows you to start from today or yesterday, for example, when you have to rerun the calendar due to a snow day, etc.
HA currently does not have the ability to edit or delete events through automations. In order to avoid duplication of calendar entries, you should delete all calendar events before re-running the cycle days. (See below) Once HA implements editing and deleting events programmatically, I'll modify this app. As the HA API currently works, you can only add events.
Sometimes schools create their own calendars which can be exported into .ics format. In that case, you need to add the calendar to HA first and use the upload function. The app then allows you to select that calendar from a dropdown and add entries from the school's .ics file into the non-school days. This function searches for "No School". Other entries (concerts, field day, etc.) are not included as part of the pull from the .ics file.
To accomplish the pull since AppDaemon does not yet allow receiving return responses from service calls (like a list of calendar events), I used the icalendar library to read through the .ics file, parse the data, and then add the dates from that calendar to the non-school days already in place. The app checks to see if a date has already been added in order to avoid duplication. You can delete non-school day entries from there.
Though it worked well the first year, when I tried to set up the second year, the calendar was not formatted properly which caused Home Assistant to error out. I created a no_school_calendar.py to pull all entries from a .ics file where the summary had the words "No School." To make this work, you specify an input and output file at the top of the file, like input_file = "calendar.ics" and output_file = "no_school_clean.ics". Then just run the python script, and it will create a much smaller .ics file to be imported into Home Assistant.
Warning: This process can take a minute or two depending on how many entries are in the other calendar. Please be patient. A system message will appear once complete showing how many entries have been added to the list.
This app incorporates the holidays python import. The app can be configured as described in its documentation. You can either use the holiday list as specified or delete the holidays and add the non-school days manually. Once you have set up your preferred list of holidays, you need to run the task to add the holidays to the list of non-school days.
HA does not currently have a way to delete individual events through an automation, but I'm hoping that will change in the near future. In the meantime, the Delete Calendar Events button physically deletes the .ics file from the .storage folder. HA keeps a pointer to that calendar, and the .ics will be recreated upon adding at least one event.
The Delete and ReRun Calendar Cycle Days function allows you to delete the calendar and re-run the entire calendar again (based on your start and end dates). This short function removes the need to delete the calendar by hitting a button and then rerunning the cycle days.
This is what the local HA calendar looks like once you have added the cycle days.
Sample calendar event when you click on an entry.
This is my first python program, so I am positive that the code is a lot less efficient than it could have been. It works, and so I'm putting it out there for others. I have heavily commented the code both for my later coding as well as others so you can understand my logic. If you have suggestions for improving the code and/or want new features, please create a PR, and I'll do my best. I enjoy this even if it can be a bit frustrating at times.
