Blog

  • Scanner-Server mit USB-Scanner über Netzwerk

    Problem

    Viele haben einen Scanner, der nur über USB direkt an einem PC betrieben werden kann. Wenn dieser Scanner aber z.B. ein spezieller Dokumentenscanner ist und/oder von mehreren Personen benutzt werden soll (z.B. ein „Abteilungsscanner“ auf dem Flur), dann ist das entweder unmöglich, oder ein extra „Scan-PC“ muss neben dem Scanner stehen, der auch immer laufen muss — oder für jeden Scan extra eingeschaltet werden muss. Diesen „Scan-PC“ kann man aber sehr kostengünstig und so geschickt aufsetzen, dass daraus eine wahrlich geniale Lösung wird:

    Lösungskonzept

       

        • kleine Linux-Kiste (alter Raspberry Pi) neben dem Scanner, an der der Scanner direkt per USB hängt. Natürlich muss die Box im Netz hängen.

        • per udev-Regel wird das Einschalten des Scanners erkannt und ein Dienst gestartet.

             

              • dieser Dienst startet den USB-IP-Hostdienst und stellt den Scanner somit als USB-Gerät im Netz zur Verfügung

              • ausserdem logt sich der Dienst per SSH im Verarbeitungsserver ein und startet dort ebenfalls einen Dienst

          • Dieser Dienst hängt mit USB-IP den remote Scanner als lokales USB-Gerät

               

                • Auf diesem Verarbeitungsserver kann dann die kompliziertere Image-Verarbeitung (OCR, etc.) stattfinden.

          Somit steht der Scanner automatisch wenige Sekunden nach dem Einschalten auf der remote Box als „pseudo“ lokales Gerät zur Verfügung.

          Vorteile dieses Ansatzes

          Der PC, der die Scans verarbeitet muss durchaus einige Ressourcen in Bezug auf CPU und Memory haben, weil die graphische Aufbereitung der Scans (entrauschen), sowie OCR doch relativ Ressourcen-hungrig sind. Dazu kommt, dass im Fall eines Dokumentenscanners eigentlich alle Scan-Parameter vorher festgelegt werden können. Der Scanner sollte eigentlich auf Knopfdruck starten und die Scans idealerweise irgendwo im Netzwerk ablegen. Mit diesem Ansatz kann die eigentliche Verarbeitung des Scans irgendwo im Netz (z.B. auf einer ausreichend dimensionierten VM) stattfinden. Vor Ort muss der Linux-Rechner eigentlich nur das USB-Gerät über das Netz weiterreichen. Dafür reicht auch ein uralt-Raspi.

          Natürlich gibt es Dokumentenscanner, die das alles out-of-the-box können, aber die sind im Vergleich sehr teuer — sehr viel teurer, als ein einfacher, aber zuverlässiger Dokumentenscanner plus ein alter Raspberry Pi. Daher entstand die Idee.

          Raspi konfigurieren

             

              • usbip installieren + kernel-module ‚usbip-host‘ laden

              • ssh-connection zu Client-Computer vorbereiten (ssh-copy-id)

            Jetzt müssen mehrere Dienste konfiguriert werden. Zunächst die udev-Regel, die den USB-IP Dienst zur Weiterleitung des Geräts startet, sobald der Scanner „sichtbar“ wird. Das passiert z.B. beim Einschalten des Scanners (oder Aufwachen aus dem Standby).

            /etc/udev/rules.d/90-usbip.rules

            SUBSYSTEM=="usb", 
            ATTR{idVendor}=="04c5", 
            ATTR{idProduct}=="1626", 
            ENV{DEVTYPE}=="usb_device", 
            TAG+="systemd", 
            ENV{SYSTEMD_WANTS}+="usbip@%k.service"

            Die Vendor- und ProductID müssen natürlich angepasst werden. Einfach mit lsusb nachschauen, wenn der Scanner per USB verbunden ist.

            Jetzt kommt der Dienst, der von der udev-Regel gestartet werden soll. Der Trick hier ist, dass per SSH der nötige Dienst zum remote einbinden des USB-Geräts auf dem Verarbeitungsserver automatisch gestartet wird, sobald der Scanner eingeschaltet wird.

            WICHTIG: In der zweiten ExecStart-Anweisung und der ersten ExecStop-Anweisung muss natürlich „root@verarbeitungsserver“ und „usbip@raspi“ geändert werden auf die tatsächlichen ssh-Benutzer und Hostnamen.

            /lib/systemd/system/usbip@.service

            [Unit]
            Description=USB-IP Binding %i
            After=network-online.target usbipd.service
            Wants=network-online.target usbipd.service
            PartOf=usbipd.service
            StopWhenUnneeded=yes
            [Service]
            Type=simple
            RemainAfterExit=yes
            # binding a device itself
            ExecStart=/bin/sh -c "/usr/sbin/usbip bind -b %i"
            # and attching the device on remote computer
            ExecStartPost=ssh root@verarbeitungsserver "/bin/systemctl start usbip@raspi:%i"
            # expecting errors of unbinding actually "already disconnected device"
            ExecStop=ssh root@verarbeitungsserver "/bin/systemctl stop usbip@raspi:%i"
            ExecStop=-/bin/sh -c "/usr/sbin/usbip unbind -b %i"
            ExecStop=/bin/systemctl reset-failed
            [Install]
            WantedBy=multi-user.target

            Dieser Dienst stellt sicher, dass der USB-IP Service immer läuft:

            /lib/systemd/system/usbipd.service

            [Unit]
            Description=USB-IP Host Daemon
            After=network-online.target
            Wants=network-online.target
            [Service]
            Type=simple
            Restart=always
            ExecStart=/bin/sh -c "/usr/sbin/usbipd"
            [Install]
            WantedBy=multi-user.target

            Und aktivieren und starten:

            udevadm control --reload-rules
            systemctl daemon-reload
            systemctl restart systemd-udevd
            systemctl enable usbipd && sudo systemctl start usbipd

            Verarbeitungsserver konfigurieren

               

                • usbip installieren und Kernel-module laden

                • ssh-connection zu Server konfigurieren

              Als erstes brauchen wir das Gegenstück für den Dienst, der auf dem Raspi aktiviert wird, wenn der Scanner eingeschaltet wird. Der Raspi startet dann diesen Dienst auf dem Verarbeitungsserver remote per SSH. Er dient dazu, den im Netz angebotenen USB-IP-Scanner lokal wieder als USB-Gerät zur Verfügung zu stellen.

              /etc/systemd/system/usbip@.service

              [Unit]
              Description=USB-IP [At/De]taching %i
              After=network-online.target
              Wants=network-online.target
              [Service]
              Type=simple
              Restart=on-failure
              RestartSec=1
              RemainAfterExit=yes
              ExecStart=/bin/sh -c "/usr/sbin/usbip attach -r $(echo %i | sed 's/:/ -b /1')"
              #Expected error of unbinding actually "already disconnected device"
              ExecStop=-/bin/sh -c "/usr/sbin/usbip detach --port `/usr/sbin/usbip port | grep -B2 $(echo %i | awk -F':' '{print $2}') | grep 'Port' | cut -b 6-7`"
              [Install]
              WantedBy=multi-user.target

              Dazu muss natürlich der USB-IP Dienst immer laufen.

              /etc/systemd/system/usbipd.service

              [Unit]
              Description=USB-IP Client Daemon
              After=network-online.target
              Wants=network-online.target
              [Service]
              Type=simple
              Restart=always
              RemainAfterExit=yes
              # creating a tmp-file if any USB port is available
              ExecStart=/bin/sh -c "/usr/sbin/usbip list -r raspi | grep '/' > /tmp/usbip-exportable-list"
              # cleaning garbage after yourself
              ExecStop=-rm /tmp/usbip-exportable-list
              # sometime had happened too
              ExecStop=systemctl reset-failed
              [Install]
              WantedBy=multi-user.target

              Und noch ein kleiner Dienst für den Fehlerfall (siehe Kommentare im Code):

              /etc/systemd/system/usbipd-restart.service

              [Unit]
              Description=USB-IP Deamon Remote Restarting (runs by client)
              After=network-online.target usbipd.service
              Wants=network-online.target usbipd.service
              [Service]
              Type=simple
              EnvironmentFile=/etc/usbip.conf
              # Is any USB port already available? - This is the case if we bekome online after the server.
              # If so - restart usbipd.service remotely to be attached.
              # If not (file is empty) - nothing to do, USB will be attached
              # later on while being inserted into the host.
              ConditionFileNotEmpty=/tmp/usbip-exportable-list
              ExecStart=-ssh root@raspi "systemctl restart usbipd"
              # Clean up usbipd's garbage
              ExecStartPost=-rm /tmp/usbip-exportable-list
              ExecStartPost=systemctl reset-failed
              [Install]
              WantedBy=multi-user.target

              systemctl daemon-reload
              systemctl enable usbipd && sudo systemctl start usbipd

              Damit steht der Scanner dynamisch über ein Ethernet Netzwerk als USB-Gerät auf dem Verarbeitungsserver zur Verfügung. 

              Jetzt kann auf dem Verarbeitungsserver die Scannersoftware eingerichtet werden, so als ob der Scanner am lokalen USB-Port hängen würde, also z.B. ein Treiber/Software/Dienst, der den Scan-Button auslesen kann (falls es das für den eingesetzten Scanner gibt). Auch das Anstossen des eigentlichen Scans, sowie die weitere Verarbeitung müssen über eine zum Scanner passende Scansoftware konfiguriert werden. Tut man das auf einem Server (z.B. eine VM) im Netz und richtet den Prozess mitsamt fester Scanparameter so ein, dass die fertigen Dokumente auf ein Netzlaufwerk gestellt werden, hat man eine maximal flexible Lösung, wo der Scanner im Prinzip überall stehen kann, solange eine Netzwerkdose in der Nähe ist.

              Der Prozess für den Benutzer ist einfach: Der Raspi läuft bei mir immer (ich habe noch keine Lösung gefunden, um ihn durch das Einschalten eines angesteckten USB-Gerätes zu starten). D.h. Scanner einschalten, warten bis er „da ist“ (wenige Sekunden), in der Zwischenzeit die Dokumente einlegen und auf den Scan-Knopf drücken. Der dadurch angestoßene Prozess über die remote Services, die den eigentlichen Scan auslösen dauert bei mir unter 1 Sekunde. Fast augenblicklich fängt der Scanner an zu arbeiten.

            • Powerinterface für Nedap Powerrouter

              Wer eine Solaranlage auf dem Dach hat und zufällig vor ein paar Jahren einen Powerrouter der Firma Nedap gekauft hat, kennt das Problem: Die Firma ist pleite gegangen und seitdem ist der Support irgendwo zwischen katastrophal und nicht vorhanden. Am schlimmsten ist, dass die Nedap Powerrouter ihre aktuellen Produktionsdaten nur an eine spezielle Internetseite unter www.mypowerrouter.com senden und per se keine Möglichkeit bieten, diese lokal zu empfangen, bzw. einzusehen. Zu allem Übel scheint es sich seit Januar 2021 abzuzeichnen, dass auch diese Internetseite früher oder später den Betrieb ganz einstellen wird, womit die ehemaligen Kunden der Firma Nedap vollends im Regen stehen.

              In Anbetracht dieser Umstände, haben sich seit einiger Zeit Betroffene intensiv Gedanken gemacht, wie man die Daten lokal abfangen, in einer Datenbank aufzeichnen und über ein passendes Interface graphisch darstellen kann. Es gibt auf photovoltaikforum.com einen längeren Thread zu diesem Thema, in dem der aktuelle Stand der Erkenntnisse verfolgt werden kann.

              Nicklas Grießer (ngrie) hat einen Webdienst entwickelt, der in der Lage ist, die Daten des Powerrouters lokal zu empfangen und anzuzeigen. Dave Mallou hat darum herum ein fertiges Image für den Raspberry Pi gebaut hat, das die Daten in einer InfluxDB speichert und mit Grafana anzeigt. Aufbauend auf dieser hervorragenden Vorarbeit habe ich hier eine Weiterentwicklung dieses Ansatzes zusammengestellt, bei dem ich im wesentlichen 2 Dinge geändert habe:

              1. Keine zusätzliche externe USB-Netzwerkkarte nötig
              2. AdGuard ist durch Pi-Hole ersetzt. Dies ist meine subjektive Präferenz, da ich Pi-Hole schon lange im Einsatz habe und einfach kenne und liebe. Soweit ich das überblicke, gibt es kaum echte objektive Argumente, die AdGuard hinter Pi-Hole zurückfallen lassen. Ist wohl eher frei nach dem Motto „Was der Bauer net kennt, …“ 😉
              3. Ich stelle (bisher) kein fertiges Image zur Verfügung, da ich der Meinung bin, dass die Schritte zur Installation nicht weiter kompliziert sind. Außerdem hat man durch das Selbermachen den großes Vorteil, dass man sein System genau kennt und leicht in einzelnen Details an seine Bedürfnisse/sein Umfeld anpassen kann.

              Grundkonzept

              Im lokalen LAN wird ein eigener DNS-Server eingerichtet (z.B. Pi-Hole, AdGuard, dnsmasq, bind9, o.ä.), der für die Webadresse „logging1.powerrouter.com“ (an die der Nedap Powerrouter seine Daten schicken möchte) eine lokale IP-Adresse ausgibt, unter der der Webdienst von Niklas läuft, der die Daten des Powerrouters entgegennimmt und in eine Datenbank (InfluxDB) schreibt. Auf diese DB kann dann ein graphisches Anzeigesystem (Grafana) zugreifen, um die Daten wieder im lokalen LAN als Webpage zur Verfügung zu stellen. All diese Dienste können auf einem einzigen kleinen Linux-Rechner (z.B. ein Raspberry Pi, oder eine VM) laufen.

              Im folgenden führe ich Schritt für Schritt durch die Installation.

              Installation und Einrichtunbg

              Raspberry OS installieren

              Das kleinste Image, ohne Desktop reicht völlig aus, der Server läuft eh im Prinzip „headless“, d.h. ohne Monitor, Maus und Tastatur. Sollte SSH aber nicht auf Anhieb funktionieren, dann ist zur Installation ein Monitor und eine Tastatur extrem hilfreich! In meinem Fall war der Raspi etwas wählerisch mit dem Netzwerkkabel…🥴

              • aktuelles RaspberryOS-Image installieren
                • am besten mit dem „Raspberry Pi Imager“ die SD-Karte/USB-Stick vorbereiten
                • Das Image kann über das verstecktes Menü (Shift+Cmd+X) vorkonfiguriert werden:
                  • WLAN und SSH-Passwort ändern bzw. anpassen
                  • Rechnername auf „powerinterface“ setzen
                • Dann Image auf die SD-Karte schreiben
              • Raspi booten und aktualisieren
                • apt update
                • apt full-install
              • feste LAN-IP-Adresse vergeben
                • Entweder im Router-DHCP die IP fest auf die MAC-Adresse des Raspi eintragen
                • Oder DHCP im Router komplett deaktivieren und den DHCP-Dienst im Pi-Hole aktivieren (s.u.)
                  • In diesem Fall dann hier eine feste IP für die MAC des Raspi eintragen
                • Oder dem Raspi selbst über /etc/interfaces eine feste IP-Adresse geben
                • Auf jeden fall sicherstellen, dass keine IP-Adresse doppelt vorkommen kann, das wäre absolut tödlich für das ganze LAN!!!
                  • => Die feste IP-Adresse darf nicht im Adressbereich liegen, den der DHCP an Clients verteilt!!!

              Pi-Hole

              Pi-Hole ist ein DNS-Server, der eigentlich zum filtern von Werbung im ganzen LAN gebaut ist. Er kann über den DHCP-Service auch die Verteilung der IP-Adressen übernehmen.

              Details siehe hier: https://pi-hole.net

              installieren

              curl -sSL https://install.pi-hole.net | bash

              ..und den Anweisungen folgen.

              konfigurieren:

              1. Passwort für Pi-Hole Admin-Site löschen: pihole -a -p
              2. Unter „Local DNS“ die Domain „logging1.powerrouter.com“ auf die IP des Raspi setzen
              3. Dafür sorgen, dass der Pi-Hole als DNS-Server im LAN per DHCP verteilt wird:
                1. Entweder in den DHCP-Einstellungen des Routers die IP des Raspi als lokalen DNS-Server eintragen
                2. Oder DHCP im Router deaktivieren und im Pi-Hole aktivieren.

              Pi-Hole Port 80 ändern

              Standardmäßig läuft die Admin-Seite des Pi-Hole auf Port 80. Da aber unter diesem Port das Webinterface für den Nedap Powerrouter laufen muss (der schickt die Daten nämlich nur an Port 80), müssen wir das Admin-Interface des Pi-Hole auf was anderes legen. Glücklicherweise installiert der Pi-Hole mit lighttpd einen vollwertigen Web-Server. Über den bieten sich gleich mehrere Lösungen:

              1. Gesamten lighttpd server auf einem anderen Port laufen lassen, z.B. 8080
                • nano /etc/lighttpd/lighttpd.conf
                • Zeile „server.port = 80“ ändern auf z.B. 8080
                • systemctl restart lighttpd
                  • Die Pi-Hole Admin-Seite wird damit somit so aufgerufen: http://<ip-vom-Raspi>:8080/admin
              2. Für die Admin-Seite einen Vhost mit Subdomäne einrichten, in der Form „http://pihole.powerinterface“
                • (bitte separat googeln)

              Am Schluß muss der Nedap Powerrouter neu gestartet werden, damit er sich den geänderten DNS-Server per DHCP holt. Über das lokale Menü am Gerät sicherstellen, dass der DNS-Server jetzt die IP-Adresse des powerinterface ist.

              InfluxDB installieren

              • Repository einbinden
                • wget -qO- https://repos.influxdata.com/influxdb.key | sudo apt-key add -
                • source /etc/os-release
                • echo "deb https://repos.influxdata.com/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/influxdb.list
              • Influx installieren
                • apt update
                • apt install influxdb
                • systemctl unmask influxdb.service
                • systemctl start influxdb.service
                • systemctl enable influxdb.service
              • Datenbank vorbereiten (jede Zeile ein neuer Befehl)
                • influx
                  create database home
                  use home
                  create user grafana with password '<eigenes Passwort vergeben>'
                  grant all on home to grafana


                  show databases
                  show users
                  show grants for grafana

                  quit

              Webdienst (ngrie)

              Node.js installieren

              RaspberryOS kommt entweder mit gar keinem Node.js installiert, oder mit einer alten Version, die man erst deinstallieren muss.

              apt remove node nodejs

              • Repository einbinden (alternativ kann auch gleich die neue Version 16 genommen werden mit „…/setup_16.x
                • curl -sSL https://deb.nodesource.com/setup_14.x | sudo bash -
              • Node.js installieren
                • apt install nodejs
                • node -v
                • npm -v

              installieren

              cd /srv
              git clone https://github.com/ngrie/powerinterface.git
              cd powerinterface
              npm install

              Konfiguration

              nano config.yml

              Folgenden Inhalt in die Datei kopieren, danach speichern und schließen:

              forwardRequests: true
              actions:
                - type: influxdb
                  host: 127.0.0.1
                  database: home
                  username: grafana
                  password: <Passwort von oben eintragen>
                  port: 8086

              Autostart-Dienst erstellen

              nano /etc/systemd/system/powerinterface.service

              folgenden Inhalt in die Datei kopieren:

              [Unit]
              Description=Powerinterface
              Documentation=https://github.com/ngrie/powerinterface
              After=network.target [Service]
              Type=simple
              User=root
              WorkingDirectory=/srv/powerinterface
              ExecStart=/usr/bin/node /srv/powerinterface/server.js
              Restart=on-failure

              [Install]
              WantedBy=multi-user.target

              Datei speichern und schließen.

              systemctl daemon-reload
              systemctl enable powerinterface
              systemctl start powerinterface

              Grafana

              installieren

              • Repository hinzufügen
                • wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
                • echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
              • installieren
                • sudo apt update
                • sudo apt install grafana
                • systemctl enable grafana-server

              konfigurieren

              Im Browser aufrufen: (Port 3000 auf der IP-Adresse des Raspi)
              http://powerinterface.local:3000

              Benutzer: admin
              Passwort: admin

              Man wird aufgefordert, das Passwort zu ändern.

              Configuration -> Add Datasource -> InfluxDB
              HTTP -> URL: http://powerinterface:8086/

              Database: home
              User: grafana
              Password: <siehe oben>

              Dashboards importieren

              Die drei Dashboards von Dave Mallou runterladen und entpacken.

              Aktuelle Daten
              Powerrouterdaten
              Statistik

              Unter Dashboards -> Manage Dashboards -> Import die drei json-Dateien importieren.

              Anwendung

              Interface

              Es gibt zwei Interfaces, die von Interesse sind. Das native Webinterface von ngrie, sowie die Diagramme, die über Grafana angezeigt werden.

              natives Webinterface von ngrie

              Hier werden die wichtigesten Live-Daten angezeigt, ähnlich wie auf der Übersichtsseite von mypowerrouter.com.

              Aufruf über:

              http://powerinterface.local/
              http://<ip-vom-Raspi>:80/

              Port 80 kann auch weggelassen werden.

              Im Hintergrund schreibt das Interface die Daten auch in die Influx-Datenbank.

              Grafana-Interface

              Auf diesem Interface werden die angespeicherten Daten aus der Datenbank dargestellt.

              Aufruf über:

              http://powerinterface.local:3000/
              http://<ip-vom-Raspi>:3000/

              Troubleshooting

              Bitte der Versuchung widerstehen, das ganze Setup in einem anderen LAN als dem endgültigen vorzubereiten, da u.a. der Pi-Hole bei der Installation sich die aktuelle IP-Adresse Raspberry Pi in die Konfiguration schreibt.

              Wenn dennoch etwas nicht funktioniert, am besten schrittweise den Fehler eingrenzen:

              Sicherstellen, dass die Dienste laufen

              service pihole-FTL status
              service influxdb status
              service powerinterface status
              service grafana-server status

              Der Status sollte jeweils grün sein und die Dienste sollten laufen. Wenn nicht, genau die Fehlermeldungen lesen und die entsprechenden Probleme beheben. Mit tail -fn1000 /var/log/syslog sieht man ev. mehr Details.

              Sicherstellen, dass der Powerrouter den Pi-Hole als DNS-Server verwendet

              Dazu lokal am Gerät über das Menü Wartung -> Status -> Internetanschluß -> DNS-Server prüfen, ob die IP des Raspi angezeigt wird.

              Sicherstellen, dass der Powerrouter den Pi-Hole als DNS-Server verwendet

              Dazu im Admin-Webinterface vom Pi-Hole im Menü „Query Log“ prüfen, ob es ca. 1 mal pro Minute eine DNS-Anfrage vom Client „PowerRouter“ an die Domäne „logging1.powerrouter.com“ gibt.

              Außerdem in einer Dos-box mit ping logging1.powerrouter.com prüfen, ob der Pi-Hole die „richtige“ IP-Adresse ausliefert – nämlich seine eigene!

              Prüfen, ob der Webdienst Daten bekommt

              Dazu den Webdienst im Browser aufrufen http://powerinterface, er sollte die live-Daten des Powerrouter anzeigen.

              Prüfen, ob die Daten in die InfluxDB gespeichert werden

              influx
              use home

              show series
              show measurements

              Bei den letzten beiden Befehlen sollte jeweils eine längere Liste von Daten angezeigt werden.

              Prüfen, ob Grafana auf die richtigen Daten zugreift

              Unter http://powerinterface:3000 -> Configuration -> Datasource -> InfluxDB alle Werte genauestens prüfen, insbesondere den Datenbank-Namen, Benutzer und Passwort.

              Ausblick

              Ihr dürft gerne in den Kommentaren schreiben, wenn irgend etwas nicht gut beschrieben ist, oder nicht funktioniert. Ich kann zwar keinen echten Support für jeden liefern, aber ich versuche schon, die Anleitung aktuell zu halten.


            • Beschattung mit sonnengeführten elektrischen Rollläden

              Schön langsam entwickeln sich die Temperaturen auch in Deutschland in Richtung Sommer. Da sich aufgrund von Corona immer noch viele die meiste Zeit zu Hause aufhalten, mag der Wunsch entstehen, seine elektrischen Rollläden zur Beschattung der Wohnung zu verwenden. Die einfachste Möglichkeit ist, alle Rollos auf 80% schließen und den ganzen Tag im Dunkeln sitzen. Da das aber nicht wirklich schön ist, wäre eine Beschattung mit sonnengeführten elektrischen Rollläden wesentlich cooler. Wie das geht, zeige ich hier.

              Als erstes braucht man natürlich neben den elektrischen Rollläden, eine bereits funktionierende Haussteuerung. Ich verwende bereits seit vielen Jahren FHEM dafür.

              Beschattung

              Die Idee ist, sich mit einem notify-device an ein Twilight-modul zu hängen, um damit alle paar Minuten den aktuellen Sonnenstand zu bekommen. Im notify-device wird dann ein Makro aufgerufen, das die neue Position jedes Rollladens anhand des aktuellen Sonnenstandes berechnet und diesen auf die neue Position fährt. Im Ergebnis bewegen sich alle paar Minuten alle Rollläden um einige Millimeter, sodass sie genau richtig abschatten — nicht zu viel (-> nicht zu dunkel) und nicht zu wenig (-> zu viel Sonneneinstrahlung).

              Sonnengeführt

              Zur Berechnung verwenden wir zwei Vektoren im Raum:

              • Der Sonneneinfallsvektor bestehend aus Azimuth und Elevation (Himmelsrichtung und Höhe der Sonne)
              • Der Rollo-Bewegungsvektor, der den Fahrweg des Rollos beschreibt

              Anschließend berechnen wir den Winkel der beiden Vektoren, der dem Winkel der Sonne zur Fensterfläche entspricht. Durch einfache Winkelberechnungen können wir nun die Rolloposition bestimmen, die für eine optimale Abschattung sorgt.

              elektrische Rollläden

              Für jeden Rollladen sollten eine Reihe von Werten berücksichtigt werden, die Einfluß auf die Beschattung haben:

              • Breite und Höhe des Fensters
              • Tiefe der Fensterlaibung (bzw. wie weit die Sonne „rein“ scheinen darf)
              • Dachwinkel (90 ° bei senkrechten Fenstern)
              • Höhe der Auf-/Vorbauten (z.b. außen liegender Rollokasten, oder ein Mauervorsprung
              • Fensterausrichtung (Himmelsrichtung)
              • Min-/Max Azimuth und Min-/Max Elevation für die Beschattung
              • Min-/Max schattenrelevante Position

              Umsetzung

              Die Logik des Makros läßt sich sicherlich in fast jeder Haussteuerung umsetzen, aber ich beschreibe hier wie es mit FHEM geht.

              Als erstes das Twilight-Modul. Gemäß der Dokumentation, hier die allgemeine Definition:

               

              define <name> Twilight <latitude> <longitude> [<indoor_horizon> [<Weather_Position>]]

              Die Parameter dürften selbstredend sein, latitude und longitude ist die eigene geographische Position, der indoor_horizon beschreibt, bei wieviel Grad Sonnenstand das Event indoor Sunset ausgelöst werden soll. Für die Beschattung brauchen wir ihn nicht. Eine fertige Definition wäre somit:

              define MyTwilight 49.123456 8.123456 -3

              Das notify-device (Dokumentation) ist einfach definiert:

              MyTwilight:azimuth:.* {
              }

              Somit wird es immer dann ausgelöst, wenn das Twilight-Device die Eigenschaft azimuth aktualisiert. Dies geschieht alle paar Minuten. Innerhalb der geschweiften Klammern kommt das eigentliche Steuerungsmakro zur sonnengeführten Beschattung mit elektrischen Rollläden:

              MyTwilight:azimuth:.* {
              	######### Rolladensteuerung
              	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
              	my $Elev = ReadingsVal('MyTwilight', 'elevation', '0');
              	my $Azim = ReadingsVal('MyTwilight', 'azimuth', '0');
              	Log 1, "Elev: $Elev";
              	Log 1, "Azim: $Azim";
              	# finales hochfahren, nach Sonnenuntergang, Deaktivierung (Aktivierung erfolgt ausserhalb kurz vor Sonnenaufgang)
              	if ($Elev < 22 && $Azim > 180) {
              		fhem("set BU_RolloS up");
              		fhem("set BU_RolloN up");
              		fhem("set KU_Rollo up");
              		fhem("set WC_Rollo up");
              		if ($Elev < 15) {
              			fhem("set GZ_RolloW up");
              			fhem("set notRolloSteuerung inactive");
              			fhem("set Rollos up");
              		}
              	}
              	# Fensterdefinitionen (DevName, Breite, Höhe, Tiefe, Dachwinkel, Aufbauhöhe, Fensterausrichtung, Aktiv)
              		# 0, DeviceName des Rollos
              		# 1, 2: Breit, Höhe (cm) = sichtbare Breite, Höhe des Fensters (Glas innerhalb des Rahmens)
              		# 3: Tiefe (cm) = Wie weit darf die Sonne "ins Fenster" scheinen (vom Fensteraussenscheibe messen!)
              		# 4: Dachwinkel (Grad) = native Neigung der Fensterscheibe (=Dach), bzw. 90° bei "normalen" Wandfenstern
              		# 5: Aufbauhöhe (cm) = Wieviel cm tragen Rollläden, Wandvorsprünge, etc. zur Fensterscheibe auf?
              		# 6: Fensterausrichtung (Grad) = In welche Himmelsrichtung "schaut" das Fenster? -> Senkrechte auf die Scheibe, Nord = 0°, Süd = 180°
              		# 7: Min Azim. (Grad) = ab welchem Azimut wird beschattet?
              		# 8: Max Azim. (Grad) = bis welchem Azimut wird beschattet?
              		# 9: Min Elev. (Grad) = ab welcher Elevation wird beschattet?
              		# 10: Max Elev. (Grad) = bis welcher Elevation wird beschattet?
              		# 11: Min Schatten (%) = minimale schattenrelevante Rollo-Position (z.B. 12%)
              		# 12: Max Schatten (%) = maximale schattenrelevante Rollo-Position (z.B. 98%)
              		# 13: Aktiv = 0 -> Deaktiviert, 1 -> Aktiv
              	my @BU_S = ("BU_RolloS", 77, 98, 15, 43, 0, 276, 90, 360, 29, 90, 16, 100, 1);
              	my @BU_N = ("BU_RolloN", 77, 98, 15, 43, 0, 276, 90, 360, 22, 90, 16, 100, 1);
              	my @KU = ("KU_Rollo", 77, 100, 15, 43, 0, 276, 90, 360, 21, 90, 16, 100, 1);
              	my @WC = ("WC_Rollo", 60, 90, 15, 43, 0, 276, 90, 360, 20, 90, 16, 100, 1);
              	my @SZ = ("SZ_Rollo", 300, 200, 35, 90, 15, 96, 0, 186, -10, 90, 14, 100, 1);
              	my @WZ = ("WZ_Rollo", 300, 200, 35, 90, 15, 96, 0, 186, 0, 90, 14, 100, 1);
              	my @GA = ("GA_Rollo", 105, 130, 15, 43, 0, 96, 0, 270, -10, 90, 13, 100, 1);
              	my @GZ_O = ("GZ_RolloO", 105, 130, 15, 43, 0, 96, 0, 270, 0, 90, 13, 100, 1);
              	my @GZ_W = ("GZ_RolloW", 125, 130, 15, 43, 0, 276, 90, 360, 15, 90, 14, 100, 1);
              	my @ArrFenster = (@BU_S, @BU_N, @KU, @WC, @SZ, @WZ, @GA, @GZ_O, @GZ_W);
              	# Array-Zugriffskonstanten
              	my $C_DEVNAME = 0;
              	my $C_BREITE = 1;
              	my $C_HOEHE = 2;
              	my $C_TIEFE = 3;
              	my $C_DACHWINKEL = 4;
              	my $C_AUFBAU = 5;
              	my $C_F_AUSRICHTUNG = 6;
              	my $C_MINAZIM = 7;
              	my $C_MAXAZIM = 8;
              	my $C_MINELEV = 9;
              	my $C_MAXELEV = 10;
              	my $C_MINPOS = 11;
              	my $C_MAXPOS = 12;
              	my $C_AKTIV = 13;
              	# allgemeine Variablen/Konstanten
              	my $pi = 3.1415926535897932;
              	my @V_Rollo = (0, 0, 0);	# Vektor Rollo
              	my @V_Sonne = (0, 0, 0);	# Vektor Sonne
              	my $Aufbauwinkel = 0;
              	my $Gesamtneigung = 0;
              	my $Theta = 0;
              	my $Phi = 0;
              	my $Winkel = 0;
              	my $RolloPos = 0;
              	# rad = grad * pi / 180; 		grad = rad * 180 / pi
              	# Sonnenvektor berechnen
              	$Theta = (90 + $Elev) * ($pi / 180);
              	$Phi = $Azim * ($pi / 180);
              	$V_Sonne[0] = sin($Theta) * sin($Phi);
              	$V_Sonne[1] = cos($Theta);
              	$V_Sonne[2] = sin($Theta) * cos($Phi);
              	foreach (@ArrFenster) {
              		my @Fenster = @$_;
              		# Gesamtneigung berechnen
              		$Aufbauwinkel = atan($Fenster[$C_AUFBAU] / $Fenster[$C_HOEHE]) * (180 / $pi);
              		$Gesamtneigung = $Fenster[$C_DACHWINKEL] + $Aufbauwinkel;
              		# Rollo-Vektor berechnen
              		$Theta = (90 + $Gesamtneigung) * ($pi / 180);
              		$Phi = $Fenster[$C_F_AUSRICHTUNG] * ($pi / 180);
              		$V_Rollo[0] = sin($Theta) * sin($Phi) * -1;
              		$V_Rollo[1] = cos($Theta);
              		$V_Rollo[2] = sin($Theta) * cos($Phi) * -1;
              		# Sonnenwinkel auf Fensterfläche berechnen (=Winkel der beiden Vektoren)
              		$Winkel = acos(($V_Rollo[0] * $V_Sonne[0] + $V_Rollo[1] * $V_Sonne[1] + $V_Rollo[2] * $V_Sonne[2]) /
              				  (sqrt($V_Rollo[0] ** 2 + $V_Rollo[1] ** 2 + $V_Rollo[2] ** 2) * 
              				  sqrt($V_Sonne[0] ** 2 + $V_Sonne[1] ** 2 + $V_Sonne[2] ** 2)));
              		# Rollo-Position bestimmen
              		if ($Fenster[$C_MINAZIM] < $Azim && $Azim < $Fenster[$C_MAXAZIM] && 
              			$Fenster[$C_MINELEV] < $Elev && $Elev < $Fenster[$C_MAXELEV]) {
              			$RolloPos = ((($Fenster[$C_TIEFE] + $Fenster[$C_AUFBAU]) / tan($Winkel)) / $Fenster[$C_HOEHE]) * 100;
              		} else {
              			$RolloPos = 100;
              		}
              		if ($RolloPos < 0) {$RolloPos = 0};
              		if ($RolloPos > 100) {$RolloPos = 100};
              		# Rollo-Pos auf schattenrelevanten Bereich skalieren
              		$RolloPos = ($Fenster[$C_MAXPOS] - $Fenster[$C_MINPOS]) * ($RolloPos / 100) + $Fenster[$C_MINPOS];
              		# Rollo auf Position fahren
              		if ($Fenster[$C_AKTIV] == 1) {
              			fhem("sleep 1; set ".$Fenster[$C_DEVNAME]." pct ".sprintf("%.0f", $RolloPos));
              			Log 1, "Rollo ".$Fenster[$C_DEVNAME]." auf ".sprintf("%.0f", $RolloPos);
              			#Log 1, "Rollo ".$Fenster[$C_DEVNAME].": Winkel ".($Winkel * 180 / $pi)." Theta ".($Theta*(180/$pi))." Phi ".($Phi*(180/$pi));
              			#Log 1, "Rollo ".$Fenster[$C_DEVNAME].": V_Sonne ".$V_Sonne[0]."; ".$V_Sonne[1]."; ".$V_Sonne[2];
              			#Log 1, "Rollo ".$Fenster[$C_DEVNAME].": V_Rollo ".$V_Rollo[0]."; ".$V_Rollo[1]."; ".$V_Rollo[2];
              			#Log 1, "Rollo ".$Fenster[$C_DEVNAME].": Tiefe ".$Fenster[$C_TIEFE]." Höhe ".$Fenster[$C_HOEHE];
              		}
              	}
              }

            • Eigenen DynDNS Server mit dynamischer IP Adresse betreiben

              Einen eigenen DynDNS Server mit dynamischer IP Adresse zu betreiben stellt eine grundsätzliche Herausforderung dar. Man hat ein handfestes Henne-vor-Ei-Problem: Wenn sich die IP-Adresse ändert, muss die Adresse im DNS geändert werden, obwohl der DynDNS Server unter dieser Adresse nicht mehr erreichbar ist…

              Detailprobleme

              Meine Konstellation dürfte im privaten Umfeld vermutlich des öfteren vorkommen. Ich habe mir irgendwann einmal günstig eine Domain registrieren lassen — nennen wir sie example.com. Diese liegt bei einem günstigen Registrar, der mir per Webfrontend Zugriff auf die cPanel-Administration erlaubt, worüber ich die DNS-Einträge für example.com editieren kann. Um jetzt zu Hause einen kleinen Server betreiben zu können (z.B. für Nextcloud oder einen kleinen Blog) sollte der A-Record für example.com auf die gerade aktuelle IP-Adresse des DSL-Anschlusses zeigen. Die Subdomains für blog oder cloud können per CNAME-Record einfach auf example.com verweisen. Analog dazu sollte der DynDNS Dienst unter ddns.example.com erreichbar sein. Somit müssen wir also erstmal eine Lösung finden, um unseren zentralen A-Record von example.com dynamisch aktualisieren zu können.

              DynDNS-Client für cPanel-DNS

              Die Lösungen hier hängen ausschließlich an den Möglichkeiten, die unser Registrar für example.com anbietet. Im Idealfall bietet er eine DynDNS Option an, die vom DSL-Router angesprochen werden kann. Mein Anbieter hat es mir leider nicht ganz so einfach gemacht. Immerhin gibt es aber die Möglichkeit auf das cPanel-API zuzugreifen. Damit kann man also programmatisch DNS-Einträge anzeigen und manipulieren.

              Konzept

              Somit haben wir also folgendes Konzept: Ein Skript prüft regelmäßig, ob sich die IP-Adresse geändert hat. Falls ja, macht es per cPanel-API ein Update des A-Records von example.com. Somit kann unser DynDNS-Dienst als „normaler“ Webserver unter ddns.example.com laufen und ist immer erreichbar.

              Tipp: Die TTL für „ddns.example.com“ sollte sehr klein (z.B. 60 Sekunden) gesetzt werden.

              Meine Anforderungen an einen DynDNS Server sind sehr gering. Ich habe im Familien- und Freundeskreis eine Handvoll Fritzboxen, die per VPN immer erreichbar sein sollen und somit einen DDNS-Eintrag brauchen. Es ist für mich (und die beteiligten anderen) völlig akzeptabel, dass ihre Fritzbox unter familienname.example.com erreichbar ist. Hätten sie einen kommerziellen DynDNS Dienst im Internet wäre es ja schließlich auch nicht anders.

              Somit kann ich also ohne Probleme das gleiche Skript für meinen eigentlichen DynDNS Server verwenden, wie ich selbst brauche, um meine eigene IP-Adresse aktuell zu halten.

              Umsetzung

              Unter https://github.com/raceybe/cpanel-ddns habe ich bereits eine passende Lösung hierfür gefunden. Also im wesentlichen ein PHP-Skript, das per cPanel-API einen beliebigen DNS-Eintrag aktualisiert. Das Readme ist bereits sehr ausführlich und die Umsetzung sollte eigentlich keine Probleme machen. Deswegen beschreibe ich jetzt nur kurz die Einbettung in den Apache-Server und das Zusammenspiel aller Komponenten.

              Das zentrale Update.php braucht eine Config.php an seiner Seite mit den Zugangsdaten, um auf das cPanel-API zugreifen zu können. Ausserdem gibt es eine Testclient.php, das ebenfalls eine Config.php braucht, um einen bestimmten Eintrag zu aktualisieren. Der Testclient spricht also mit Update.php auf der Serverseite. Ich nutze den Testclient, um per cron-job meine eigene IP aktuell zu halten. Dafür muß Update.php als Vhost im Apache-server sowohl über Port 80 intern erreichbar sein, als auch über Port 443 unter ddns.example.com aus dem Internet.

              Apache Server

              Datei /etc/apache2/sites-available/ddns.example.com

              Um die Konfiguration des eigentlichen Vhosts nicht für Port 80 und 443 doppelt zu pflegen, habe ich sie ausgelagert:

              <VirtualHost *:80>
                      Include /etc/apache2/ddns.vhost
              </VirtualHost>
              
              <VirtualHost *:443>
                      Include /etc/apache2/ddns.vhost
              
                      SSLCertificateFile /etc/letsencrypt/live/ddns.example.com/fullchain.pem
                      SSLCertificateKeyFile /etc/letsencrypt/live/ddns.example.com/privkey.pem
                      Include /etc/letsencrypt/options-ssl-apache.conf
              </VirtualHost>

              Auf die Anforderung und Einbindung des Let’s-Enctrypt Zertifikats gehe ich jetzt an der Stelle nicht gesondert ein, dafür gibt es genügend Dokumentation im Internet.

              Datei ddns.vhost

              ServerName ddns.example.com
              DocumentRoot /var/www/vhosts/ddns/
              DirectoryIndex index.php index.html
              ErrorLog /var/log/apache2/ddns.example.com-error.log
              TransferLog /var/log/apache2/ddns.example.com-access.log
              
              <Directory /var/www/vhosts/ddns/>
                  AllowOverride All
                  Options FollowSymLinks
                  <IfVersion < 2.3>
                      Order allow,deny
                      Allow from all
                  </IfVersion>
                  <IfVersion >= 2.3>
                      Require all granted
                  </IfVersion>
              </Directory>

              Datei /var/www/vhosts/ddns/.htaccess

              CGIPassAuth on
              SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
              
              <IfModule mod_rewrite.c>
                 RewriteEngine On
                 RewriteCond %{REQUEST_FILENAME} !-f
                 RewriteCond %{REQUEST_FILENAME} !-d
                 RewriteRule ^(.+)$ $1.php [QSA,L]
              </IfModule>

              Unter /var/www/vhosts/ddns/ kommt dann die Update.php. Da es mir etwas zu unsicher erschien die Konfigurationsdatei mit den Zugangsdateien im selben Verzeichnis neben die aus dem Internet zugängliche Update.php zu legen, habe ich den Pfad angepaßt und die Config.php unter /var/www/ abgelegt:

              require "../../ddns.config.php";

              Datei /var/www/ddns.config.php

              <?php
              /*********************************************************************************
              *Config Example
              *
              *Make sure this file is in the same folder as update.php and is called config.php
              *********************************************************************************/
              //cPanel API user, password, server, and port
              $cpUser = 'username';
              $cpPassword = 'MySuperSecretPassword';
              $cpServer = 'https://cp188.myregistrar.net';
              $cpPort = '2083';
              
              //Authorized ddns users, passwords, and hostnames
              $authUsers=array('user1','user2');
              $authPasswords=array('Password1','Password2');
              $authHostnames=array('example.com','host1.example.com');

              Hier können also für jeden zu aktualisierenden Host unterschiedliche Zugangsdaten hinterlegt werden. Der erste Eintrag ist für unseren cron-job, da er den A-Record für die Domain selbst aktualisiert. Jeder weitere Host braucht auch einen Benutzer und ein Passwort.

              An der Stelle merkt man auch, dass das ganze nicht für größere Installationen geeignet ist. Aber für meinen Bedarf reicht es locker.

              cron-job

              Analog und passend zu den Zugangsdaten, die in der ddns.config.php hinterlegt wurden, muss natürlich auch der Testclient konfiguriert werden:

              Datei /opt/updateIP/config.php

              <?php
              /*********************************************************************************
              *Config Example
              *
              *Make sure this file is in the same folder as testclient.php and is called config.php
              *********************************************************************************/
              //ddns client configuration details
              $myip='1.2.3.4';
              $hostname='example.com';
              $login='user1';
              $password='Password1';
              $server='ddns.example.com';

              Die Variable $myip= wird vom cron-job gesetzt und enthält die aktuelle IP-Adresse, die nach cPanel geschrieben werden soll.

              Datei /opt/updateIP/update.sh

              #!/bin/bash
              
              oldip=`grep myip= /opt/updateIP/config.php |sed 's/;.*//' |grep -v ^$ |sed s/.*=\ *//`
              ip=`curl -4s http://my.ip.fi`
              
              if [ "$oldip" != "$ip" ]
              then
                      sed -i "s/myip=$oldip/myip='$ip'/" /opt/updateIP/config.php
                      cd /opt/updateIP
                      php testclient.php
              fi
              

              Der Skript holt sich als erstes die bisherige IP-Adresse aus der Config.php. Dann holt er sich über http://my.ip.fi die aktuelle externe IP-Adresse des Anschlusses. Wenn der Router hierfür ein API anbietet, oder das ganze Script vielleicht eh auf dem Router läuft, kann man sich die externe IP-Adresse eventuell auch direkt vom DSL-Router holen. Hat sich die Adresse geändert, trägt er die neue Adresse in der Config.php ein und ruft den Testclient auf.

              Damit das funktioniert, muss lediglich sichergestellt sein, dass der cron-job unseren DynDNS-Server auch intern (d.h. wirklich ohne Internet!) unter ddns.example.com erreichen kann. Sollte sich die IP-Adresse nämlich geändert haben, würde ein externer DNS ja auf die alte IP-Adresse auflösen, womit der Update in die Hose geht! Das ganze läßt sich durch einen Eintrag in der /etc/hosts Datei lösen.

              Die Datei /opt/updateIP/update.sh kann jetzt per cron, z.B. über /etc/crontab eingebunden werden, sodaß sie beispielsweise zu jeder Stunden in Minute 13 läuft:

                13 *  *  *  * root    /opt/updateIP/update.sh >/dev/null 2>&1

              Zusammenfassung

              Damit sollten wir jetzt am Ziel sein: Der cron-job hält den A-Record für die ganze Domäne aktuell, womit der DynDNS-Server immer unter ddns.example.com erreichbar ist. Jetzt fehlt nur noch, externe Hosts, wie z.B. eine Fritzbox so zu konfigurieren, dass sie diesen DynDNS-Server verwendet. Die dafür nötige URL ist:

              https://user1:Passwort1@ddns.example.com/update?hostname=$hostname&myip=$myip

              User1 und Passwort1 müssen natürlich den hinterlegten Zugangsdaten in der Datei ddns.config.php entsprechen. Die genaue Bezeichnung der Variablen $hostname und $myip hängt vom verwendeten Router ab, bei der Fritzbox lauten sie z.B. <domain> und <ipaddr>. Am Besten in der Doku des Routers nachsehen.

            • Asterisk ConfBridge und Zoom-Meeting

              Zoom-Meetings sind in letzter Zeit sehr populär geworden und werden für sehr viele geschäftliche und private Konferenzen und Besprechungen verwendet. Zoom bietet auch die direkte Einwahl und Teilnahme per Telefon an. Die zugehörigen Einwahlnummern sind aber nur in den kostenpflichtigen Accounts sichtbar. Die Option, externe Teilnehmer aus einem Zoom-Meeting heraus anzurufen und mit hereinzuholen, kostet beträchtlichen Aufpreis. Da bei mir aber bereits ein Asterisk mit ConfBridge läuft, kam schnell der Gedanke, die Asterisk ConfBridge mit einem Zoom-Meeting zu verbinden.

              Voraussetzung: Asterisk ConfBridge

              Wie man einen Asterisk Telefonieserver installiert und konfiguriert würde an der Stelle viel zu weit führen, da dies ein sehr komplexes Thema ist. Ich verweise hierfür auf die offizielle Dokumentation, sowie auf www.voip-info.org, die sehr viele hilfreiche Informationen zum Thema bereit hält.

              Asterisk sollte also bereits mit dem Modul ConfBridge installiert sein und laufen. Interne Telefone sollten eingerichtet sein und von innen und außen erreichbar sein. Die Telefonkonferenz sollte ebenfalls von innen und außen erreichbar sein.

              Damit kann’s dann mit der Zoom-Anbindung losgehen:

              Konzept

              Im Groben funktioniert das Verbinden von Asterisk ConfBridge und Zoom-Meeting so, dass man in der Telefonkonferenz eine bestimmte Taste drückt. Asterisk fragt dann die Einwahlnummer, die Meeting-ID, sowie das Passwort über die Telefontastatur ab. Nach Eingabe all dieser Nummern wählt Asterisk sich in das Zoom-Meeting ein und verbindet das Telefonat dann mit der Telefonkonferenz.

              Asterisk ConfBridge erweitern

              Eine bisher nicht belegte Taste wird verwendet, um mithilfe der Funktion „dialplan_exec“ in den Dialplan zu verzweigen. Hier ein Beispiel:

               [sample_user_menu]
              type=menu
              *=playback_and_continue(conf-usermenu)
              *1=toggle_mute
              1=toggle_mute
              *2=dialplan_exec(addcaller,1,1)
              2=dialplan_exec(addcaller,1,1)
              *3=dialplan_exec(addcaller,2,1)
              3=dialplan_exec(addcaller,2,1)
              *4=decrease_listening_volume
              4=decrease_listening_volume
              *6=increase_listening_volume
              6=increase_listening_volume
              *7=decrease_talking_volume
              7=decrease_talking_volume
              ;*8=leave_conference
              ;8=leave_conference
              *9=increase_talking_volume
              9=increase_talking_volume

              Ich habe hier sogar 2 Tasten für die Einbindung externer Teilnehmer verwendet. Eine für Zoom-Meetings, die andere für normale externe Telefonnummern.

              Die Funktion dialplan_exec(addcaller,1,1) verzweigt in den Kontext „addcaller“ mit der Extension „1“ und der Priorität „1“.

              dialplan_exec(context,exten,priority)

              The dialplan_exec action allows a user to escape from the conference and execute commands in the dialplan. Once the dialplan exits the user will be put back into the conference. The possibilities are endless!

              Asterisk Doku

              Dialplan für Zoom-Meeting

              Im Dialplan passiert nun folgendes:

              [addcaller]
              ; add Zoom-Meeting   
              exten => 1,1,NoOp(${CALLERID})
               same = n,Read(CALL_NUMBER,enter-conf-call-number,25,2,60)
               same = n,Playback(auth-thankyou)
               same = n,Read(GLOBAL(MEETING_ID),enter-conf-call-number,13,2,30)
               same = n,Playback(auth-thankyou)
               same = n,Read(GLOBAL(PWD),enter-conf-call-number,13,2,30)
               same = n,Playback(auth-thankyou)
               same = n,Originate(PJSIP/${CALL_NUMBER}@T_9331206,exten,Features,92,1)
              
              ; add Mobile-Number    
              exten = 2,1,NoOp(${CALLERID})
               same = n,Read(CALL_NUMBER,enter-conf-call-number,25,2,60)
               same = n,Playback(auth-thankyou)
               same = n,Originate(PJSIP/${CALL_NUMBER}@VM_tom,exten,Features,91,1)
              • Read() (Doku) Liest mit der Aufforderung aus Enter-conf-call-number eine Zahlenfolge über die Telefontastatur ein und speichert sie in der Variablen CALL_NUMBER.
              • GLOBAL() (Doku) Ist um die Variable herum ist nötig, da CALL_NUMBER eine globale Variable ist.
              • Originate() (Doku) Macht den eigentlichen Anruf und verzweigt in den Kontext „Features“ mit der Extension „91“, sobald der Gegenpart (Zoom) den Anruf annimmt.
              [Features]
              exten = 91,1,Verbose(1, "Zoom-Meeting wird der Konferenz hinzugefügt.")
               same = n,Answer()
               same = n,Wait(3)
               same = n,Confbridge(conf1,,,sample_user_menu)
               same = n,Hangup()
              

              Im Kontext „Features“ (die Kontexte können übrigens heißen wie sie wollen) wird der Anruf dann über die Funktion Confbridge() (Doku) der bestehenden Konferenz hinzugefügt. Hierbei ist nur wichtig, dass mit „conf1“ die selbe Konferenz genannt wird, die ja bereits besteht.

              Die Konferenz hat jetzt somit mindestens 2 Teilnehmer, der interne Anrufer, die per Tastendruck die Anwahl des Zoom-Meetings veranlaßt hat, sowie das Zoom-Meeting als weiteren Teilnehmer. Natürlich können sich parallel noch beliebig viele andere Telefonteilnehmer in die Konferenz einwählen.

            • LED-Leiste als Hue-Lampe ansprechen

              Nachdem ich mir eine hübsche RGB-LED Leiste mit einem ESP32 zusammengebaut habe und diverse Arduino-Sketche mit allen möglichen Lichteffekten zusammen kopiert habe, entstand nach einiger Zeit doch der Wunsch, die LED-Leiste als Hue-Lampe ansprechen zu können.Der Vorteil ist eine einheitliche Bedienung über die Hue-App, sowie die Möglichkeit alle Lichter auf einen Schlag auszuschalten.

              Wie funktioniert das?

              Um eine LED-Leiste als Hue-Lampe ansprechen zu können, muss zunächst eine virtuelle Hue-bridge auf einem Host (z.B. als Docker Container) installiert werden. Diese emulierte Hue-bridge gibt sich gegenüber dem LAN und somit auch gegenüber der Hue-App als originale Bridge aus. Auf der anderen Seite ist sie aber in der Lage auch andere (nicht-Hue-)Lampen einzubinden. Ich habe ein paar verschiedener solcher Hue-Emulationen gefunden, aber am besten und flexibelsten ist meiner Meinung nach „diyHue“, zu finden auf GitHub (https://github.com/diyhue/diyHue). Man muss aber sagen, dass die Dokumentation ziemlich schlecht ist. Ich habe die Schritte zur Installation und Konfiguration deshalb hier zusammen gestellt: diyHue bridge emulation installieren

              Das schöne ist aber, dass man die LED-Leiste in beliebig viele „virtuelle“ Hue-Lampen aufteilen kann, die alle einzeln über die Hue-App wie klassische Hue-Lampen steuerbar sind. Dabei kann man angeben, wie viele „Pixel“ (=Leds) jede Lampe „breit“ sein soll und wie viele Pixel zwischen den Lampen „frei“ bleiben sollen.

              LED-Leiste vorbereiten

              Hier gehe ich jetzt nicht auf die Hardwareseite ein, das würde zu weit führen. Es gibt viele Tutorials im Netz, wie man eine RGB-Led-Leiste an einen Arduino-kompatiblen Microcontroller anschließt und verkabelt. Ich habe mich für SK9822-Leds entschieden, das sind billige Clone der APA102, die aus einzeln steuerbaren RGB-Leds bestehen. Als Controller verwende ich ein ESP32-Dev-Board von Espressif, weil es leistungsfähig ist (240 MHz, 4 MB Flash) und mit Bluetooth und Wifi alles mitbringen, was man braucht.

              Zum Programmieren habe ich die Arduino-IDE verwendet. Ein Beispiel-Sketch ist unter https://github.com/linuzer/Arduino-HueAPI zu finden.

              Natürlich müssen ein paar Dinge an die entsprechenden Gegebenheiten angepasst werden:

              NUM_LEDS Gesamtzahl der physikalischen LEDs in der Leiste
              DATA_PIN
              CLOCK_PIN
              Arduino-Pins, an denen die LEDs angeschlossen sind
              COLOR_ORDER
              LED_TYPE
              Reihenfolder der Farben. In der Doku der LEDs nachsehen
              HUE_(…) Der interessanteste Teil, konfiguriert die Anzahl und Größe der einzelnen virtuellen Hue-Lampen.
              useDhcp selbstredend. Wenn True, dann muss keine IP-Adresse konfiguriert werden.
              ConnectMQTT() Hier müssen IP, Port und Topic, sowie Client-Name angepasst werden.
              class HueApi Hier wird die Kommunikation mit der diyHue gekapselt, sowie Hue-spezifische Routinen abgelegt.
              StateGet()
              StatePut()
              Hier passiert die eigentliche Kommunikation mit der diyHue.
              Detect() Diese Methode produziert die richtige Antwort auf die Suchanfrage der diyHue,
              sodass die LED-Leiste als Hup-Lampe gefunden werden kann.

              Am Ende den Sketch einfach per USB-Kabel auf den ESP flashen. Ist das erfolgreich geschehen, sucht man mit dem Handy nach dem unverschlüsseltem WLAN des ESP und verbindet sich. Die SSID sollte die sein, die man mit HUE_Name festgelegt hat.

              Sodann kann man unter http://192.168.4.1 auf die Lampe zugreifen und das eigentliche WLAN konfigurieren. Nach einem Neustart, sollte die Lampe im WLAN auftauchen.

              Am besten testet man jetzt auch gleich das Arduino OTA-Update (Over-the-air Flashing). Wenn das auch funktioniert, kann man den ESP an seinem endgültigen Bestimmungsort verbauen.

              LED-Leiste als Hue-Lampe ansprechen

              Wenn bis hierhin alles geklappt hat, öffnet man die Hue-App auf dem Handy und sucht nach einer neuen Bridge. Eventuell muss man die IP-Adresse der diyHue bridge manuell eingeben. Ist die App mit der diyHue verbunden, sucht man nach neuen Lampen. Es kann einen Moment dauern (der sogar länger als der Timeout der App-Suche sein kann) und dann sollte die App eine (oder mehrere) neue Hue-Lampen vermelden. Diese kann man jetzt in gewohnter Art und Weise konfigurieren, sie Räumen, Szenen und Gruppen zuordnen, umbenennen, etc. … und natürlich steuern.

              An dieser Stelle ist man dann am Ziel. Jetzt kann man die selbstgebaute LED-Leiste als Hue-Lampe ansprechen. Jede virtuelle Hue-Lampe ist einzeln in der App zu sehen und kann entsprechend gesteuert werden.

              Troubleshooting / Debugging

              Bei Arduino Compiler-Fehlern genau lesen und alle Fehler beheben. Dabei immer mit dem obersten (!) Fehlern beginnen, der Rest können (verwirrende) Folgefehler sein. Am Anfang ist es wahrscheinlich, dass noch benötigte Bibliotheken nicht installiert sind (z.B. die ArduinoJson v6).

              Die Funktionen zum Laden und Wiederherstellen der gespeicherten Settings sind bewußt auskommentiert (in Setup() ), weil sie am Anfang Probleme machen können, wenn es noch keine gespeicherte Konfiguration auf dem ESP gibt.

              Wenn das Flashen geklappt hat, die Lampe aber nicht gefunden wird, dann mit dem Arduino Serial-Monitor (nur per USB-Kabel möglich) prüfen, ob es Fehlermeldungen gibt. Allerdings muss man echt Geduld haben, die Lampen tauchen häufig erste nach (viel-)maligem suchen in der App auf. Hintergrund ist, die diyHue sucht alle IP-Adressen im Netz ab und schaut, ob eine Lampe antwortet. Dabei ist das Zeitfenster pro IP-Adresse sehr klein… für einen ESP, der im WLAN hängt u.U. zu klein, sodass er nicht beim ersten Ping gefunden wird.

              Sobald das Wifi konfiguriert ist, am Router prüfen, welche IP-Adresse vergeben wurde.

              Per Serial-Monitor prüfen, ob die Lampe sich am MQTT-Broker anmelden konnte. Falls ja, ist man einen großen Schritt weiter: Ab jetzt kann man sich die wichtigsten Log-Meldungen auch per MQTT-Client auf dem Topic „iot/ledcontroller/log“ abholen. Damit ist auch ein remote Debugging per WLAN möglich.

              Jetzt kann man auch zu Debug-Zwecken weitere Log()-Kommandos in den Arduino-Code einbauen, um sich per MQTT über den Status informieren zu lassen.

              Zur weiteren Fehleranalyse empfiehlt es sich den Debug-Modus auf der diyHue einzuschalten und mit tail -f ...log | grep "z.b. IP-Adresse" nach relevanten messages filtern zu lassen. Details hierzu siehe meinen Artikel, oder die offizielle Doku. Mit den MQTT-Messages (was die Led-Leiste abschickt) und mit dem diyHue-Log (was dort ankommt und die Bridge damit macht) sollte es dann möglich sein allen Problemen auf die Schliche zu kommen.

            • diyHue installieren

              diyHue (GitHub) ist eine virtuelle Hue-bridge, die nicht nur eine originale Philips Hue-bridge mit ihren verbundenen Hue-Lampen ansprechen kann, sondern über eigene APIs eine breite Palette von anderen Lampen einbinden kann. Da sie aber netzwerkseitig das originale Hue-API spricht, kann sich die originale Hue-App (und alle Alternativen) an diyHue anmelden, und alle eingebundenen Lampen genau so steuern, wie die original Hue-Lampen. Wie man diyHue installieren kann, zeige ich in dieser Anleitung.

              Das Schaubild auf GitHub gibt einen guten Überblick, was alles eingebunden werden kann.

              diyHue installieren

              Leider ist die Dokumentation des Projekts ziemlich schlecht, sodass es nicht besonders leicht ist, alles richtig hinzukriegen. Deshalb dokumentiere ich hier die Schritte, mit denen ich Erfolg hatte. Natürlich kann man die virtuell Hue-Bridge diyHue auch anders installieren, meine Lösung erhebt weder den Anspruch, die einzig Richtige, noch die Beste zu sein.

              Ich habe mich für eine Installation in Docker entschlossen, da somit alles recht sauber gekapselt ist. Wer Docker noch nicht installiert hat, sollte sich erstmal ein bisschen einlesen, das führt jetzt hier zu weit, aber die Installation ist denkbar einfach.

              Netzwerk

              Dann muss man sich über das Netzwerk kurz Gedanken machen. Der diyHue container muss ja direkt aus dem Netz erreichbar sein. Ich habe mich entschlossen, meine Docker-container über eine macvlan-bridge direkt ins LAN zu hängen, ohne das sonst bei Docker übliche NAT Subnetz.

              Mein LAN hat die Adresse 192.168.3.x mit Subnet 255.255.255.0. Die oberen 32 Adressen, von 192.168.3.224 – 192.168.3.255 möchte ich für meine Container reservieren.

              Achtung! Diesen Bereich vom DHCP-Server des LAN ausklammern!

              Die entsprechende network bridge erzeugt dann dieser Befehl:

              docker network create -d macvlan --subnet=192.168.3.0/24 --ip-range=192.168.3.224/27 --aux-address 'host=192.168.3.224' --gateway=192.168.3.1 -o parent=eth0 pub_net

              --aux-address 'host=192.168.3.224' Diese Adresse wird vom Docker-DHCP ausgenommen. Sie dient der Verbindung zwischen Host und Docker-Netzwerk.

              -o parent=eth0 Das normale network interface des Hosts, bitte entsprechend anpassen.

              pub_net Der Name der Bridge an den persönlichen Geschmack anpassen.

              Als nächstes müssen wir eine Eigenheit von Docker, bzw. dem macvlan Treiber umgehen. Docker filtert standardmäßig den Traffic zwischen dem Host und den Containern heraus. Umgehen lässt sich das mit einem 2. Netzwerk-interface, welches wir unter /etc/network/interfaces eintragen. Die Befehle gelten alle für Debian/Ubuntu, auf anderen Distributionen könnte es anders sein.

              auto docker-bridge
              iface docker-bridge inet static
                      address 192.168.3.224
                      netmask 255.255.255.224
                      pre-up ip link add docker-bridge link eth0 type macvlan mode bridge
                      post-down ip link del docker-bridge link eth0 type macvlan mode bridge
                      up route add -net 192.168.3.224 netmask 255.255.255.224 gw 192.168.3.1
                      down route del -net 192.168.3.224 netmask 255.255.255.224 gw 192.168.3.1

              Wir verwenden hierfür die oben ausgeklammerte Adresse 192.168.3.224. Der Name „docker-bridge“ kann beliebig sein. Das Haupt-Interface „eth0“ muss entsprechend angepasst werden. Es wird auch gleich die nötige Route mit angelegt.

              Nach dem Speichern einmal service networking restart ausführen (oder analog auf anderen Systemen), um alles zu aktivieren.

              Container starten

              Damit können wir nun den eigentlichen Container installieren:

              docker run --name=diyHue --env="IP=192.168.3.225" --env="DEBUG=true" --env="MAC=dc:fe:07:e1:80:e6" --volume="/mnt/hue-emulator/:/opt/hue-emulator/export/:rw" --network=pub_net -p 1982:1982/udp -p 1900:1900/udp -p 443:443 -p 80:80 -p 2100:2100/udp --restart=always --detach=true diyhue/core:latest

              Tipp: Wer sich den Befehl eines bereits laufenden Containers nicht aufgeschrieben hat und wieder anzeigen möchte, findet hier eine Lösung!

              --env="IP=192.168.3.225" Sofern man dem Container eine feste IP zuweisen möchte.

              --env="DEBUG=true" Aktiviert den Debug-Modus. Details siehe https://diyhue.readthedocs.io/en/latest/AddFuncts/debug.html. Ansehen kann man sich das Logfile mit tail -f /var/lib/docker/containers/(ID)/(ID).log. Die (ID) bekommt man mit docker ps -a. Wenn alles läuft, nicht vergessen den Debug-Mode wieder auszuschalten! …sonst wird das Logfile ziemlich groß…!

              --network=pub_net Das oben konfigurierte Netzwerk, ggf. den Namen anpassen.

              -p 1982:1982/udp sowie die anderen: Diese Ports müssen freigegeben werden.

              diyhue/core:latest Name des Containers, neueste Version.

              diyHue konfigurieren

              Wenn alles läuft, sollte sich die diyHue unter http://192.168.2.225 (oder die IP, die sie bekommen hat) erreichen lassen.

              (Screenshot von installierter diyHue

              originale Hue-bridge koppeln

              Zunächst wird die Anzeige leer sein, denn als erstes muss die echte Hue-bridge gekoppelt werden. Wie das genau geht, steht unter https://diyhue.readthedocs.io/en/latest/configuration.html

              In Kurz: Unter „Import from bridge“ die IP-Adresse der echten Hue-bridge eintragen, den Hardware-Button auf der echten Bridge drücken, dann OK drücken. Wenn alles klappt, bestätigt eine kurze Meldung, wie viele Hue-Lampen importiert wurden.

              Hue-App koppeln

              Sehr wahrscheinlich werden jedoch immer noch keine Lampen angezeigt, denn es fehlt noch ein Raum, oder eine Gruppe, in die die Lampen zugeordnet werden. Leider bietet diyHue hier keine Möglichkeit das zu erledigen. Aber über die originale Philips-Hue App kann man diesen Schritt ausführen. Zum koppeln, läßt man die Hue-App nach einer neuen Bridge suchen. Wird sie nicht von alleine gefunden, gibt man über „Hilfe“ die IP-Adresse direkt ein. Zum finalen Verbinden, muss man sich auf der diyHue-Seite unter „Link device“ mit Benutzer „Hue“ und Passwort „Hue“ anmelden. Der anschließende Klick auf „Activate“ stellt den Hardware-button dar. Jetzt kann man innerhalb 30 Sekunden die Hue-App koppeln. Wenn auch dies geklappt hat, sollten in der Hue-App alle Lampen wie bisher zu sehen sein. Auch die diyHue-Seite sollte jetzt alle Lampen anzeigen.

              Wenn jetzt in der diyHue andere (nicht-Hue-)Lampen importiert werden, werden diese sofort auch in der Hue-App angezeigt und können über diese gesteuert werden.

              mögliche Probleme

              Die Hue-App findet die diyHue nicht

              Über „Hilfe“ die IP-Adresse von Hand eingeben

              Die Hue-App kann nicht Verbunden werden

              Es kann eine Hilfe sein, die Hue-App komplett zu löschen (mit allen Daten) und neu zu installieren.

              Die diyHue-Seite zeigt keine Lampen an

              Eine Möglichkeit ist, dass noch keine Lampen von der originalen Hue-Bridge importiert worden sind.

              Eine andere Möglichkeit ist, dass die Hue-App noch nicht gekoppelt ist und somit noch kein einziger Raum oder Gruppe angelegt werden konnte.

            • Docker run command rekonstruieren

              Ich hatte bereits mehrfach das Problem, dass ich mir den konkreten Befehl zum Starten eines bestimmten Docker-Containers nicht dokumentiert hatte, ihn aber hinterher wieder gebraucht habe. Aber es gibt einen Trick, mit dem man den Docker run command rekonstruieren kann.

              Als erstes mit diesem Befehl alle laufenden Container anzeigen lassen:

              docker ps -a

              Hier die „Container ID“ des gewünschten Containers kopieren.

              Dann:

              docker run --rm -v /var/run/docker.sock:/var/run/docker.sock assaflavie/runlike e0aced1e4011

              Hierbei die Container ID hinten durch die oben kopierte ersetzen.

              Der Befehl lädt einen speziellen Docker-Container temporär herunter und führt ihn aus. Als Ausgabe wird der run-Befehl des gefragten Containers angezeigt.