Monitorowanie Temperatury w Serwerowni

W dzisiejszych czasach prawie każda firma posiada mniejszą lub większa serwerownie, która powinna być przed administratorów monitorowana. Ponieważ monitorowanie temperatury w serwerowni dziś to bardzo ważny aspekt bezpieczeństwa. Temperatura oraz wilgotność ma bezpośredni wpływ na żywotność urządzeń znajdujących się w serwerowni. Dlatego też, monitorowanie temperatury oraz wilgotności jest nie tylko zalecane, ale często obowiązkowe.

Współcześnie, kiedy serwerownie są coraz większe i bardziej skomplikowane, rola monitorowania temperatury staje się fundamentem efektywnego zarządzania infrastrukturą IT.

W niniejszym artykule przedstawię sposób na proste ale jednocześnie skuteczne monitorowanie temperatury oraz wilgotności w serwerowni.

Jaka temperatura i wilgotność w serwerowni?

Istnieją standardy, takie jak ASHRAE (American Society of Heating, Refrigerating and Air-Conditioning Engineers), które także podają wytyczne dotyczące temperatury i wilgotności w serwerowniach.

Bardzo dużo wykresów oraz opinii można znaleźć na internecie. Poniżej moje osobiste opracowanie na podstawie aktualnych danych oraz własnych doświadczeń.

Prawda jest taka że im bardziej zawansowana serwerownia tym łatwiej jest utrzymać idealne parametry, no ale nie zawsze jest idealnie. Staramy się utrzymać chociaż te dopuszczalne.

Przygotowanie środowiska

Zaczynamy od aktualizacji listy pakietów

apt update

Instalacja Pakietów

apt install php php-cgi php-mysqli php-pear php-mbstring libapache2-mod-php php-common php-phpseclib php-mysql mariadb-server mariadb-client -y

Konfiguracja bezpieczeństwa

mysql_secure_installation

(Optymalne) Odpowiedzi na pytania: n,y,y,y,y,y

Pobieranie i instalacja phpMyAdmin

wget -P Downloads https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.tar.gz
mkdir /var/www/html/phpmyadmin
tar xvf phpMyAdmin-latest-all-languages.tar.gz --strip-components=1 -C /var/www/html/phpmyadmin
cp /var/www/html/phpmyadmin/config.sample.inc.php /var/www/html/phpmyadmin/config.inc.php

Konfiguracja tajnego klucza phpMyAdmin

nano /var/www/html/phpmyadmin/config.inc.php
$cfg['blowfish_secret'] = 'JOFw435365IScA&Q!cDugr!lSfuAz*OW';

Edycji pliku konfiguracyjnego phpMyAdmin i ustawienia sekret Blowfish, który jest używany do szyfrowania ciasteczek należy podać własne

Ustawienia uprawnień i właściciela dla phpMyAdmin:

chmod 660 /var/www/html/phpmyadmin/config.inc.php
chown -R www-data:www-data /var/www/html/phpmyadmin

Tworzenie użytkownika i nadanie uprawnień (przykład)

CREATE USER 'root'@'localhost' IDENTIFIED BY 'tomek';
GRANT ALL PRIVILEGES ON *.* TO 'tomek'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;

Jak już wszystko mamy skonfigurowane, nadszedł czas aby umieścić nasze pliki. Pliku umieszczamy w katalogi /var/www/html

Pliki PHP

index.php

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wykres</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body class="d-flex justify-content-center mt-3">
    <div class="d-flex flex-column justify-content-center align-items-center w-25">
        <h2>Lista czujników</h2>
        <ul class="list-group w-100">
            <?php
                include_once("config.php");
                // Pobranie listy czujników
                $query = $pdo -> prepare("SELECT ID, nazwa FROM listaczujnikow");
                $query -> execute();
                $lista = $query->fetchAll(PDO::FETCH_ASSOC);
                //wygenerowanie listy czujników
                foreach ($lista as $one) {
                    echo "<li class='list-group-item d-flex flex-row justify-content-between pe-5 ps-5 pt-3 pb-3'> <h3>" . $one['nazwa'] . "</h3>";
                    echo "<a href='wykres.php?id=" . $one['ID'] . "' class='btn btn-primary'>Zobacz</a></li>";
                }
            ?>
        </ul>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>

config.php

<?php
// Zdefiniowanie danych do logowania do bazy danych
define("DBLOGIN", "login");
define("DBPASS", "haslo");
define("DBADDRESS", "localhost");
define("DBNAME", "baza");

define("DELAY", 1);

// Stworzenie łączenia z bazą danych
$pdo;
try {
    $dsn = "mysql:host=" . DBADDRESS . ";dbname=" . DBNAME . ";charset=utf8mb4";
    $pdo = new PDO($dsn, DBLOGIN, DBPASS);
} catch (PDOException $e) {
    echo 'Błąd połączenia: ' . $e->getMessage();
}
?>

cron.php

<?php
    include_once("config.php");
    
    // Sprawdzenie czy już pobrać dane
    $godzina = date("H"); // Pobiera godzinę w formacie 24-godzinnym
    $minuta = date("i"); // Pobiera minutę
    $sumaMinut = 60 * $godzina + $minuta;
    if ($sumaMinut % DELAY == 0) {
        // Pobranie IP i ID czujników
        $query = $pdo->prepare("SELECT IP, ID FROM listaczujnikow");
        $query -> execute();
        $lista = $query -> fetchAll(PDO::FETCH_ASSOC);
        foreach ($lista as $one) {
            try {
                // Pobranie pliku
                $ch = curl_init($one['IP'] . "/dane");
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                $plik = curl_exec($ch);
                curl_close($ch);
                // Dekodowanie JSON
                if ($plik !== false) {
                    $data = json_decode($plik, true); // Uwaga na drugi argument, true oznacza, że dekodujemy do tablicy asocjacyjnej
                } else {
                    echo "Nie udało się pobrać pliku z adresu: " . $one['IP'];
                    continue; // Przechodzimy do następnej iteracji pętli
                }
                // Dodanie rekordu do bazy
                $query = $pdo->prepare("INSERT INTO  danezczujnikow(ID, IDCzujnikow, temperatura, wilgotnosc, data) VALUES
                (NULL, :idczujnika, :temperatura, :wilgotnosc, NULL)");
                $query -> bindValue(':idczujnika', $one['ID'], PDO::PARAM_INT);
                $query -> bindValue(':temperatura', $data['temperature']);
                $query -> bindValue(':wilgotnosc', $data['humidity']);
                $query -> execute();
            } catch (\Throwable $th) {
                if ($th instanceof \ErrorException && strpos($th->getMessage(), 'file_get_contents') !== false) {
                    echo "Nie udało się pobrać pliku z adresu: " . $one['IP'];
                } else {
                    echo "Wystąpił inny błąd: " . $th->getMessage();
                }
                exit();
            }
        }
        echo "Wszytko przeprowadzono prawidłowo";
    }

    exit();

wykres.php

<?php
    if (!isset($_GET['id'])) {
        header("Location: index.php");
    }
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wykres</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body class="d-flex justify-content-center mt-3 position-relative">
    <img class="position-absolute img-fluid z-1" src="" alt="">
    <div class="d-flex flex-column justify-content-center align-items-center w-75">
            <!-- PHP -->
            <?php
                include_once("config.php");
                // Pobranie nazwy czujnika
                $query = $pdo->prepare("SELECT nazwa FROM listaczujnikow WHERE ID = :id");
                $query -> bindValue(':id', $_GET['id'], PDO::PARAM_INT);
                $query -> execute();
                $lista = $query -> fetchAll(PDO::FETCH_ASSOC);
                $nazwaCzytnika = $lista[0]['nazwa'];
                
                // Pobranie danych z ostatniego dnia
                $limit = (60 * 3) / DELAY;
                $query = $pdo->prepare("SELECT * FROM danezczujnikow WHERE IDCzujnikow = :id ORDER BY data DESC LIMIT :iloscdanych");
                $query -> bindValue(':iloscdanych', $limit, PDO::PARAM_INT);
                $query -> bindValue(':id', $_GET['id'], PDO::PARAM_INT);
                $query -> execute();
                $dane = $query -> fetchAll(PDO::FETCH_ASSOC);
                $jsonDanych = json_encode($dane);
                
                // Przekazanie danych do JS
                echo "<script> let dane = " . $jsonDanych . "</script>";
            ?>
        <h2><?php echo $nazwaCzytnika; ?></h2>
        <canvas id="wykres" class="w-100" ></canvas>

        <!-- JS -->
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
        <script>
            // Stworzenie tabel dla danych
            var daty = [];
            var temperatury = [];
            var wilgotnosci = [];

		dane = dane.reverse();
            
            // Przewertowanie danych
            dane.forEach(function (element) {
                daty.push(element.data);
                temperatury.push(element.temperatura);
                wilgotnosci.push(element.wilgotnosc);
            });

            // Genergowanie wykresu poprzez Chart.js
            var ctx = $("#wykres")[0].getContext('2d');
            ctx.clearRect(0, 0, $("#wykres").width(), $("#wykres").height());
            var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: daty, // Ustawiamy daty jako etykiety osi X
                    datasets: [
                    {
                        label: 'Temperatura (°C)',
                        data: temperatury, // Ustawiamy dane temperatury
                        borderColor: 'rgba(255, 99, 132, 1)', // Czerwony kolor linii
                        backgroundColor: 'rgba(255, 99, 132, 0.2)', // Przezroczyste tło
                        borderWidth: 1,
                        fill: true // Wypełnienie pod linią
                    },
                    {
                        label: 'Wilgotność (%)',
                        data: wilgotnosci, // Ustawiamy dane wilgotności
                        borderColor: 'rgba(54, 162, 235, 1)', // Niebieski kolor linii
                        backgroundColor: 'rgba(54, 162, 235, 0.2)', // Przezroczyste tło
                        borderWidth: 1,
                        fill: true // Wypełnienie pod linią
                    }
                    ]
                },
                options: {
                    responsive: true,
                    plugins: 
                    {
                        legend: 
                        {
                            labels: 
                            {
                                color: 'black' // Kolor tekstu w legendzie
                            }
                        },
                        title: 
                        {
                            display: true,
                            text: 'Wykres Temperatury i Wilgotnośćci',
                            color: 'black' // Kolor tekstu tytułu
                        }
                    },
                    scales: {
                        x: 
                        {
                            ticks: 
                            {
                                color: 'black' // Kolor tekstu na osi X
                            }
                        },
                        y: 
                        {
                            ticks: 
                            {
                                color: 'black' // Kolor tekstu na osi Y dla pierwszego zestawu danych
                            }
                        }
                    }
                }

            });
        </script>

    </body>
</html>

Baza danych

Po całej konfiguracji musimy jeszcze utworzyć gotową bazę danych która będzie przechowywać nasze dane.

Tworzymy tabelę dane z czujników.

CREATE TABLE `danezczujnikow` (
  `ID` bigint(20) NOT NULL,
  `IDCzujnikow` bigint(20) NOT NULL,
  `temperatura` float NOT NULL,
  `wilgotnosc` float NOT NULL,
  `data` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

Tworzymy tabelę lista czujników

CREATE TABLE `listaczujnikow` (
  `ID` bigint(20) NOT NULL,
  `IP` text NOT NULL,
  `nazwa` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

Indeksy dla tabeli danezczujnikow

ALTER TABLE `danezczujnikow`
  ADD PRIMARY KEY (`ID`),
  ADD KEY `IDCzujnikow` (`IDCzujnikow`);

Indeksy dla tabeli listaczujnikow

ALTER TABLE `listaczujnikow`
  ADD PRIMARY KEY (`ID`);

Ustawiamy autoincrement

ALTER TABLE `danezczujnikow`
  MODIFY `ID` bigint(20) NOT NULL AUTO_INCREMENT;
ALTER TABLE `listaczujnikow`
  MODIFY `ID` bigint(20) NOT NULL AUTO_INCREMENT;

ESP8266 + DHT22

Zacznijmy od schematu jak powinniśmy wszystko podłączyć.

Monitorowanie Temperatury w Serwerowni

Czas na instalację, tutaj polecam Visual Studio Code

Po instalacji musimy doinstalować rozszerzenie które nazywa się

PlatformIO IDE – tak jak na obrazku poniżej.

monitorowanie-temperatury-w-serwerowni

Następnie instalujemy biblioteki

ESPAsyncWebServer-esphome

Monitorowanie Temperatury w Serwerowni

DHT22 sensor

Monitorowanie Temperatury w Serwerowni

Monitorowanie Temperatury w Serwerowni gotowy kod do wgrania na ESP8266+DHT22

Jak już wszystko udało nam się skonfigurować, czas wgrać nasz kod na urządzenie i sprawdzić czy wszystko działa poprawnie.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>

#define DHTPIN D7 // Zdefiniowanie wejścia danych z DHT
#define DHTTYPE DHT22 // Zdefiniowanie modelu DHT

DHT_Unified dht(DHTPIN, DHTTYPE); // Zdefiniowanie obiektu DHT


const char* ssid = "SSID"; // Nazwa sieci WiFi
const char* password = "Hasło"; // Hasło do sieci WiFi
const char* newHostname = "ESP_CZUJNIK_NAP"; // Hostname

// Zdefiniowanie serwera na ESP oraz jego portu
ESP8266WebServer server(80);

void setup() { // Funkcja bootowa kontrolera
  // Inicjalizacja komunikacji szeregowej oraz jej prędkości
  Serial.begin(9600);

  // Inicjalizacja czujnika DHT
  dht.begin();

  // Set custom hostname
  WiFi.hostname(newHostname);

  // Połączenie z wcześniej zdefiniowaną siecią WiFi
  WiFi.begin(ssid, password);

  // Czekanie na połączenie z WiFi
  while (WiFi.status() != WL_CONNECTED) {
    Serial.println("Łączenie z WiFi...");
  }

  Serial.println("Połączono z WiFi");
  Serial.println("Twoje IP");
  Serial.println(WiFi.localIP());
  Serial.println("--------------------");

  // Definiowanie endpointu /dane, który zwróci dane w formacie JSON
  server.on("/dane", HTTP_GET, []() {

    // Odczytaj temperaturę i wilgotność
    // Pobranie eventu
    sensors_event_t event;
    // Pobranie temperatury
    dht.temperature().getEvent(&event);
    float temperature = event.temperature;
    // Pobranie wilgotności
    dht.humidity().getEvent(&event);
    float humidity = event.relative_humidity;

    // Tworzenie JSON
    String jsonData = "{\"temperature\": " + String(temperature) + ", \"humidity\": " + String(humidity) + "}";

    // Wysłanie JSON jako odpowiedź na zapytanie
    server.send(200, "application/json", jsonData);
  });

  // Tworzenie serwera
  server.begin();
}

void loop() {
  // Nasłuch połączeń
  server.handleClient();
}

Uwagi

Monitorowanie Temperatury w Serwerowni

Ważne jest aby po wgraniu na urządzenie kodu sprawdzić czy wszystko działa poprawnie. Jeżeli mamy możliwość podejrzenia na routerze jakie urządzenie się podłączyło do naszej sieci. Nie powinno to byś też trudne, ponieważ w linijce:

const char* newHostname = "ESP_CZUJNIK_NAP";

Ustawmy taki hostname aby był on powiązany z lokalizacją naszego czujnika.

Warto również przypisać na routerze stały adres IP dla czujnika, ponieważ jeżeli tego nie zrobimy a adres się zmieni to automatycznie ten adres również będziemy musieli zmienić w bazie danych a a dokładnie w tabeli listaczujnikow.

Sprawdzenie podstawowych funkcjonalności

Aby sprawdzić czy czujnik działa i prawidłowo, wystarczy że wejdziesz na stronę:

http://ip_esp8622/dane

Aby sprawdzić czy cron oraz reszta czujników które dodaliśmy działają prawidłowo działa prawidłowo wystarczy wpisać

http://ip_serwer_www/cron.php