Welcome, if you enjoy these posts please consider staying updated via RSS Feed.

Displaying Office 365 Calendars on your Electronic Shelf Labels

Posted June 4th, 2024 by tomba | No Comments

Openepaperlink is an awesome way of repurposing Electronic Shelf Labels. The awesome work by the community has allowed using them to display anything you like.

Home Assistant is my favorite Domotica solution so using the info from the numerous sensors placed all around my house to be displayed on the nice ePaper tags felt like a nobrainer. Apparently I was not the only one because the amazing Jonas Niesner created a HA Plugin just with that in mind. In no time I had several info tags to place around the house:

Since I also bought a larger display (4.2″) I wanted to show the coming appointments on it so I wouldn’t have to take out my phone. Since I already had the tags connected to my Home Assistant I decided to try and use that as a central hub for this.

Adding an Office 365 Calendar to HA

Since my personal calendars are not Google based but hosted on Office 365 (which in true Microsoft style is not very open) I needed to do some extra work to get my calendar visible in HA. These are the steps I needed to take:

  • Log in to Outlook.
  • Open settings and go to Calendar –> Shared Calendars.
  • Select the Calendar you want to share and click Publish
  • Copy the ICS link that’s generated. (note that anyone that has this link can view your Calendar so make sure not to share it!

To allow automatic import to the Home Assistant calendar we need a plug-in. I use ics_calendar as it’s installable through HACS.

Now open your configuration.yaml, add the ICS link you copied earlier and restart your Home Assistant


calendar:
- platform: ics_calendar
  calendars:
      - name: "test"
         url: "https://outlook.live.com/owa/calendar/xxxxx/xxxx/calendar.ics"

If everything was setup correctly your HA Calendar should start populating, so let’s create the automation which will draw the info for the next 7 days to the ePaper:

alias: Update ePaper Calendar
description: ""
trigger:
  - platform: time_pattern
    minutes: /15
condition: []
action:
  - delay:
      hours: 0
      minutes: 4
      seconds: 0
      milliseconds: 0
    enabled: true
  - service: calendar.get_events
    data:
      start_date_time: >-
        {{(now()).strftime('%Y-%m-%d') }} {{ now().hour }}:{{ now().minute }}:{{
        now().second }}
      end_date_time: "{{(now()).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events1
    alias: Get Appointments for today
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -1)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -1)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events2
    alias: Get Appointments for today +1
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -2)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -2)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events3
    alias: Get Appointments for today +2
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -3)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -3)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events4
    alias: Get Appointments for today +3
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -4)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -4)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events5
    alias: Get Appointments for today +4
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -5)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -5)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events6
    alias: Get Appointments for today +5
  - service: calendar.get_events
    data:
      start_date_time: "{{(now() - timedelta( days = -6)).strftime('%Y-%m-%d') }} 00:00:00"
      end_date_time: "{{(now() - timedelta( days = -6)).strftime('%Y-%m-%d') }} 23:59:59"
    target:
      entity_id: calendar.test
    response_variable: cal_events7
    alias: Get Appointments for today +6
  - service: open_epaper_link.drawcustom
    target:
      entity_id:
        - open_epaper_link.000002b338873418
    data:
      background: white
      rotate: 180
      payload:
        - type: text
          value: Agenda
          font: rbm.ttf
          x: 15
          "y": 7
          size: 30
          color: red
        - type: text
          "y": 8
          value: "{{(now()).strftime('%d-%m-%Y') }} "
          font: ppb.ttf
          x: 400
          size: 14
          anchor: rt
          color: red
        - type: text
          "y": 36
          value: "{{(now()).strftime('%H:%M') }}"
          font: ppb.ttf
          x: 399
          size: 10
          anchor: rb
          color: red
        - type: rectangle
          x_start: 0
          x_end: 400
          y_start: 38
          y_end: 39
          fill: red
          outline: red
          width: 2
        - type: multiline
          value: >
            {% if cal_events1['calendar.test'].events | count != 0 %}
            {{(now()).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events1['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
 
            {% if cal_events2['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -1)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events2['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}


            {% if cal_events3['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -2)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events3['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
 
            {% if cal_events4['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -3)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events4['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
 
            {% if cal_events5['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -4)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events5['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
 
            {% if cal_events6['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -5)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events6['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
 
            {% if cal_events7['calendar.test'].events | count != 0 %}{{(now() -
            timedelta( days = -6)).strftime('%d-%m-%Y') }} |{% endif %}
 
            {% for event in
            cal_events7['calendar.test'].events|sort(attribute='start') -%} {%
            if as_timestamp(event.start) | timestamp_custom('%H:%M') ==
            "00:00"%} All day: {{ event.summary }}|{% else %} {{
            (event.start|as_datetime).strftime('%H:%M') }}-{{
            (event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }}
            |{% endif -%}{% endfor -%}
          font: rbm.ttf
          start_y: 50
          offset_y: 20
          delimiter: "|"
          x: 10
          size: 15
          color: black
    alias: Draw it to 4.2 ePaper
mode: single

Some explanation:

{{(now() - timedelta( days = -1)).strftime('%Y-%m-%d') }}

Expands to tomorrow since timedelta(days = -1) adds 24 hours (e.g. 2023-08-15)

{% if cal_events1.events | count != 0 %} {{(now()).strftime('%d-%m-%Y') }} #{% endif %}      

If there are no actual events on a specific data I don’t want to show anything so we evaluate if the number of events is not 0. If it’s not we display the date, e.g. 15-08-2023

{% for event in cal_events1.events|sort(attribute='start') -%} {% if as_timestamp(event.start) | timestamp_custom('%H:%M') == "00:00"%} All day: {{ event.summary }}#{% else %} {{(event.start|as_datetime).strftime('%H:%M') }}-{{(event.end|as_datetime).strftime('%H:%M') }}: {{ event.summary }} #{% endif -%}{% endfor -%}

Now we enumerate all events in the Response Data. If it’s an all Day event (by checking for starttime of 00:00) we don’t show start/end time but in stead All Day:. If it’s not an all day event we show the start and end time.

More than one Office 365 Calendar

Since Home Assistant only allows enumerating one calendar using the list_events service and I’ve got 3 Office 365 calendars, a waste calendar and a favorite football team I want to see the upcoming events for we need to do some bash magic:

cd /var/www/cal
wget https://outlook.live.com/owa/calendar/xxxxx/cid-xxxx/calendar.ics -O 1.ics
wget https://outlook.live.com/owa/calendar/xxxxx/cid-xxxx/calendar.ics -O 2.ics
wget https://www.voetbalkrant.com/soccer/calendar/team/team_9_nl.ics -O 3.ics
wget http://192.168.1.24:8123/local/afval.ics -O 4.ics
wget https://outlook.live.com/owa/calendar/xxxxx/cid-xxxx/calendar.ics -O 5.ics
#Remove last line of 1.ics
sed -i '$d' 1.ics
#Remove first 20 lines of 2.ics
sed -i '1,20d' 2.ics
#Remove last line of 2.ics
sed -i '$d' 2.ics
#Remove first 7 lines of 3.ics
sed -i '1,7d' 3.ics
#Remove last line of 3.ics
sed -i '$d' 3.ics
#Remove first 20 lines of 5.ics
sed -i '1,20d' 5.ics
#Remove last line of 5.ics
sed -i '$d' 5.ics
#Remove first 8 lines of 4.ics
sed -i '1,8d' 4.ics

cat 1.ics 2.ics 3.ics 5.ics 4.ics > temp.txt
# now strip out the overlapping lines and make one big valid ICS file
cat temp.txt | grep -v DARBEG > comb.ics
# Fix Microsofts weird timezones:
sed -i 's/"tzone:\/\/Microsoft\/Custom"/Europe\/Amsterdam/g' comb.ics
sed -i 's/Customized Time Zone/Europe\/Amsterdam/g' comb.ics
sed -i 's/W. Europe Standard Time/Europe\/Amsterdam/g' comb.ics

We need to remove all superfluous headers and footers (20 lines for Office365 ICS files, 7 for the football feed and 8 for the Waste calendar) and since Microsoft uses invalid TZone variables we also need to fix those. sed is the obvious choice for that so I used that. I create a simple apache2 website to host the comb.ics file and changed the calendar test to point to that ics file

calendar:
- platform: ics_calendar
  calendars:
      - name: "test"
         url: "http://192.168.1.24/comb.ics"

Works like a charm 🙂

Posted in category: openepaperlink | Tags:

A MiniPC for BlueIris (and a bit more :) )

Posted November 27th, 2023 by tomba | No Comments

I’ve been using Blue Iris for quite some time now but after adding another 4K cam I decided it was time for a dedicated machine. As I was very intrigued by the new Nxxx series by Intel and am a big fan of small PC’s I decided to hunt for a small i3-N305 based PC. The cool thing about the Nxxx series is that it completely consists of the Intel E-cores used in their 12th gen processor in stead of a Big-little design like regular Intel processors. After some browsing I stumbled on a dirt cheap Minisforum UN305 MiniPC!

Luckily for me Amazon had a great deal so after 2 days I got the 16GB/500GB version for less than 300€! Unfortunately not all was well. The included SSD (a Vickter, never heard of this brand) was abysmally slow and the included WiFi card was an Intel AC7265 which means no WPA3 on Windows, which to me seems pretty useless. Since I wasn’t planning on using the WiFi at all no biggie but still 😉

UN305 with underside removed
Top of motherboard showing the SSD and RTC battery
The less than stellar included SSD and WiFI card

After replacing the SSD with an Adata SX8200 and installing Windows 2022 Server however I ran into the first real issue. The noise of the tiny fan was unbearable and the fan setting adjusments I made in the BIOS were completely ignored it seamed by the machine.

Massive amounts of settings but useless if ignored of course…

Even worse, the fan control used not only ignored BIOS settings but was also undetectable by any of the Windows tools I tried including Fancontrol which meant that I had to resort to more drastic measures. So I removed the included fan and the enormous blob of cooling paste and replaced it with Arctic MX-4 plus I added 2 USB powered fans in the hopes of keeping it cool enough to make it usable.

After this change the machine became somewhat usable. Not enough to put on my desk, but silent enough to put it in the cupboard I had planned to put it in. I put in production with the following specs:

  • Intel i3-N305 processor
  • 16GB LP-DDR5
  • Dual Realtek 1Gbit NIC
  • Adata SX8200 500GB SSD
  • Micron 5200 ECO 7.68TB
  • Windows Server 2022 with Hyper-V role
  • Blue Iris 5

Besides the fact I could still hear the fan when holding my ear to cupboard it was in I was quite happy with it. The 8 core N305 felt enormously snappy and everything was running smoothly until after about a month it didn’t. The machine started crashing spectacularly and without so much as a BSOD. By rebooting it every 2 hours I kept it running for a couple of more days until it completely died and wouldn’t even post anymore. After contacting Amazon they told me they could not replace the machine and would be reimbursing me in full, so excellent service there!

Because I didn’t want to run into the same issues again I decided to go for a more established brand and landed on a MSI Cubi 5-12M . This machine was not available with a i3-N30x processor but they did have a barebone with the Intel i5-1235U which is essentually the N305 with 2 added P-cores (which allow for 12 threads in total since the P-cores support Hyper Threading) and since I had some laptop DDR4 laying around I was also able to spec the machine out a bit more than the Minisforum:

  • Intel i5-1235U processor
  • 2x16GB DDR4
  • 1x Realtek 2.5Gbit NIC + 1x Realtek 1Gbit NIC
  • Adata SX8200 500GB SSD
  • Micron 5200 ECO 7.68TB
  • Windows Server 2022 with Hyper-V role
  • Blue Iris 5

Since the machine felt a lot faster than the Minisforum (and was dead silent in comparison) and I had already run a lot of performance tests on the i3-N305 I decided to do the same with the MSI:

Todo

The difference is just insane. The machine is much faster (especially storage wise due to x4 in stead of x1 PCIe lanes) and after some tweaking with Fancontrol is just completely quiet. This is the machine I should have bought to begin with!

Posted in category: Uncategorized | Tags: