Schnelles Deployen auf Raspberry Pi

Immer wieder dieses lästige Basissetup eines Raspberry Pis

TODO

Vorstellung Ansible

  • Connections / Hosts
  • Playbooks & Roles

Aber was ist mit meinen Passwörtern?

  • Vorstellung ansible_vault
  • Wie kann ansible_vault von mehreren Nutzern genutzt werden?

Lokale Entwicklerumgebung einrichten

  • Hosts
  • Secrets

OpenHAB3 mit Ansible installieren

  • Notwendige Roles

Mein Repository

  • Meinen Code-Tree teilen

Frische Luft ohne Eiszeit

Wer kennt nicht das Problem: man meint es gut und möchte lüften und dann vergisst man das offene Fenster. Kein Problem bei hohen Plus-Graden aber was ist im Winter bei Minus-Graden und laufender Heizung. Dies ist nicht nur eine Umweltsünde sondern macht die Wohnung auch ganz schön ungemütlich.

Aus diesem Grund habe ich mir die Zeit genommen um die bereits vorhandenen Fenstersensoren in ein Programm einzubinden, welches den Nutzer (also mich und alle im Haushalt) nach einer gewissen Zeit und vor allem periodisch über offene Fenster zu benachrichtigen.

Bridge openweathermap:weather-api:weather "OpenWeatherMap Account"
[
  apikey="...",
  refreshInterval=10,
  language="de"
]
{
    Thing weather-and-forecast local "Lokales Wetter"
    [
      location="48.118436,11.618119",
      forecastHours=0,
      forecastDays=0
    ]
    Thing onecall-history local "Lokale Historie"
    [
      location="48.118436,11.618119",
      forecastHours=0,
      forecastDays=0
    ]
    Thing onecall local "Lokales Wetter 2"
    [
      location="48.118436,11.618119",
      forecastHours=0,
      forecastDays=0
    ]
}
Switch contactWatchState "Fensterüberwachung" <window>
Switch contactWatchReset "Zurücksetzen" <button>

Number contactWatchTriggerTimer "Alarmtimer" <time>
Number contactWatchAlertSleep "Sleep between alerts in s" <time>

Number:Temperature contactWatchTriggerTemperature "Schwelle Außentemperatur [%.1f °C]" <temperatur>
Number:Temperature currentLocalTemperature "Aktuelle Temperatur [%.1f %unit%]" <temperature>
{
  channel="openweathermap:weather-and-forecast:weather:local:current#temperature"
}

Group ContactWatch "Gruppe zu überwachender Fenster"

Switch contactSimulation "Fenstersimulation" <window> (ContactWatch)
import java.util.Map
import java.time.Duration

val String loggerName = "ContactWatch"
var Map<String, ZonedDateTime> contactRegistry = newHashMap
var Map<String, ZonedDateTime> lastAlerts = newHashMap

rule "Register contact that got opened"
when
  Member of ContactWatch changed from CLOSED to OPEN or
  Member of ContactWatch changed from OFF to ON
then
  contactRegistry.put(triggeringItemName, new DateTimeType().getZonedDateTime())
  lastAlerts.remove(triggeringItemName)
  logError(loggerName, "Contacts {} now {}. {} contacts registered", triggeringItemName, newState, contactRegistry.size())
end

rule "Unregister contact that got closed"
when
  Member of ContactWatch changed from OPEN to CLOSED or
  Member of ContactWatch changed from ON to OFF
then
  contactRegistry.remove(triggeringItemName)
  lastAlerts.remove(triggeringItemName)
  logError(loggerName, "Contacts {} now {}. {} contacts registered", triggeringItemName, newState, contactRegistry.size())

end

rule "Reset contacts and system"
when
  Item contactWatchReset changed from OFF to ON
then
  logWarn(loggerName, "Reset requested. Will now reboot Max Cube and reset contacts")
  contactRegistry.clear()
  lastAlerts.clear()
  val maxCubeActions = getActions("max-cube", "max:bridge:cube")
  val Boolean success = maxCubeActions.reboot()
  logWarn(loggerName, "Reset requested with result {}", success)
  contactWatchReset.postUpdate(OFF)
end

rule "Check and alert opened contact state"
when
    Time cron "*/5 * * * * ?"
then
  val ZonedDateTime now = new DateTimeType().getZonedDateTime()
  val Integer triggerMinutes = (contactWatchTriggerTimer.state as Number)
  val Integer sleepSeconds = (contactWatchAlertSleep.state as Number)
  val Double triggerTemperature = (contactWatchTriggerTemperature.state as QuantityType<Number>).doubleValue
  val Double currentTemperature = (currentLocalTemperature.state as QuantityType<Number>).doubleValue
  val Boolean active = (contactWatchState.state == ON)
  logDebug(loggerName, "state={}; currentTemp={}; triggerTemp={}", active, currentTemperature, triggerTemperature)

  if (active && currentTemperature <=triggerTemperature)
  {
    for (Map.Entry<String, DateTimeType> openContact : contactRegistry.entrySet())
    {
      val Integer openSinceMinutes = Duration.between(openContact.getValue(), now).getSeconds() / 60
      logDebug(loggerName, "triggerMinutes={}; openSinceMinutes={}", triggerMinutes, openSinceMinutes)
      if (openSinceMinutes >= triggerMinutes)
      {
        var Boolean triggerAlert = true
        val lastAlert = lastAlerts.get(openContact.getKey())
        if (lastAlert !== null)
        {
          val Integer lastAlertBehindSeconds = Duration.between(lastAlert, now).getSeconds()
          if (lastAlertBehindSeconds < sleepSeconds)
            triggerAlert = false
        }
        if (triggerAlert)
        {
          lastAlerts.remove(openContact.getKey())
          lastAlerts.put(openContact.getKey(), now)
          val String msg = "Kontakt " + openContact.getKey() + " offen seit " + openSinceMinutes + " Minuten."
          sendNotification("michael@klemm-dachau.de", msg)
          logWarn(loggerName, msg)
        }
      }
      else
      {
        lastAlerts.remove(openContact.getKey())
      }
    }
   }
end

Raspberry Cluster mit einem Node

Ja ich weiß, ein Cluster besteht aus vielen Nodes. Nichtsdestotrotz ist das Konzept eines Clusters wirklich gut, so dass ich dies auch gerne auf einem einzigen Raspberry (aka Node) aufsetzte. Wer will kann das natürlich auch beliebig erweitern. Zunächst geht es mir dabei aber darum die Funktionsweise und die vorhandenen OpenSource-Werkzeuge vorzustellen.

Motivation

  • Sauberes setup
  • Wechsel zwischen Instanzen

Docker

Docker Swarm

Portainer

Traefik

Praxisteil

Stacks, Applications und Services anlegen und verwalten

MariaDB Image für den Raspberry bauen

Eigenen Cloudspeicher mit dem Raspberry

Anforderungen

Warum keine Fertiglösung?

Lessons learned:

  • Niemals ein Array auf nicht eindeutigen Identifiern anlegen wie z.B. /dev/sda. Bei meinem USB Laufwerken hatte dies fatale folgen.

Raid 5 mit 3 x HDDs + 1 x SSD

CACHE_DISK=/dev/disk/by-id/usb-SanDisk_SSD_PLUS_480GB_152D00539000-0:3

DATA_DISK_1=/dev/disk/by-id/usb-SAMSUNG_HD154UI_152D00539000-0:0
DATA_DISK_2=/dev/disk/by-id/usb-SAMSUNG_HD154UI_152D00539000-0:1
DATA_DISK_3=/dev/disk/by-id/usb-SAMSUNG_HD154UI_152D00539000-0:2

DATA_RAID_LABEL=raid-data
DATA_RAID_NODE=/dev/md/${DATA_RAID_LABEL}

VOLUME_GROUP=vg1
LOGICAL_VOLUME_PREFIX=lv_data1

CACHE_MODE=writeback

hdparm -W1 ${CACHE_DISK}

hdparm -W0 ${DATA_DISK_1}
hdparm -W0 ${DATA_DISK_2}
hdparm -W0 ${DATA_DISK_3}

mdadm --create ${DATA_RAID_LABEL} --level=5 --raid-devices=3 ${DATA_DISK_1} ${DATA_DISK_2} ${DATA_DISK_3}
mdadm --detail --scan ${DATA_RAID_NODE} >> /etc/mdadm.conf

pvcreate ${DATA_RAID_NODE}
pvcreate ${CACHE_DISK} # is this required?

vgcreate ${VOLUME_GROUP} "${DATA_RAID_NODE}" "${CACHE_DISK}"

lvcreate  --size 1500G  --name ${LOGICAL_VOLUME_PREFIX}             ${VOLUME_GROUP}   "${DATA_RAID_NODE}"
lvcreate  -L 450G   --name ${LOGICAL_VOLUME_PREFIX}_cache       ${VOLUME_GROUP}   "${CACHE_DISK}"
lvcreate  -L 1.6G   --name ${LOGICAL_VOLUME_PREFIX}_cache_meta  ${VOLUME_GROUP}   "${CACHE_DISK}"

lvconvert \
  --type cache-pool \
  --cachemode ${CACHE_MODE} \
  --poolmetadata ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache_meta \
  ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache

lvconvert \
  --type cache \
  --cachepool ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache \
  ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}

Testing mit 1 x HDD + 1 x SSD

CACHE_DISK=/dev/sde
DATA_DISK_1=/dev/sdd

VOLUME_GROUP=vg1
LOGICAL_VOLUME_PREFIX=lv_data1

CACHE_MODE=writeback

hdparm -W1 ${CACHE_DISK}
hdparm -W0 ${DATA_DISK_1}

pvcreate ${DATA_DISK_1}
pvcreate ${CACHE_DISK} # is this required?

vgcreate ${VOLUME_GROUP} "${DATA_DISK_1}" "${CACHE_DISK}"

lvcreate  -L 1400G  -n ${LOGICAL_VOLUME_PREFIX}             ${VOLUME_GROUP}   "${DATA_DISK_1}"
lvcreate  -L 450G   -n ${LOGICAL_VOLUME_PREFIX}_cache       ${VOLUME_GROUP}   "${CACHE_DISK}"
lvcreate  -L 1.6G   -n ${LOGICAL_VOLUME_PREFIX}_cache_meta  ${VOLUME_GROUP}   "${CACHE_DISK}"

lvconvert \
  --type cache-pool \
  --cachemode ${CACHE_MODE} \
  --poolmetadata ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache_meta \
  ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache

lvconvert \
  --type cache \
  --cachepool ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache \
  ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}

Partitioning and mount

fdisk /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX} <<EOF
g
n
1

w
EOF
partprobe /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX}
mkfs.ext4 /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX}p1
mount /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX}p1 /mnt/data

Testen von Performance und Caching

dd

Undo steps

fdisk /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX} <<EOF
g
w
EOF
partprobe /dev/mapper/${VOLUME_GROUP}-${LOGICAL_VOLUME_PREFIX}

lvconvert --splitcache ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}
lvremove ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}
lvremove ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache
lvremove ${VOLUME_GROUP}/${LOGICAL_VOLUME_PREFIX}_cache_meta
vgremove ${VOLUME_GROUP}
pvremove ${DATA_RAID_NODE}
pvremove ${CACHE_DISK}

Raspberry

# Create large/slow data volume on mechanical disk
pvcreate /dev/sdb
vgcreate vg-data /dev/sdb
lvcreate --name data -l 100%FREE vg-data /dev/sdb
mkfs.ext4 /dev/vg-data/data
mount /dev/vg-data/data /media/data/

# Create smaller/fast volume to store random access data (e.g. mysql DB and docker volumes)
pvcreate /dev/sda
vgextend vg-data /dev/sda
lvcreate -L 100G -n docker vg-data /dev/sda
mkfs.ext4 /dev/vg-data/docker

# Use the remaining free disk space on fast volume to create disk cache for large volume
lvcreate --name data-cache --type cache-pool -l 100%FREE vg-data /dev/sda
lvconvert --type cache --cache-pool data-cache --cachemode writeback vg-data/data

Aktivieren / Deaktivieren

lvconvert --splitcache vg-data/data
lvconvert --type cache --cachemode writethrough --cachepool cache vg-data/data

Zyklischer Dauerbetrieb vom Raspberry im Wohnwagen

🔧 Ziel

Ein energieeffizientes, zuverlässiges System, das:

  • den Raspberry Pi periodisch oder manuell startet
  • bei Bedarf nach abgeschlossener Arbeit automatisch wieder abschaltet
  • zwischen Dauerbetrieb, Wartungsbetrieb und Alarmbetrieb unterscheidet
  • bei Einbruch oder Störung im Alarmmodus zuverlässig meldet
  • mit dem Raspberry Pi über eine serielle Schnittstelle (UART) kommuniziert
  • nicht einfach durch einen Schalter deaktivierbar ist

⚙️ Systemkomponenten

KomponenteFunktion
ATtiny4313Steuereinheit mit Zustandsspeicher, UART-Kommunikation, Alarmlogik
Raspberry PiHauptrechner für Netzwerk, Monitoring, Logging etc.
Power SwitchMOSFET oder elektronischer Schalter zur Versorgung des Pi
UART (TTL)Kommunikation ATtiny ↔ Raspberry Pi (TX/RX, 5 V TTL)
Taster & SensorenEingaben zur Modusumschaltung & Alarmüberwachung
12 V → 5 V BuckVersorgung von ATtiny & ggf. Pi via Wandler

⚡ Spannungs- & Pegelkompatibilität

VerbindungSpannungPegelkompatibel?
ATtiny4313 Vcc5 V (vom Buck-Regler)✅ Ja
UART zu Raspberry Pi5 V TTL → 3.3 V Pi RX⚠️ Pegelwandler empfohlen
UART vom Pi TX → Tiny RX3.3 V → 5 V AVR RX✅ meistens stabil

Empfehlung: Pegelwandler oder Spannungsteiler für ATtiny TX → Pi RX.


🔌 GPIO-Zuordnung am ATtiny4313

ATtiny PinFunktionRichtungKommentar
PD0UART RXINVom Pi
PD1UART TXOUTZum Pi (5 V → 3.3 V Wandler nötig)
PD2TasterINZur Modusumschaltung
PD3Alarm Input (Schleife)INÜberwacht Fenster/Türen
PD4Pi Power SwitchOUTSteuert MOSFET
PD5Status LED (optional)OUTOptional für Feedback
RESETProgrammierbarISP nötig
Vcc/GNDStromversorgung5 V vom Buck

🔄 Zustände & Übergänge

stateDiagram-v2
    [*] --> Init
    Init --> Periodic : Timerstart
    Init --> Permanent : Taster (lang)
    Init --> Alarm : Taster (doppelklick) oder EEPROM
    Periodic --> Shutdown : Timer abgelaufen
    Permanent --> Shutdown : Kommando vom Pi
    Alarm --> Shutdown : Alarm quittiert & Pi gibt frei
    Alarm --> AlarmTrigger : Alarmkontakt geöffnet
    AlarmTrigger --> Shutdown : Meldung an Pi gesendet
    Shutdown --> [*] : Power-Off Pi

 


📋 Zustandstabelle

ZustandBeschreibungÜbergänge
InitWird beim Start des ATtiny gesetztÜbergang über Taster oder gespeicherte Info
PeriodicStartet Pi nach Timer-Intervall, fährt nach getaner Arbeit wieder runterShutdown nach Timer oder Pi-Freigabe
PermanentPi bleibt dauerhaft an, manuelle Steuerung möglichShutdown durch expliziten Befehl vom Pi
AlarmAlarmaktiv: ATtiny überwacht SensorenAlarmTrigger bei Öffnung
AlarmTriggerAlarm ausgelöst, Pi wird hochgefahrenShutdown durch Pi nach Quittierung
ShutdownPi wird heruntergefahren, Strom wird getrennt→ [*]

🔐 Sicherheit & Sabotageschutz

  • Kein Kippschalter deaktiviert das System – nur Taster mit Logik
  • Alarm wird nicht vom Pi allein steuerbar deaktiviert
  • ATtiny „besitzt“ die Kontrolle über Power & Modus
  • Zustand kann im EEPROM gespeichert werden, um Neustarts zu überleben

🔧 ISP-Anschlussbelegung (für Programmierung des ATtiny4313)

PinFunktion
RESETReset
MOSIDaten in
MISODaten out
SCKTakt
VCC5 V
GNDMasse

Programmierbar z. B. über USBasp oder Arduino as ISP.


📶 UART-Protokoll (minimal)

Pi → ATtiny:

  • STATUS=OK → Lebenszeichen
  • SHUTDOWN → Fahr herunter
  • MODE? → Fragt den aktuellen Modus ab

ATtiny → Pi:

  • MODE=PERIODIC|PERMANENT|ALARM
  • ALARM=TRIGGERED
  • SHUTDOWN → Power-Off in x Sekunden

📦 Optionen für die Umsetzung

  • Serielle Kommunikation über /dev/serial0
  • ATtiny Code via Arduino IDE oder avr-gcc
  • Raspberry Pi Service überwacht UART / sendet STATUS regelmäßig
  • Fehlermeldung & Shutdown bei Inaktivität oder Alarmereignis

Wenn du möchtest, kann ich dir im nächsten Schritt liefern:

  • Einen vollständigen KiCad-Schaltplan
  • Den Arduino-Code für den ATtiny4313
  • Ein Beispiel für das Python-Monitoring am Pi
  • EEPROM-gestützte Modusverwaltung

Sag mir einfach, womit du weitermachen willst.

Arduino als Energiewächter und Alarmzentrale im Wohnwagen

Ziel: Extrem Energieeffizienter Betrieb eines Raspberry Pi im Wohnwagen inkl. zyklischem Start, Alarmüberwachung. Übertragung Systemstatus und Alarme an Backend/Smartphone über Wi-Fi oder LTE.

🔋 Stromversorgung & Energieoptimierung

  • Versorgung: 12 V LiFePO₄-Akku mit 200Ah
  • Regler: 3.3 V MP1584 für den ATmega328P
  • Raspberry Pi 4: Strom wird über MOSFET Treiber nur bei Bedarf aktiviert. Buck converter 5V 10A
  • RTC-Modul (DS3231): Weckt ATtiny4313 periodisch via Interrupt (ca. 3 µA Standby)
  • MPU-Modul
  • Keypad zur Pin-Eingabe
  • RFID Karte zum Aktivieren/deaktivieren
  • Alarmsirene und kleiner Piezo zum quittieren
  • Verteiltes System von ATmega328P die über 4-Draht verbunden sind (Wakeup-Line von main and die nodes, Alarm-Line von Nodes an Main, 2 x A/B für RS485 Kommunikation)
  • Gesamtsystem im Sleep: <30 µA Stromverbrauch mit aktivem Watchdog

KomponenteModell/TypFunktion
MikrocontrollerATtiny4313 (DIL)Zentrale Steuerung
RTC-ModulDS3231Zeitgeber für Zyklusstart
TasterDigital, mit Pull-UpStart-/Shutdown-Steuerung
2 x AlarmkontaktReed-Schalter o.Ä.Fenster-/Türüberwachung
LED3.3 VStatusblinker alle 10 s wenn Armed. Wenn Alarm flackern. Ansonsten aus.
UARTTX/RX auf 3.3 VKommunikation mit Raspberry Pi
AlarmausgangGPIO an Treiber mit 12v schaltausgang Anschluss von Verbraucher wie zum Beispiel Sirene
SchaltausgangGPIO an Treiber der 12 V auf einen 5 V 5A DC-DC Converter bringt.Stromzufuhr für Raspberry und weitere 5 V Verbraucher
Screenshot
Arduino PINPhysischer
Pin
PinFunktionRichtungBeschreibung
1PA2ResetEingangFür ISP
02PD0UART RXEingangKommunikation RPi → ATtiny
13PD1UART TXAusgangKommunikation ATtiny → RPi
4PA1
5PA0
26PD2INT0 (RTC Alarm)EingangWeckt ATtiny
37PD3TasterEingangKurz-/Lang-Druck
48PD4I²C SDAI²CRTC-Kommunikation
59PD5I²C SCLI²CRTC-Kommunikation
10GNDGround
611PD6Status-LEDAusgangBlinkt alle 30s
712PB0RPi Power EnableAusgangSteuert MOSFET mit 12V der über Buck-Converter RPi versorgt
813PB1Alarmkontakt 1EingangFenster-/Türkontakt (Hüllschutz)
914PB2Alarmkontakt 2Eingangzusatzkontakte wie fliegengitter falls Fenster offen.
1015PB3AlarmausgangAusgang Ansteuerung Verbraucher über 12 V Treiber (z.B. Sirene)
1116PB4
1217PB5MOSIEingangISP
1318PB6MISOAusgangISP
1419PB7SCLEingangISP
20VCCVersorgungEingangam 3,3 V Buck-Converter

Arduino Main

ATmega328P hat 3 PCINT Gruppen jeweils für die Bänke B, D und D:

VektorPin-GruppeRegisterPins
PCINT0_vectPORTBPCMSK0PB0–PB7 (D8–D13 + SPI)
PCINT1_vectPORTCPCMSK1PC0–PC5 (A0–A5)
PCINT2_vectPORTDPCMSK2PD0–PD7 (D0–D7)

Wichtige Kriterien für die Wahl der Pin:

  • Möglichst viele Interrupteingänge in meiner Hand
  • Hardware-Interrupts sollen für Taster genutzt werden um Debouncing zu ermöglichen
  • Interrupt von der Peripherie (RTC, MPU, RFID, PIR) können auf einen gemeinsamen PCINT Vector wenn notwendig.
  • Die Software Serial benötigt einen Interrupt auf dem RX pin. Damit ist die Gruppe nicht mehr für andere Interrupts in Code außerhalb von Software Serial (oder auch andere Instanzen von Software Serial) nutzbar.
  • Da zwei Software Serial eingesetzt werden, sind das schon zwei PCINT vectoren die nicht genutzt werden können. Daher wählte ich die PINs so dass auf den Gruppen eh bereits viele Standardfunktionen wie HW-Serial, SPI, I2C, Xtal genutzt werden.
  • Daher Port / PCINT2_vect für Eingänge, wodurch ich auf zusätzliche 4 zusätzliche IRQ Eingänge (PD4-PD7) komme. Zum Vergleich: Da SPI genutzt wird hätte PCINT0_vect / PortB nur 2 IRQ Pins.
Use-CaseStatus ✅ / ⚠️ / ❌Kommentar
2× TasterD3 (INT1) & D5 (PCINT2)
1× Status-LEDA0 / PC0
RTC via I²CPC4/PC5 (A4/A5)
RTC Wakeup (Interrupt)PD2 (INT0)
MPU via I²CPC4/PC5 (A4/A5)
MPU InterruptPD4 (PCINT2)
Raspberry Power SwitchPB6 (XTAL1)
Alarmgeber SchaltausgangPB7 (XTAL2)
RFID via SPIPB2–PB5
RFID IRQPD7
RFID StromschaltungPD6 über ULN
RS-485 KommunikationSW Serial auf PB0 (RX) / PB1 (TX)
Victron VE.Direct UARTSW Serial auf PC2/PC3
I²C Geräte (RTC, MPU, MCP)PC4/PC5
Piezo SignalgeberPC1 (A1)
HWPinArduinoFunktionVerwendungInterruptInterrupt-Vektor
1RESETRESETISP RST (evtl. auch an Raspberry zum Flashen über Bootloader HW-Serial)
2PD0D0HW UART RXKommunikation mit Raspberry Pi über TTL 3,3VNein
3PD1D1HW UART TXKommunikation mit Raspberry Pi über TTL 3,3VNein
4PD2D2INT0 / TasterSystem-Taster für Start und ShutdownJaINT0
5PD3D3INT1 / TasterExterner Taster um System kurzzeitig in Bereitschaft für Interaktionen (z.B. Alarm deaktivieren) zu versetzenJaINT1
6PD4D4Digital In / Peripherie InterruptRTC Interrupt für tägliches Aufwach-EventJaPCINT2
7VCC
8GND
9PB6XTAL1Digital Out / Power SwitchRaspberry Power über MOSFET Treiber um den High-Power 5V Buckconverter zu schaltenNein
10PB7XTAL2Digital Out / Alarm SwitchAlarmgeber (z.B. Sirene) mit Strom versorgen. Kann entweder über ULN2003 (<500ms) oder über MOSFET betrieben werdenNein
11PD5D5Digital In / Peripherie InterruptMPU IRQ / Alarm bei Erschütterung/Neigung/Bewegung des WohnwagensJaPCINT2
12PD6D6Digital Out / Power SwitchRFID Power (über ULN2003 für GND)NeinPCINT2
13PD7D7Digital In / Peripherie InterruptRFID IRQJaPCINT2
14PB0D8RS485 RX (SW Serial)Software Serial an RS485 TreiberJaPCINT0
15PB1D9RS485 TX (SW Serial)Software Serial an RS485 TreiberNeinPCINT0
16PB2D10SPI SSRFID SSNeinPCINT0
17PB3D11SPI MOSIRFID MOSI / RFID MOSINeinPCINT0
18PB4D12SPI MISORFID MISO / ISP MISONeinPCINT0
19PB5D13SPI SCKRFID SCK / ISP SCKNeinPCINT0
20AVCC
21AREF
22GND
23PC0A0Digital OutStatus LED (An/Watchdog/Alarm-Status/Fehler/Shutdown)NeinPCINT1
24PC1A1PiezoQuittierungston für Alarm (de-)aktivierung (evtl. über Treiber ULN2003)NeinPCINT1
25PC2A2SW UART RXVerbindung zu Victron MPPT Solar ChargerJa (in SoftwareSerial Lib)PCINT1
26PC3A3SW UART TXVerbindung zu Victron MPPT Solar ChargerNeinPCINT1
27PC4A4I2C SDAVerbindung zu RTC, MPU und Port ExtenderNeinPCINT1
28PC5A5I2C SCLVerbindung zu RTC, MPU und Port ExtenderNeinPCINT1

🧠 Betriebs- und Alarmzustände

🔁 operatingMode

  • Off: Kein Betrieb
  • Periodic: Pi startet durch RTC-Alarme (zyklisch)
  • On: Pi bleibt dauerhaft an
  • Alert: Alarm wurde ausgelöst
stateDiagram-v2
    [*] --> off
    off --> alert: Alarm-In
    off --> periodic: RTC event
    off --> on: Button
    periodic --> on: Button
    periodic --> on: (PI) set_state
    periodic --> alert: Alarm-In
    periodic --> off: (PI) ack_shutdown
    on --> alert: Alarm-In
    on --> off: (PI) ack_shutdown
    alert --> on: (PI) quit_alert
State inState outTriggerComment
offperiodicRTC eventOnce a day the RTC will start the PI
offonButtonStart PI permanent due to user button press
periodiconButtonStart PI permanent due to user button press
onoffSerial msg: “ack_shutdown”Pi confirms that an ordinary shutdown took place. Power can be turned off
periodicoffSerial msg: “ack_shutdown”Pi confirms that an ordinary shutdown took place. Power can be turned off
offalertAlert-Contactalarm loop has been triggered
periodicalertAlert-Contactalarm loop has been triggered
onalertAlert-Contactalarm loop has been triggered
alertonSerial msg: “quit_alert”Alarm stopped. Pi keeps on.

Ausgangslogik:

AusgangOffPeriodicOnAlert
Pi-Power0111
Signal0001
Status LEDArmed: Slow / Disarmed: OffFast

Event- / Status-Matrix

Event / InputOffPeriodicOnAlert
ButtonOnOnnoopx
RTCPeriodicnoopxx
Serial set_statenoopOKOKx
Alert-ContactAlertAlertAlertx
Serial ack_shutdownnoopOffOffx
Serial quit_alertnoopnoopnoopOn

🚨 alarmMode

  • Disarmed: Alarmüberwachung aus
  • Armed: Überwachung aktiv

🔄 Zustandsübergänge

AuslöserAktion
Taster kurz (<1 s)Pi einschalten
Taster lang (>3 s)Pi herunterfahren (außer im Alert-Modus)
RTC-Alarm (DS3231)Pi einschalten im Periodic-Modus außer wenn Alter oder Permanent 
Alarmkontakt öffnetalarmMode → Alert
Seriell vom RPi: 
set_mode permanent→ operatingMode permanent
set_alarm armed→ alarmMode armed
shutdown→ Poweroff 10s nach Bestätigung
set_next_cycle 04:00:00→ RTC-Alarm neu setzen

💬 Serielles Protokoll (UART)

Baudrate: 9600 bps
Pegel: 3.3 V TTL (direkt kompatibel mit Raspberry Pi)

RPi → ATtiny:


set_mode periodic
set_alarm armed
set_next_cycle 06:00:00
shutdown

ATtiny → RPi:


status armed
shutdown

⏱️ LED mit Watchdog-Timer

  • LED blinkt alle 30 Sekunden für ca. 100 ms
  • Getriggert durch Watchdog Interrupt + Softwarezähler
  • Kein Sleep-Ausfall, Stromverbrauch bleibt gering

🔧 ISP-Anschluss für Programmierung

ISP PinFunktion
PA2RESET
PB7SCK
PB6MISO
PB5MOSI

Bewegende Momente

Bewegungsmelder an einen Wi-Fi RGB(WW/CW) LED-Controller anbinden

Für mein Badezimmer finde ich doch mein normales Licht etwas hell, weshalb eine indirekte LED Beleuchtung her musste die auch in der Farbe variabel einstellbar ist. Alles kein Hexenwerk aber es sollte auch mit meiner bestehenden OpenHAB 3 über alle Inputs steuerbar sein und ein schönes Nachlicht hergeben damit man auch Nachts sicher unterwegs ist ohne zu stark aufzuwachen.

Somit waren die Anforderungen recht schnell klar:

  • Gute Anbindung an OpenHAB (3)
  • Viel Power um auch lange LED Strips verwenden zu können
  • Möglichst gutes Privacy Verhalten (schließt das China-Zeugs aus)
  • Es sollte nur bei Bedarf leuchten (z.B. mit Bewegungsmelder)

Nun gibt es bei Amazon einige Wi-Fi RGB LED Controller die alle vielversprechend aussahen. All diese Controller basieren auf mehr oder weniger dem gleichen Chipsatz ESP8266 auf welchem es eine breite Masse an alternativer Firmware gibt. Für mich ist der Tasmota die Wahl der Dinge, aber es gibt bestimmt auch andere gute Derivate. Mit ESPurna habe ich jedoch keine gute Erfahrung gemacht.

Mit dieser alternativen Firmware ist bereits der erste große Schritt gemeistert und die No-Privacy China-Firmware kann entfernt werden. Zudem bietet diese Firmware die Möglichkeit für eigene Erweiterungen (“Rules” / Scripting) und kann über MQTT angesteuert werden.

Für meine Verwendung habe ich einen größeren Controller ausgewählt, da dieser viel Power bietet, Schraubterminals besitzt und im Gehäuse genügend Platz zum basteln und. tüfteln ist.

Nun kommen wir aber zum interessanten Teil der Arbeit: Und zwar soll das Licht ja auch nicht die ganze Nacht leuchten und nur bei Bedarf angehen. Und zwar das nur für die Nacht.

Dazu kommen ein paar zusätzliche Anpassungen dazu:

  • Ein Bewegungsmelder muss an den LED Controller angebunden werden um Bewegungen in der Umgebung wahrzunehmen
  • Die Controllersoftware muss erweitert werden um die LEDs bei Bewegung einzuschalten und für eine bestimmte vorkonfigurierte Zeitspanne eingeschaltet. Dieser Teil sollte auch deaktivierbar (Tag/Nacht) sein.
  • In OpenHAB gibt es über das Astro Binding für den aktuelle Standort minutengenaue Angaben zum Sonnenunter- und -aufgang. OpenHAB soll so erweitert werden, so dass dieses die Steuerung im Controller zum richtigen Zeitpunkt aktiviert (Sonnenuntergang) und deaktiviert (Sonnenaufgang) wird.

PIR Sensor an den Wi-Fi RGB LED-Controller anbinden

TODO

Ich habe mich für die PIR-Sensoren AM312 (z.B. als 5er-Pack bei Amazon) entschieden das diese mit 3,3 V ausreichend versorgt werden und einfach einen Schließerkontakt bei Bewegung haben. Nachteil gegenüber einigen anderen ist, dass diese nicht einstellbar sind. Für diesen Use-Case sollte dies aber nicht von belang sein.

Belegung AM312

Auf der Unterseite des LED-Controllers befinden sich einige Pins die zum flashen aber auch zum herausführen der Stromversorgung genutzt werden können

Abzwacken der Stromversorgung für den PIR-Sensor + temporäre Stiftleiste zum Flashen

Die Stiftleiste die auf IO, RX und TX geht ist ausschließlich temporär zum erstmaligen Flashen der Tasmota Firmware gedacht und wird anschließend wieder entfernt.

Nun muss noch ein Plätzchen für den Sense-PIN des PIR Moduls gefunden werden. Hierfür habe ich den GPIO 4 ausgewählt da dieser anscheinend noch nicht belegt ist (siehe Grafik)

image
Belegung ESP Wi-Fi Chipsatz auf dem LED Controller

Dieser wird auf der Vorderseite direkt an das ESP Breakout-Board angelötet

Anlöten an den GPIO 4 des ESP Breakout-Boards auf der Vorderseite

GPIO Konfiguration in Tasmota mit Mapping von GPIO4 (PIR) auf Switch_1

Falls nicht nur RGB sondern auch WW (Warm White) bzw. CW (Cold White) angebunden werden soll, müssen diese GPIOs auch noch konfiguriert werden. Es empfiehlt sich aber nur die benötigten Ausgänge zu konfigurieren, da Tasmota anhand der Konfiguration die Farben mischt.

Anpassung Software auf dem Controller

Die Anpassung auf dem LED Controller habe ich in zwei Rules aufgeteilt. Dies kommt daher dass sich Rules einfach ein- und ausschalten lassen und ich damit die ganze Funktion (“schalte Licht bei Bewegung an”) ausschalten kann während noch notwendige Funktionalitäten in der anderen Rule aktiv sind. Eine Rule setze ich also zur Beobachtung des Status, wie z.B. Events/Veränderungen die über MQTT oder des ablaufenden Timers ein, also die Funktionen die immer aktiv sein sollen.

RULE2
ON event#PIR1-STATE==?
DO
 VAR1 %value%
ENDON
ON event#PIR1-TIMEOUT>0
DO
 VAR2 %value%
ENDON
ON VAR1#State==?
DO BACKLOG
 RULE3 %value%;
 Publish stat/%topic%/PIR1-STATE %value%
ENDON
ON VAR2#State>0
DO
 Publish stat/%topic%/PIR1-TIMEOUT %value%
ENDON
ON Rules#Timer==3
DO
 Power1 OFF
ENDON

Man sieht hier gut dass die Rule3 je nach Zustand von Var1 gesetzt wird. Var1 nutze ich somit zum (de)aktivieren der Bewegungsmelderfunktion. Diese Variable wird wiederum von dem MQTT Event “PIR1-STATE” verändert. Zudem wird das Licht ausgeschalten wenn der Timer 3 abläuft.

Eine weitere Rule soll rein auf die Bewegungen lauschen und somit auf Veränderungen an dem GPIO 4, welcher durch die vorige Konfiguration auf “Switch1” gemapped wurde. Die Veränderungen werden auch über MQTT announced und wenn der Switch1 auf ON geht (== Bewegung erkannt) wird zusätzlich Timer 3 auf die in Var2 festgelegte Zeit gesetzt. Dadurch kann man einen Nachlauf nach der zuletzt erkannten Bewegung sicherstellen.

RULE3
ON Switch1#State==ON
DO BACKLOG
 Power1 %value%;
 RuleTimer3 %VAR2%;
 Publish stat/%topic%/PIR1-MOTION %value%
ENDON
ON Switch1#State==OFF
DO
 Publish stat/%topic%/PIR1-MOTION %value%
ENDON

Im folgenden noch einige Standardwerte die einmalig definiert werden sollten. Der Rest kann dann über MQTT gesteuert werden.

BACKLOG
 RULE2 ON;
 RULE3 OFF;
 VAR1 OFF;
 VAR2 300;

Um zu verhindern dass Switch_1 den zugeordneten Output (Power) ansteuert und dass der Switch nur unserer Rule zur Verfügung steht muss die folgenden Option gesetzt werden:

SetOption114 1

Da standardmäßig die Konfiguration für einen Switch auf “Toggle” gestellt ist, wir aber in unserer Rule ein ON/OFF erwarten muss das Switch-Verhalten umgestellt werden:

SwitchMode 1

(De-)Aktivierung des Bewegungsmelders bei Sonnenuntergang / Sonnenaufgang

TODO

val String loggerName = "SunsetSunriseRules"

rule "Sunrise handler"
when
  Channel "astro:sun:local:rise#event" triggered START
then
    //bad_led_motion_detection.postUpdate(OFF)
    bad_led_motion_detection.sendCommand(OFF)
    logError(loggerName, "Run sunrise handler")
end

rule "Sunset handler"
when
    Channel "astro:sun:local:set#event" triggered START
then
    //bad_led_motion_detection.postUpdate(ON)
    bad_led_motion_detection.sendCommand(ON)
    logError(loggerName, "Run sunset handler")
end

Damit die zuvor definiert Rule funktioniert, müssen natürlich die Things und Items entsprechend in OpenHAB definiert sein.

Passbilder am Drogerie/Discounter Fotodrucker

Immer wieder ärgere ich mich wenn ich neue Passbilder brauche. Zwar ist eine Foto selbst schnell gemacht, aber die Vorbereitung für das Drucken ist immer wieder eine nervige Angelegenheit. Wie groß muss das Foto gleich wieder sein? Wie bekomme ich es hin das am Fotodrucker genau diese Größe am Ende rauskommt? Macht der Fotodrucker mein Foto etwas größer und in welchem Maße?

Bislang bemühte ich dazu Gimp und einen Taschenrechner. Nachdem ich nun das Tool montage kenne, habe ich ein kleines wrapper script geschrieben um diese Schritte automatisiert zu machen.

Manuelle Schritte:

  1. Foto machen und auswählen
  2. Foto zuschneiden nach der Maske für Passfotos (kinder) und dem Verhältnis 35:45
  3. Evtl. Lichtverhältnisse (Helligkeit/Kontrast/…) optimieren
  4. Bestimmen der Druckgröße. Bei dm ist z.B. das Standardformat am Fotodrucker 10x15cm

Schritte die das Programm übernimmt:

  1. Auslesen der Auflösung und Maße aus dem Foto / der Quelldatei
  2. Bestimmen der Auflösung der Zieldatei so dass das Foto mit der optimalen Qualität eingefügt wird.
  3. Berechnung der horizontalen und vertikalen Rahmen so genau 2 auf 2 Passfotos auf das Zielfoto passen.
  4. Einfügen der Fotos mittels dem Tool montage

Hier das Script für MacOS / ZSH:

#!/usr/bin/env zsh

# Groesse in cm des Zielformats
PHOTO_W=10
PHOTO_H=15

WORK_DIR=$(pwd)
SCRIPT_DIR=$(cd $(dirname $0) && pwd)

if [[ -z "$1" ]]; then
  echo "No input file given. Usage: $0 <IMG>"
  exit 1
fi

IMAGE_IN_PATH=$(cd $SCRIPT_DIR && cd $(dirname $1) && pwd)/$(basename $1)
IMAGE_OUT_PATH=$(cd $SCRIPT_DIR && cd $(dirname $1) && pwd)/out-${PHOTO_W}x${PHOTO_H}.jpg

if [[ ! -f "${IMAGE_IN_PATH}" ]]; then
  echo "File not found: ${IMAGE_IN_PATH}"
  exit 1
fi

# Auslesen des Formats der Quelldatei
eval $(identify -format 'IMG_WIDTH=%w\nIMG_HIGHT=%h\n' ${IMAGE_IN_PATH})
echo "Using file ${IMAGE_IN_PATH} with size ${IMG_WIDTH}x${IMG_HIGHT}"

# Berechnung der horizontalen und vertikalen Rahmen
BORDER_VERT="$(echo $(( (( ${IMG_WIDTH} / 3.5 * ${PHOTO_W} ) - ( 2 * ${IMG_WIDTH} )) / 4 )) | awk '{ print int($1) }')"
BORDER_HORT="$(echo $(( (( ${IMG_HIGHT} / 4.5 * ${PHOTO_H} ) - ( 2 * ${IMG_HIGHT} )) / 4 )) | awk '{ print int($1) }')"

# Anwendung des Tools montage
montage \
  ${IMAGE_IN_PATH} ${IMAGE_IN_PATH} ${IMAGE_IN_PATH} ${IMAGE_IN_PATH} \
  -geometry ${IMG_WIDTH}x${IMG_HIGHT}+${BORDER_VERT}+${BORDER_HORT} \
  -quality 100 \
  ${IMAGE_OUT_PATH}

Das Script kann in diesem Repo gefunden werden: https://github.com/MichaelKlemm/passphoto-tools