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.

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

Die Hardware und Ihre Verkabelung; RPi 4 2GB, WM8960 Audio HAT, DS3231 I2C RTC, BH1750 Helligkeitssensor, BME680 – aktuell nicht in Verwendung)

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&notification=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")

Leave A Comment

Deine E-Mail-Adresse wird nicht veröffentlicht.