Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

Meine Projekte

Mein Server

  • Hardware

My projects

  • Autarkic weather station
  • Display

My server

  • Hardware
display

Display

Um das Dashboard nicht nur am PC oder am Handy anzuschauen, habe ich mir ein Display besorgt.
Display Anzeige


So kann ich mir auch die Daten der Wetterstationen anzeigen lassen.

Von links nach rechts und oben nach unten ist folgendes zu sehen:

  • Reihe 1
    • Uhrzeit
    • Wochentag als Wort
    • Datum
    • Geplantes Essen
    • Termine
  • Reihe 2
    • Werte der Nord/Ost und Süd/West Wetterstation
    • Tiefste Temperatur der Letzten 24 Stunden
    • Werte der Tamperatur Sensoren der 5 Wohnräume
  • Reiher 3
    • Temperatur der Wetterstationen Nord/Ost und Süd/West als Grafik


Die Infos werden von Node Red geholt. Dort nutze ich einen http in und http response Node.
Die verschiedenen Daten lasse ich mit flow.set in den Flow schreiben und die Webseite ruft diesen dann alle 60 Sekunden ab.
Die Charts hab ich in Grafana erstellt und als iframe in die Webseite eingebunden. Diese Aktualisieren sich alle 5 Minuten.
Code

Hinten am Display ist ein Raspberry Pi 4 befestigt.
Display Rückseite






Damit das ganze Funktioniert, braucht man ein HDMI auf Micro HDMI Kabel und ein Micro USB auf USB Kabel.
Ich habe mir noch einen 180° HDMI Winkel Adapter geholt, damit das Kabel nicht nach links heraus steht. Das USB Kabel hat beim Micro USB Anschluss schon einen 90° Winkel und da es ein Flachband ist, braucht man kein 180° Winkel.
HDMI und USB Anschluss am Display








Und einen 180° USB Adapter damit das Kabel rechts nicht heraus steht.
USB Anschluss am Raspberry Pi









Beim Micro HDMI habe ich ebenfalls auf einen Winkel geachtet, da nach oben nicht viel Platz ist, falls man das ganze in eine Hülle setzen will. Deshalb auch der Winkel für den USB-C Anschluss, damit man das Kabel in eine andere richtig heraus führen kann. Meistens sind Steckdosen ja unten ;-)
USB-C und HDMI Anschluss am Raspberry Pi







Auf dem Pi habe ich das Raspberry Pi OS 64bit Bookworm installiert.
Nach der Installation muss man noch ein paar Dinge einstellen.
Im Terminal oder über eine ssh Verbbindung:

sudo raspy-config

1 System Options
S5 Boot / Auto Login
B4 Desktop Autologin

1 System Options
S8 Browser
1 Chromium

3 Interface Options
I5 I2C

5 Localisation Options
L1 Locale

8 Update

6 Advanced Options
A6 Wayland

6 Advanced Options
A1 Expand Filesystem
sudo apt update && sudo apt upgrade -y


Ich nutze den Kiosk Modus, damit man den Browser nicht sieht sondern nur das Dashboard.

sudo apt install wtype

sudo nano .config/wayfire.ini


Folgendes einfügen:

[autostart]
panel = wfrespawn wf-panel-pi
background = wfrespawn pcmanfm --desktop --profile LXDE-pi
xdg-autostart = lxsession-xdg-autostart
chromium = chromium-browser http://IP:PORT/ui --kiosk --noerrdialogs --disable-infobars --no-first-run --ozone-platform=wayland --enable-features=OverlayScrollbar --start-maximized
screensaver = false 
dpms = false 

Bei IP:PORT könnt Ihr auch DOMAIN:PORT eintragen, falls euer Node Red Dashboard darüber erreichbar ist.


Ich regel noch die Helligkeit Nachts herunter. Man kann damit aber auch noch mehr Dinge einstellen. https://www.ddcutil.com/

sudo vi /boot/firmware/config.txt

Einfügen oder ändern

dtparam=i2c_vc_on
dtoverlay=vc4-kms-v3d


sudo vi /etc/modules

Folgendes eintragen

i2c_dev
sudo reboot


sudo apt install ddcutil


Um zu sehen wie viele und welche Displays erkannt werden:

sudo ddcutil detect


Was mir angezeigt wird:

sudo ddcutil detect
Display 1
   I2C bus:  /dev/i2c-20
   DRM connector:           card1-HDMI-A-1
   EDID synopsis:
      Mfg id:               HCH - UNK
      Model:                HETECH
      Product code:         4  (0x0004)
      Serial number:        20200915
      Binary serial number: 20180608 (0x0133ee80)
      Manufacture year:     2018,  Week: 26
   VCP version:         2.2


Um zu sehen was man einstellen kann:

sudo ddcutil capabilities


Was mir angezeigt wird:

Model: RTK
MCCS version: 2.2
Commands:
   Op Code: 01 (VCP Request)
   Op Code: 02 (VCP Response)
   Op Code: 03 (VCP Set)
   Op Code: 07 (Timing Request)
   Op Code: 0C (Save Settings)
   Op Code: E3 (Capabilities Reply)
   Op Code: F3 (Capabilities Request)
VCP Features:
   Feature: 02 (New control value)
   Feature: 04 (Restore factory defaults)
   Feature: 05 (Restore factory brightness/contrast defaults)
   Feature: 06 (Restore factory geometry defaults)
   Feature: 08 (Restore color defaults)
   Feature: 0B (Color temperature increment)
   Feature: 0C (Color temperature request)
   Feature: 10 (Brightness)
   Feature: 12 (Contrast)
   Feature: 14 (Select color preset)
      Values:
         01: sRGB
         02: Display Native
         04: 5000 K
         05: 6500 K
         06: 7500 K
         08: 9300 K
         0b: User 1
   Feature: 16 (Video gain: Red)
   Feature: 18 (Video gain: Green)
   Feature: 1A (Video gain: Blue)
   Feature: 52 (Active control)
   Feature: 60 (Input Source)
      Values:
         01: VGA-1
         03: DVI-1
         0f: DisplayPort-1
         11: HDMI-1
   Feature: 87 (Sharpness)
   Feature: AC (Horizontal frequency)
   Feature: AE (Vertical frequency)
   Feature: B2 (Flat panel sub-pixel layout)
   Feature: B6 (Display technology type)
   Feature: C6 (Application enable key)
   Feature: C8 (Display controller type)
   Feature: CA (OSD/Button Control)
   Feature: CC (OSD Language)
      Values:
         01: Chinese (traditional, Hantai)
         02: English
         03: French
         04: German
         06: Japanese
         0a: Spanish
         0d: Chinese (simplified / Kantai)
   Feature: D6 (Power mode)
      Values:
         01: DPM: On,  DPMS: Off
         04: DPM: Off, DPMS: Off
         05: Write only value to turn off display
   Feature: DF (VCP Version)
   Feature: FD (Manufacturer specific feature)
   Feature: FF (Manufacturer specific feature)


Für die Helligkeit ist es in meinem Fall die „Feature: 10 (Brightness)“ die ich brauche:

sudo ddcutil getvcp 10


Was mir angezeigt wird:

VCP code 0x10 (Brightness                    ): current value =    90, max value =   100


sudo ddcutils setvcp 10 XX

XX durch die Zahl ersetzen auf die man die Helligkeit setzen möchte. Dabei ist 0 Maximale Helligkeit, 100 Minimale Helligkeit.


Ich lasse die Helligkeit um 5 Uhr morgens auf 90 setzen und um 21 Uhr auf 100. Also 100 ist das Dunkelste nicht das hellste ^_^

crontab -e

0 5 * * * sudo ddcutil setvcp 10 90
0 21 * * * sudo ddcutil setvcp 10 100

Wie die Webseite aufgebaut ist

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #000;
            color: #e2e2e2;
            display: none;
        }

        .container {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-areas:
                "time food appointments"
                "nosw min24 rooms"
                "grafana grafana grafana";
            gap: 10px;
            padding: 10px;
        }

        /* Grid-Zuweisungen */
        .box.time         { grid-area: time; }
        .box.food         { grid-area: food; }
        .box.appointments { grid-area: appointments; }
        .box.nosw         { grid-area: nosw; }
        .box.min24        { grid-area: min24; }
        .box.rooms        { grid-area: rooms; }
        .grafana-container{ grid-area: grafana; }

        .box {
            background: #000;
            border-radius: 0;
            padding: 10px;
            color: #e2e2e2;
        }

        .box h2 {
            margin: 0 0 10px 0;
            color: #e2e2e2;
            font-size: 1.2em;
        }

        table {
            width: 100%;
            border-collapse: collapse;
        }

        table td {
            padding: 5px;
            border: none;
            color: #e2e2e2;
        }

        .grafana-container {
            background: #000;
            border: 1px solid #333;
            padding: 0;
            display: flex;
            flex-wrap: wrap;
        }

        .grafana-iframe {
            width: 50%;
            height: 220px;
            border: none;
            background: #1f1f1f;
            box-sizing: border-box;
        }

        @media (max-width: 767px) {
            .container {
                grid-template-columns: 1fr;
                grid-template-areas:
                    "time"
                    "food"
                    "appointments"
                    "nosw"
                    "min24"
                    "rooms"
                    "grafana";
            }
            .grafana-iframe {
                width: 100%;
            }
        }

        .error-message {
            color: #ff0000;
            padding: 10px;
            background: #ffeeee;
            border-radius: 4px;
            margin-bottom: 10px;
            grid-column: 1 / -1;
            color: #000;
        }

        /* Temperatur-Farben */
        .temp-cold { color: #009aee; }   /* blau */
        .temp-ok   { color: #00de00; }   /* grün */
        .temp-hot  { color: #e52632; }   /* rot */
    </style>
</head>
<body>
    <div class="container">
        <!-- Box für Uhrzeit und Datum -->
        <div class="box time">
            <p id="time" style="font-size: 2.5em; margin: 0;"></p>
            <p id="day" style="font-size: 2em; margin: 0;"></p>
            <p id="date" style="font-size: 1.5em; margin: 0;"></p>
        </div>

        <!-- Box für geplantes Essen -->
        <div class="box food">
            <h2>Geplantes Essen</h2>
            <p><strong>Heute:</strong> <span id="food-today">—</span></p>
            <p><strong>Morgen:</strong> <span id="food-tomorrow">—</span></p>
        </div>

        <!-- Box für Termine -->
        <div class="box appointments">
            <h2>Termine</h2>
            <table id="appointments-table"></table>
        </div>

        <!-- Box für Sensoren (Nordosten/Südwesten) -->
        <div class="box nosw">
            <table>
                <tr><td class="bold">NO</td><td class="bold">SW</td></tr>
                <tr>
                    <td><span id="no-temp">—</span> °C</td>
                    <td><span id="sw-temp">—</span> °C</td>
                </tr>
                <tr>
                    <td><span id="no-hum">—</span> %</td>
                    <td><span id="sw-hum">—</span> %</td>
                </tr>
                <tr>
                    <td id="no-ts">—</td>
                    <td id="sw-ts">—</td>
                </tr>
            </table>
        </div>

        <!-- Box für Minimalwerte der letzten 24 Stunden -->
        <div class="box min24">
            <h2>Min 24 Stunden</h2>
            <p><span id="min24-temp">—</span> °C</p>
            <p><span id="min24-hum">—</span> %</p>
            <p><span id="min24-pre">—</span> hPa</p>
            <p id="min24-time">—</p>
        </div>

        <!-- Box für Sensoren in verschiedenen Räumen -->
        <div class="box rooms">
            <table>
                <tr>
                    <td>Wohn</td>
                    <td><span id="livingroom-temp">—</span> °C</td>
                    <td><span id="livingroom-hum">—</span> %</td>
                </tr>
                <tr>
                    <td>Küche</td>
                    <td><span id="kitchen-temp">—</span> °C</td>
                    <td><span id="kitchen-hum">—</span> %</td>
                </tr>
                <tr>
                    <td>Bad</td>
                    <td><span id="bathroom-temp">—</span> °C</td>
                    <td><span id="bathroom-hum">—</span> %</td>
                </tr>
                <tr>
                    <td>Schlaf</td>
                    <td><span id="bedroom-temp">—</span> °C</td>
                    <td><span id="bedroom-hum">—</span> %</td>
                </tr>
                <tr>
                    <td>Flur</td>
                    <td><span id="corridorfritz-temp">—</span> °C</td>
                    <td><span id="corridorfritz-hum">—</span> %</td>
                </tr>
            </table>
        </div>

        <!-- Grafana-Charts -->
        <div class="grafana-container">
            <iframe class="grafana-iframe" data-original-url="http://192.168.188.74:3001/d-solo/bevk89hgy914wa/48-stunden?from=now-48h&to=now&timezone=Europe%2FBerlin&orgId=1&theme=dark&panelId=1&__feature.dashboardSceneSolo" title="Temperatur (48 Stunden)"></iframe>
            <iframe class="grafana-iframe" data-original-url="http://192.168.188.74:3001/d-solo/cevk8ujjbevwgc/7-tage?from=now-7d&to=now&timezone=Europe%2FBerlin&orgId=1&theme=dark&panelId=1&__feature.dashboardSceneSolo" title="Temperatur (7 Tage)"></iframe>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            document.body.style.display = 'block';
            updateClock();
            setInterval(updateClock, 1000);
            initGrafanaIframes();
            fetchData();
            setInterval(fetchData, 60000);
            setInterval(refreshGrafanaIframes, 5 * 60 * 1000);
        });

        function updateClock() {
            const now = new Date();
            document.getElementById('time').textContent = now.toLocaleTimeString('de-DE');
            document.getElementById('date').textContent = now.toLocaleDateString('de-DE');
            document.getElementById('day').textContent = now.toLocaleDateString('de-DE', { weekday: 'long' });
        }

        function initGrafanaIframes() {
            const now = new Date();
            const iframes = document.querySelectorAll('.grafana-iframe');
            iframes.forEach(iframe => {
                let url = iframe.dataset.originalUrl;
                if (url.includes('48-stunden')) {
                    const from = now.getTime() - 48 * 60 * 60 * 1000;
                    const to = now.getTime();
                    url = url.replace(/from=now-48h/, `from=${from}`).replace(/to=now/, `to=${to}`);
                }
                else if (url.includes('7-tage')) {
                    const from = now.getTime() - 7 * 24 * 60 * 60 * 1000;
                    const to = now.getTime();
                    url = url.replace(/from=now-7d/, `from=${from}`).replace(/to=now/, `to=${to}`);
                }
                iframe.src = url;
            });
        }

        function refreshGrafanaIframes() {
            const now = new Date();
            const iframes = document.querySelectorAll('.grafana-iframe');
            iframes.forEach(iframe => {
                try {
                    let url = iframe.dataset.originalUrl;
                    if (url.includes('48-stunden')) {
                        const from = now.getTime() - 48 * 60 * 60 * 1000;
                        const to = now.getTime();
                        url = url.replace(/from=[^&]*/, `from=${from}`).replace(/to=[^&]*/, `to=${to}`);
                    }
                    else if (url.includes('7-tage')) {
                        const from = now.getTime() - 7 * 24 * 60 * 60 * 1000;
                        const to = now.getTime();
                        url = url.replace(/from=[^&]*/, `from=${from}`).replace(/to=[^&]*/, `to=${to}`);
                    }
                    iframe.src = url + '&_=' + now.getTime();
                } catch (error) {
                    console.error('Fehler beim Aktualisieren des Iframes:', error);
                }
            });
        }

        // Temperatur einfärben
        function colorizeTemperature(elementId, value) {
            const el = document.getElementById(elementId);
            if (!el) return;
            el.classList.remove('temp-cold', 'temp-ok', 'temp-hot');
            const temp = parseFloat(value);
            if (isNaN(temp)) return;
            if (temp < 10) {
                el.classList.add('temp-cold');
            } else if (temp <= 23) {
                el.classList.add('temp-ok');
            } else {
                el.classList.add('temp-hot');
            }
        }

        async function fetchData() {
            try {
                const response = await fetch('http://192.168.188.74:1880/api/data');
                if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
                const data = await response.json();

                if (data.food) {
                    document.getElementById('food-today').textContent = data.food.today || '—';
                    document.getElementById('food-tomorrow').textContent = data.food.tomorrow || '—';
                }

                if (data.appointments && Array.isArray(data.appointments)) {
                    const table = document.getElementById('appointments-table');
                    const rows = data.appointments.map(appt => {
                        const [name, date] = appt.split('\t');
                        return `<tr><td>${name}</td><td>${date}</td></tr>`;
                    }).join('');
                    table.innerHTML = rows || '<tr><td colspan="2">—</td></tr>';
                }

                if (data.sensors) {
                    if (data.sensors.no) {
                        document.getElementById('no-temp').textContent = data.sensors.no.temp || '—';
                        colorizeTemperature('no-temp', data.sensors.no.temp);
                        document.getElementById('no-hum').textContent = data.sensors.no.hum || '—';
                        document.getElementById('no-ts').textContent = data.sensors.no.ts || '—';
                    }
                    if (data.sensors.sw) {
                        document.getElementById('sw-temp').textContent = data.sensors.sw.temp || '—';
                        colorizeTemperature('sw-temp', data.sensors.sw.temp);
                        document.getElementById('sw-hum').textContent = data.sensors.sw.hum || '—';
                        document.getElementById('sw-ts').textContent = data.sensors.sw.ts || '—';
                    }
                }

                if (data.min24) {
                    document.getElementById('min24-temp').textContent = data.min24.temp || '—';
                    colorizeTemperature('min24-temp', data.min24.temp);
                    document.getElementById('min24-hum').textContent = data.min24.hum || '—';
                    document.getElementById('min24-pre').textContent = data.min24.pre || '—';
                    document.getElementById('min24-time').textContent = data.min24.time || '—';
                }

                if (data.rooms) {
                    const updateSensor = (id, value, isTemp = false) => {
                        const el = document.getElementById(id);
                        if (!el) return;
                        if (value !== undefined) {
                            el.textContent = id.includes('-hum') ? Math.round(value) : value;
                            if (isTemp) colorizeTemperature(id, value);
                        } else {
                            el.textContent = '—';
                        }
                    };
                    updateSensor('livingroom-temp', data.rooms['livingroom-temp'], true);
                    updateSensor('livingroom-hum', data.rooms['livingroom-hum']);
                    updateSensor('kitchen-temp', data.rooms['kitchen-temp'], true);
                    updateSensor('kitchen-hum', data.rooms['kitchen-hum']);
                    updateSensor('bathroom-temp', data.rooms['bathroom-temp'], true);
                    updateSensor('bathroom-hum', data.rooms['bathroom-hum']);
                    updateSensor('bedroom-temp', data.rooms['bedroom-temp'], true);
                    updateSensor('bedroom-hum', data.rooms['bedroom-hum']);
                    updateSensor('corridorfritz-temp', data.rooms['corridorfritz-temp'], true);
                    updateSensor('corridorfritz-hum', data.rooms['corridorfritz-hum']);
                }
            } catch (error) {
                console.error('Fehler beim Abrufen der Daten:', error);
            }
        }
    </script>
    <script>
        window.addEventListener('error', function(e) {
            if (e.message.includes('WEBGL_debug_renderer_info')) {
                e.preventDefault();
                e.stopPropagation();
            }
        }, true);
    </script>
</body>
</html>
display.txt · Zuletzt geändert: von Administrator

Seiten-Werkzeuge