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.