Die eigentliche Idee war es ein MagicMirror aufzubauen, doch im Verlauf stellten sich mit dem Spiegel bzw. der halbdurchlässigen Folie viele Probleme heraus sodass die Vorstellungskraft einen neuen Weg suchte und auch fündig wurde. Der Film „Per Anhalter durch die Galaxis“ kam in den Sinn. Wer Ihn nicht kennt, es geht um einen Erdling der erfährt, dass sein Planet eigentlich von Außerirdischen erbaut wurde und nun vor der Zerstörung steht (amüsanter Film mit einigen Tücken). Eben in diesem Film spielt auch ein Computer namens „Deep Thought“ eine wichtige Rolle. Er soll die Antwort auf „alles“ finden. Da er zu Urzeiten gebaut wurde sieht er aus wie ein etwas unförmiger Buddha auf einer Aztekenpyramide. Damit war die Grundform für das neue Modell klar.
Inhalt
Das Modell
Da es sich um eine komplexere Form handelt mit Grundplatte, Bau, Kopf, Hals und Händen wurde Keramiplast gewählt. Dies ist gut zu formen (im nassen Zustand), bricht nicht allzu leicht – insbesondere auch beim Aushärten (im Gegensatz z.B. zu Ton).
Der erste Versuch widmete sich dem Kopf. Da hier auch die Elektronik untergebracht werden sollte musste ein stabiler Rahmen her sodass dieser aus Balsaholz erstellt wurde. Anschließend wurde das Keramiplast in Platten mit einer Glasflasche ausgerollt und im Nassen Zustand aufgetragen. Beim Aushärten blieb des entsprechend auf der Holzplatte kleben, sodass einige Risse entstanden, welche letztlich dekorativ wirken.
Der Rumpf wurde zunächst mit Zeitung geformt und anschließend auch mit Keramiplast Platten überzogen (wenn es zu trocken wird mit Wasser gut nacharbeiten – die Platten verbinden sich durch Wischen mit dem Finger hervorragend).
Hardware
Das Herzstück des Deep Thought ist ein Raspberry Pi (Model 4B 2GB). Dieser eignet sich aufgrund seiner Vielzahl von Input und Outputs, welche alle angedachten Sensoren und Ausgabegeräte bedienen kann. Ebenfalls ist für diese Plattform bereits viel programmiert worden, sodass man nicht das Rad neu erfinden muss.
Sollten im Verlauf mit den I2C Geräten Probleme auftauchen sollte immer geprüft werden, ob diese überhaupt erkannt werden bzw. welche Adresse diese verwenden:
sudo i2cdetect -y 1
Inputs
Die Real Time Clock (DS3231)
Der Pi selbst hat leider keine Echtzeituhr an Board. Das ist normalerweise kein Problem, da das Raspbian OS in gewissen Abständen über Zeitserver via Internet die aktuelle Uhrzeit abfrägt und so eine ausreichende Genauigkeit besteht. Ist jedoch keine Internetverbindung gegeben bzw. möchte man die Zeit autark Einstellen ist ein zusätzliches Modul notwendig.
Dies lässt sich am einfachsten über ein z.B. RTC DS3231 I2C Modul realisieren. Hierbei schwingt auch ein präzises Quarz welches mittels Temperaturmessungen zusätzlich korrigiert wird.
Grundlegend aktiviert werden kann das Modul in der config.txt.
sudo nano /boot/config.txt
dtparam=i2c_arm=on
dtoverlay=i2c-rtc,ds3231
Außerdem sollte man die „Fake“ Hardware Uhr des RPi OS deaktivieren.
sudo systemctl disable fake-hwclock
Da später ein Python-Skript die Synchronisierung übernimmt muss diese nicht explizit eingerichtet werden.
Testen lässt sich das Modul schließlich über:
sudo hwclock
Helligkeitssensor (BH1750)
Zur Steuerung der Bildschirmhelligkeit sowie der Helligkeit der Neopixel wurde ein Helligkeitsensor etabliert. Auch hier bietet sich ein I2C Modul z.B. BH1750 an. Als Bibliothek dient hier:
wget https://bitbucket.org/MattHawkinsUK/rpispy-misc/raw/master/python/bh1750.py
Bewegungssensor (PIR)
Da man nicht ständig in der Nähe des Deep Thoughts ist und man sich damit dann auch die Energie für die Anzeige sparen kann wurde ebenfalls ein Bewegungssensor integriert.
Hier bietet sich ein einfacher PIR Sensoren an, welcher durch einen Potenziometer für eine bestimmte Schwelle kalibriert werden kann. Bei Erreichen der Schwelle wird über einen einzelnen Pin ein digitales Signal ausgegeben, welches über einen einfach GPIO Port (digital) wahrgenommen werden kann.
Gestensensor (APDS-9960)
Um später zwischen verschiedenen Seiten (Bildschirmen) wechseln zu können, wurde auch ein APDS-9960 Gestensensor ebenfalls über die I2C Schnittstelle, integriert. Die Anbindung wird auch hier über ein bestehendes Paket gelöst.
pip3 install adafruit-circuitpython-apds9960
Outputs
Bildschirm
Das offizielle DSI-Display der Raspberry Pi Foundation wird zur Anzeige verwendet. Dies hat den Vorteil, dass es einfach angeschlossen werden kann, die Stromversorgung über den Raspberry Pi gelöst wird und auch die Display Hintergrundbeleuchtung gesteuert werden kann (hier ist zu erwähnen, dass diese auch tatsächlich vollständig ausgeschalten werden kann, was bei anderen DSI-Displays nicht selbstverständlich ist).
Die Display Hintergrundbeleuchtung lässt sich steuern mittels des Paketes
pip3 install rpi-backlight
Das aktuell RPI OS hat wohl ein Problem bei manchen RPi’s mit der I2C Schnittstelle, welches dazu führt, dass sich beim Starten der Bildschirm mit Linien füllt und nichtmehr reagiert. Bisher hat letztlich nur folgender Workaround in der /boot/config.txt geholfen:
dtparam=i2c_vc_baudrate=50000
Neo-Pixel LED
Zur erweiterten visuellen Anzeige gehört auch ein Neo-Pixel LED Ring, welcher unterhalb einer alten Röhre angebracht ist und diese so von unten beleuchtet. Die Ansteuerung erfolgt hier über das Paket
pip3 install adafruit-circuitpython-neopixel
Sound (WM8960 Audio HAT)
Zur Audioausgabe wurde ein SPI/I2C Audio Hat gewählt. Die Installation findet sich auf GitHub.
https://github.com/waveshare/WM8960-Audio-HAT
Deaktiviert man in der /boot/config.txt mittels
# Enable audio (loads snd_bcm2835)
dtparam=audio=off
den RPi Audio Chip, wird primär automatisch das WM8960 verwendet. Audio Einstellungen können über den Alsamixer vorgenommen und gespeichert werden.
alsamixer
sudo alsactl store
Die Software
Grundlegend basiert die Installation auf dem Raspbian OS (bullseye).
Wie immer sollte vor weiteren Installationen eine Upgrade des Basissystems durchgeführt werden.
sudo apt-get update
sudo apt-get upgrade
Über das Raspbian eine Konfigurationstool müssen die I2C- / SPI-Schnittstelle aktiviert werden.
sudo raspi-config
#3 Interface Options
MagicMirror
Die Gruppe um die Node.js / Electron basierte Software „MagicMirror“ hat eine solide Basis für Magic Mirror’s und ähnliche Projekte geschaffen.
Die Installationsmöglichkeiten sind vielseitig. Auch gibt es bereits ein vorgefertigtes MagicMirror OS basierendes auf Raspbian OS. Jedoch ist es manchmal besser alles unter Kontrolle zu haben. Ein weiterer User stellt Skripte zu Verfügung, welche die meisten Arbeiten für eine Installation des MM abnehmen.
https://github.com/sdetweil/MagicMirror_scripts
Für diese Grundsoftware gibt es wiederum Module, welche integriert werden können. Hier finden sich einige bereits in der Grundinstallation, weitere können über GIT bezogen werden (Anleitungen finden sich zu genüge. Im Grunde ist die Befehlsreihenfolge meist: git clone REPOSITORY; npm install.
Die Konfiguration findet schließlich in der Datei /home/pi/MagicMirror/config/config.js statt.
Die Aktuelle Installation / Konfiguration sieht folgendermaßen aus:
let config = {
address: "localhost",
port: 8080,
basePath: "/", // The URL path where MagicMirror is hosted. If you are using a Reverse proxy
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "de",
locale: "de-DE",
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24,
units: "metric",
modules: [
{
module: 'MMM-pages',
config: {
modules:
[[ "clock","MMM-TouchAlarm", "calendar","MMM-Volumio", "MMM-BMP-sensor","MMM-MQTT", "MMM-MoonPhase"],
[ "MMM-connection-status", "MMM-SmartWebDisplay"]],
fixed: ["alert", "updatenotification", "MMM-Wallpaper"],
}
},
{
module: "MMM-MoonPhase",
position: "bottom_center",
config: {
updateInterval: 43200000,
hemisphere: "N",
resolution: "detailed",
basicColor: "white",
title: true,
phase: true,
x: 100,
y: 100,
alpha: 0.7
}
},
{
module: 'MMM-SmartWebDisplay',
position: 'middle_center', // This can be any of the regions.
config: {
// See 'Configuration options' for more information.
logDebug: false, //set to true to get detailed debug logs. To see them : "Ctrl+Shift+i"
height:"100%", //hauteur du cadre en pixel ou %
width:"100%", //largeur
updateInterval: 1, //in min. Set it to 0 for no refresh (for videos)
NextURLInterval: 0.5, //in min, set it to 0 not to have automatic URL change. If only 1 URL given, it will be updated
displayLastUpdate: false, //to display the last update of the URL
displayLastUpdateFormat: 'ddd - HH:mm:ss', //format of the date and time to display
url: ["/modules/jarvis/python/jarvis.log"], //source of the URL to be displayed
scrolling: "yes", // allow scrolling or not. html 4 only
shutoffDelay: 10000 //delay in miliseconds to video shut-off while using together with MMM-PIR-Sensor
}
},
{
module: 'MMM-websocket',
config: {
// See below for configurable options
}
},
{
module: 'MMM-MQTT',
position: 'bottom_left',
header: 'Sensoren',
config: {
logging: false,
useWildcards: false,
mqttServers: [
{
address: '192.168.178.37', // Server address or IP address
port: '1883', // Port number if other than default
user: 'XXX', // Leave out for no user
password: 'XXX', // Leave out for no password
subscriptions: [
{
topic: 'Omg433/OpenMQTTGateway_rtl_433_ESP/RTL_433toMQTT/Bresser-3CH/130', // Topic to look for
jsonpointer: '/temperature_C',
label: 'Temp. (out)', // Displayed in front of value
suffix: '°C', // Displayed after the value
decimals: 1, // Round numbers to this number of decimals
sortOrder: 10, // Can be used to sort entries in the same table
maxAgeSeconds: 60, // Reduce intensity if value is older
broadcast: true, // Broadcast messages to other modules
},
{
topic: 'Omg433/OpenMQTTGateway_rtl_433_ESP/RTL_433toMQTT/Bresser-3CH/130', // Topic to look for
jsonpointer: '/humidity',
label: '%(out)', // Displayed in front of value
suffix: '%', // Displayed after the value
decimals: 1, // Round numbers to this number of decimals
sortOrder: 10, // Can be used to sort entries in the same table
maxAgeSeconds: 60, // Reduce intensity if value is older
broadcast: true, // Broadcast messages to other modules
},
{
topic: 'tele/tasmota_5FFDE8/SENSOR', // Topic to look for
jsonpointer: '/SCD30/CarbonDioxide',
label: 'CO2', // Displayed in front of value
suffix: 'ppm', // Displayed after the value
decimals: 1, // Round numbers to this number of decimals
sortOrder: 10, // Can be used to sort entries in the same table
maxAgeSeconds: 60, // Reduce intensity if value is older
broadcast: true, // Broadcast messages to other modules
},
{
topic: 'Omg433/OpenMQTTGateway_rtl_433_ESP/RTL_433toMQTT/Bresser-3CH/6', // Topic to look for
jsonpointer: '/temperature_C',
label: 'in ', // Displayed in front of value
suffix: '°C', // Displayed after the value
decimals: 1, // Round numbers to this number of decimals
sortOrder: 10, // Can be used to sort entries in the same table
maxAgeSeconds: 60, // Reduce intensity if value is older
broadcast: true, // Broadcast messages to other modules
},
{
topic: 'Omg433/OpenMQTTGateway_rtl_433_ESP/RTL_433toMQTT/Bresser-3CH/6', // Topic to look for
jsonpointer: '/humidity',
label: 'in %', // Displayed in front of value
suffix: '%', // Displayed after the value
decimals: 1, // Round numbers to this number of decimals
sortOrder: 10, // Can be used to sort entries in the same table
maxAgeSeconds: 60, // Reduce intensity if value is older
broadcast: true, // Broadcast messages to other modules
},
]
}
],
}
},
{
module: "alert",
},
{
module: 'MMM-connection-status',
header: "Internet Connection",
position: 'top_left', // Or any valid MagicMirror position.
config: {
}
},
{
module: "MMM-Wallpaper",
position: "fullscreen_below",
config: { // See "Configuration options" for more information.
source: "chromecast",
caption: false,
filter: "grayscale(0.7) brightness(0.2)",
slideInterval: 60 * 1000 // Change slides every minute
}
},
{
module: "updatenotification",
position: "bottom_bar"
},
{
module: 'MMM-Remote-Control',
config: {
showModuleApiMenu: false, // Optional, Enable the Module Controls menu
}
},
{
module: "clock",
position: "top_left",
config: {
displaySeconds: false,
displayType: "digital",
analogSize: "100px",
dateFormat: "dd, DD. MMMM"
}
},
{
module: 'MMM-TouchAlarm',
position: 'top_left',
config: {
snoozeMinutes: 10, // I want to snooze longer
alarmTimeoutMinutes: 5, // Stop the alarm automatically after 5 minutes
alarmSoundFile: 'blackforest.mp3', // Play some birds
alarmSoundFadeSeconds: 60 // Increase the volume slowly
// ...
}
},
{
module: 'calendar',
position: 'top_center', // This can be any of the regions. Best results in left or right regions.
config: {
tableClass: "xsmall",
animationSpeed: 0,
maximumNumberOfDays: 120,
maximumEntries: 10,
fetchInterval: 60000,
calendars: [
{
url: 'http://localhost:8080/modules/calendars/home.ics',
symbol: 'Privat'
},
{
url: 'http://localhost:8080/modules/calendars/work.ics',
symbol: 'Work'
}
]
}
},
{
module: 'calendar',
position: 'top_right', // This can be any of the regions. Best results in left or right regions.
config: {
tableClass: "xsmall",
animationSpeed: 0,
maximumNumberOfDays: 120,
maximumEntries: 10,
fetchInterval: 60000,
calendars: [
{
url: 'http://localhost:8080/modules/calendars/birthdays.ics',
symbol: 'birthday-cake'
}
]
}
},
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}
Das Python-Skript
Um die I2C Geräte anzusteuern und Routinen auszuführen wird ein Python Skript als system.d Service ausgeführt.
import http.client as httplib
import logging
import sys
logging.basicConfig(filename='mmm.log', filemode='w', level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
import os
import smbus
import random
import subprocess
import time
from threading import Thread
import RPi.GPIO as GPIO
from modules.nextcloud import NCScan
import bh1750
import busio
import neopixel
import requests
from adafruit_apds9960.apds9960 import APDS9960
import board
print(board.board_id)
from board import SCL, SDA
from routines import createbirthdaycalendar
from rpi_backlight import Backlight
import requests
# Global Variables
# ========================================================
act_lcd_brightness_level = 0
disableLampChange = False
last_motion_time = time.time()
display_turned_off = False
SHUTOFF_DELAY = 60 # seconds
backlight = Backlight() # Backlight Driver
# GPIO Setting
# ========================================================
GPIO.setmode(GPIO.BCM) # set up BCM GPIO numbering
GPIO.setup(16, GPIO.IN) # PIR Pin
# Change working directory to file directory
# ========================================================
dir_path = os.path.dirname(os.path.realpath(__file__))
os.chdir(dir_path)
# logging.info("working dir: "+os.getcwd())
# PIR Sensor
# ========================================================
GPIO.add_event_detect(16, GPIO.RISING, callback=pircallback)
def displayautooff():
logging.info("backlight pir auto off started")
global display_turned_off, SHUTOFF_DELAY
while True:
if not display_turned_off and time.time() > (last_motion_time + SHUTOFF_DELAY):
backlight.power = False
time.sleep(1)
def pircallback(channel):
# logging.info("pir sensor acitvated")
global last_motion_time
backlight.power = True
last_motion_time = time.time()
# Check Internet Connection
# ========================================================
def checkInternetHttplib(url="www.google.com", timeout=3):
conn = httplib.HTTPConnection(url, timeout=timeout)
try:
conn.request("HEAD", "/")
conn.close()
logging.info("internet connected")
return True
except Exception as e:
print(e)
errorlamp("")
return False
# Check i2c
# ========================================================
# 23: BH1750
# 39: APDS 9960
# 57: WAVESHARE WM8960 i2c control ?
def checki2c():
bus = smbus.SMBus(1) # 1 indicates /dev/i2c-1
devices = list()
for device in range(128):
try:
bus.read_byte(device)
devices.append(hex(device))
except: # exception if read_byte fails
pass
if hex(0x023) not in devices:
errorlamp("BH1750 Sensor not found")
elif hex(0x039) not in devices:
errorlamp("APDS960 Sensor not found")
elif hex(0x057) not in devices:
errorlamp("WM8960 Controller not found")
else:
logging.info("i2c devices found: " + str(devices))
# APDS9960 Driver
# ========================================================
i2c = busio.I2C(SCL, SDA)
apds = APDS9960(i2c)
apds.enable_proximity = True
apds.enable_gesture = True
def readgesture():
while True:
gesture = apds.gesture()
if gesture == 0x01:
# logging.info("Gesture: up")
return 1
elif gesture == 0x02:
# logging.info("Gesture: down")
return 2
elif gesture == 0x03:
# logging.info("Gesture: right")
return 3
elif gesture == 0x04:
# logging.info("Gesture: left")
return 4
def gesturethread():
while True:
gesture = readgesture()
if gesture == 1:
mmm_showalert_std("Lamp Control", "Links: On/Off, Rechts: Random")
gesture = readgesture()
if gesture == 4:
lamptoggle()
mmm_showalert_std("Lamp Control", "> Toggle Light")
if gesture == 3:
mmm_showalert_std("Lamp Control", "> Random Light")
random_lamp()
elif gesture == 3:
mmm_nextpage()
time.sleep(1)
# NeoPixel
# ========================================================
pixel_pin = board.D12
num_pixels = 7
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=0.1, auto_write=True)
lampOff = False
def errorlamp(errormsg):
logging.error(errormsg)
mmm_showalert("ERROR", errormsg, "1")
pixels.fill((255, 0, 0))
pixels.brightness = 1.0
def fade_lampbrigthness(tolevel):
if disableLampChange:
return
# logging.info("Fade Lamp Brightness to "+str(tolevel))
if lampOff:
pixels.brightness = 0
else:
pixels.brightness = tolevel
def random_lamp():
if disableLampChange:
return
pixels.fill((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
# TTS
# ========================================================
def speak(text):
os.system("echo '" + text + "' | sudo /home/pi/nanotts/nanotts --play -v de-DE --speed 0.7 --pitch 0.8")
# Pi Display
# ========================================================
def fade_displaybrigthness(tolevel):
# logging.info("Fade LCD Backlight to "+str(tolevel))
act_fade_level = backlight.brightness
while (tolevel != act_fade_level):
act_fade_level = backlight.brightness
if tolevel < act_fade_level and act_fade_level > 0:
backlight.brightness = act_fade_level - 1
elif act_fade_level < 100:
backlight.brightness = act_fade_level + 1
time.sleep(0.1)
def setbrigthness(level):
if level < 5:
fade_displaybrigthness(5)
fade_lampbrigthness(0.1)
elif level > 100:
fade_displaybrigthness(100)
fade_lampbrigthness(0.1)
else:
fade_displaybrigthness(level)
fade_lampbrigthness(level / 100 * 2)
def backlightsensing():
logging.info("starting light sensing")
actlevel = -1
while True:
lightLevel = bh1750.readLight() * 3
lightLevel = int(lightLevel)
if lightLevel > 1000:
lightLevel = 1000
levelcorrection = 10
level = int(lightLevel * 100 / 1000) * levelcorrection
if actlevel is not level:
# logging.info("lightlevel:"+str(lightLevel))
setbrigthness(level)
actlevel = level
time.sleep(5)
# MMM
# ========================================================
def mmm_showalert_std(title, msg):
url = 'http://localhost:8080/remote?action=SHOW_ALERT&title="' + title + '"&message="' + msg + '"'
requests.get(url)
def mmm_showalert(title, msg, time):
url = 'http://localhost:8080/remote?action=SHOW_ALERT&title="' + title + '"&message="' + msg + '"&timer="' + time + "'"
requests.get(url)
def mmm_nextpage():
url = 'http://localhost:8080/remote?action=NOTIFICATION¬ification=PAGE_INCREMENT'
requests.get(url)
# RTC
# ========================================================
def syncTime():
while 1:
r = subprocess.call("sudo hwclock -s", shell=True)
createbirthdaycalendar()
# print(r)
time.sleep(3000)
# dirsyncer
# ========================================================
def runvdirsyncer():
while 1:
logging.info("run vdirsyncer")
# Workaround for Sync Error
r = subprocess.call("rm /home/pi/.vdirsyncer/status/iCloud_to_MagicMirror/*", shell=True)
r = subprocess.call("sudo -u pi vdirsyncer -verror sync", shell=True)
# print(r)
time.sleep(900) # 15 Minuten
# MAIN
# ========================================================
if __name__ == '__main__':
checkInternetHttplib()
checki2c()
pixels.fill((128, 255, 255))
logging.info("Thread GESTURE starting")
gesturethread = Thread(target=gesturethread, args=())
gesturethread.start()
logging.info("Thread BACKLIGHTSENSING starting")
backlightsensingthread = Thread(target=backlightsensing, args=())
backlightsensingthread.start()
logging.info("Thread PIR starting")
displayturnedoffthread = Thread(target=displayautooff, args=())
displayturnedoffthread.start()
logging.info("Thread VDIRSYNCER starting")
syncvdirsycner = Thread(target=runvdirsyncer, args=())
syncvdirsycner.start()
logging.info("Thread SYNCTIME starting")
syncTimeThread = Thread(target=syncTime, args=())
syncTimeThread.start()
speak("Jarvis wurde gestartet")
logging.info("Jarvis gestaret")
Systemctl Service
Damit das Skript automatisch ausgeführt wird, kann es dem system.d System übergeben werden
sudo nano /etc/system.d/system/magicmirror-python-script.service
Inhalt:
[Unit]
Description=magicmirror-python-script
[Service]
ExecStart=sudo /usr/bin/python3 -u /home/pi/MagicMirror/modules/jarvis/python/main.py
WorkingDirectory=/home/pi/MagicMirror/modules/jarvis/python/
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
Aktivieren:
sudo systemctl enable magicmirror-python-script.service
Starten:
sudo systemctl start magicmirror-python-script.service
Status:
sudo systemctl status magicmirror-python-script.service
vdirsyncer (iCloud Kalender & Geburtstage)
Eine Besonderheit der Installation ist der Abgleich mit einem iCloud Account. Hierdurch werden Kalender auf dem MagicMirror angezeigt als auch ein Kalender mit Geburtstagen aus den Kontakten des iCloud Accounts generiert.
Das Python-Skript „vdirsyncer“ bietet hier eine einfach Möglichkeit.
Installation:
pip install --user --ignore-installed vdirsyncer
Konfiguration:
sudo nano ~/.vdirsyncer/config
Zunächst müssen hier die Storages definiert werden.
Inhalt (zunächst):
[storage my_calendar_remote]
type = "caldav"
url = "https://caldav.icloud.com/"
# Authentication credentials
username = "youraccount@icloud.com"
password = "XXX" # APP Passwort https://support.apple.com/de-de/HT204397
# We only want to sync in the direction TO the mirror, so we make iCloud readonly
read_only = true
# We only want to sync events
item_types = ["VEVENT"]
url = "https://contacts.icloud.com/"
username = "youraccount@icloud.com"
password = "XXX" # APP Passwort https://support.apple.com/de-de/HT204397
read_only = true
Discover:
Um herauszufinden, wie die Kalender benannt sind kann folgender Befehl ausgeführt werden.
vdirsyncer discover
Die Namen können schließlich unter [pair my_calendar] ... collections = eingetragen werden.
Inhalt (vollständig):
# CALDAV Sync
[pair my_calendar]
a = "my_calendar_local"
b = "my_calendar_remote"
collections = ["home"] #Können mit vdirsyncer discover angezeigt werden
conflict_resolution = "b wins"
# Calendars also have a color property
metadata = ["displayname", "color"]
[storage my_calendar_local]
type = "singlefile"
path = "/home/pi/MagicMirror/modules/calendars/%s.ics"
[storage my_calendar_remote]
type = "caldav"
url = "https://caldav.icloud.com/"
# Authentication credentials
username = "youraccount@icloud.com"
password = "XXX" # APP Passwort https://support.apple.com/de-de/HT204397
# We only want to sync in the direction TO the mirror, so we make iCloud readonly
read_only = true
# We only want to sync events
item_types = ["VEVENT"]
[pair my_contacts]
a = "my_contacts_local"
b = "my_contacts_remote"
conflict_resolution = "b wins"
collections = ["card"]
[storage my_contacts_local]
type = "filesystem"
path = "/home/pi/MagicMirror/modules/contacts/"
fileext = ".vcf"
[storage my_contacts_remote]
type = "carddav"
url = "https://contacts.icloud.com/"
username = "youraccount@icloud.com"
password = "XXX" # APP Passwort https://support.apple.com/de-de/HT204397
read_only = true
Die Funktion createbirthdaycalendar() sieht schließlich folgendermaßen aus:
import os
from datetime import timedelta, datetime
import pytz
import vobject
from ics import Calendar, Event
from ics.grammar.parse import ContentLine
from pytz import UTC, timezone
import logging
def createbirthdaycalendar():
birthdaycalendar = Calendar()
birthdaycalendar.scale = 'GREGORIAN'
birthdaycalendar.method = 'PUBLISH'
birthdaycalendar.extra.append(ContentLine(name='X-WR-TIMEZONE', value='Europe/Berlin'))
for file in os.listdir("/home/pi/MagicMirror/modules/contacts/card"):
if file.endswith(".vcf"):
with open(os.path.join("/home/pi/MagicMirror/modules/contacts/card", file)) as source_file:
for vcard in vobject.readComponents(source_file):
if 'bday' in vcard.contents:
e = Event()
e.name = vcard.fn.value
start_date = datetime.strptime(vcard.bday.value, '%Y-%m-%d')
e.begin = start_date
#e.make_all_day()
e.duration = timedelta(days=1)
e.extra.append(ContentLine(name='RRULE', value='FREQ=YEARLY'))
birthdaycalendar.events.add(e)
#print(vcard.fn.value)
#print(vcard.bday.value)
#print(e)
with open('/home/pi/MagicMirror/modules/calendars/birthdays.ics', 'w+') as f:
f.writelines(birthdaycalendar)
logging.info("createbirthdaycalendar done")