Stacja Pogodowa – aplikacja po stronie serwera

Witajcie moi czytelnicy. Dzisiaj chciałbym się z Wami podzielić kolejną częścią mojego projektu inteligentnego domu. Tym razem zaprezentuję Wam moją aplikację do przechwytywania danych ze Stacji Pogodowych i przetwarzania ich. Ale po kolei…

Tak jak wspomniałem już w artykule Stacja pogodowa – v3.0.0 moje rozwiązanie współpracuje z systemem Home Assistant jak i z moim autorskim rozwiązaniem, które umożliwia przechowywanie danych, przetwarzanie ich, porównywanie jak i może robić wszystko to co sobie wymyślimy, kwestia tylko dobudowania odpowiednich funkcjonalności.

Całość rozwiązania została oparta na popularnym frameworku NestJS, który ma prostą modułową strukturę i pozwala w prosty i szybki sposób uzyskać potrzebne rozwiązanie. Przy tym, dla osób takich jak ja na co dzień piszących w Angular, konstrukcja modułów, serwisów, kontrolerów jest bardzo podobna do tych znanych właśnie z tego frameworka.

Pliki źródłowe projektu są dostępne na Github.

Jednak zacznijmy od początku.

Baza danych

Wszystkie dane są przechowywane w Bazie Danych jest ich na tyle dużo, że próba używania plików albo innego rodzaju nośnika byłaby tu nieefektywna. W moim projekcie używam bazy MySQL (w odróżnieniu od domyślnych ustawień Home Assistant, gdzie używana jest baza NoSQL).

Baza danych składa się z trzech tabel:

  • entity – przechowuje listę encji znajdujących się obiektów w systemie (różnych obiektów, bo nie wykluczam, że w przyszłości system może obsługiwać inne urządzenia)
  • weather_station – przechowuje informacje o stacjach pogody
  • weather_station_data – przechowuje dane o stanie sensorów temperatury i wilgotności z danego pomiaru czasu

Entity

CREATE TABLE `entity` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '',
  `ip` varchar(15) NOT NULL,
  `host` varchar(255) DEFAULT NULL,
  `topic` varchar(255) DEFAULT NULL,
  `topicSensorFull` varchar(255) DEFAULT NULL,
  `uniqId` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `unqi_id_idx` (`uniqId`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

Tabela ta przechowuje:

  • name – nazwa urządzenia
  • ip – obecny adres IP
  • uniqId – unikalny identyfikator każdego urządzenia

Pozostałe pola są jeszcze wykorzystywane przez moje starsze stacje pogodowe, które działają na oprogramowaniu Tasmota i do pełnej integracji wszystkich systemów potrzebne było przechowywanie tych danych. W niedalekiej przyszłości, gdy wszystkie stacje pogodowe będą działały na moim oprogramowaniu, to te niepotrzebne pola znikną ze schematu.

Weather Station

CREATE TABLE `weather_station` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '',
  `lastDataId` int(11) DEFAULT NULL,
  `entityId` int(11) DEFAULT NULL,
  `sensor` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `REL_01e6c0501a3c932ddb7a91f161` (`lastDataId`),
  KEY `entity_idx` (`entityId`,`sensor`),
  CONSTRAINT `FK_01e6c0501a3c932ddb7a91f1618` FOREIGN KEY (`lastDataId`) REFERENCES `weather_station_data` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

Tabela ta przechowuje:

  • name – nazwa stacji pogodowej, w obrębie jednego urządzenia (Entity) mogą być 2 stacje pogodowe
  • lastDataId – wskazanie na ostatni pomiar ze stacji pogodowej
  • sensor – oznaczenie stacji pogodowej w obrębie urządzenia: 0 (dla pierwszej stacji pogodowej) lub 1 (jeśli jest to druga stacja pogodowa)

Weather Station Data

CREATE TABLE `weather_station_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `temperature` double(4,2) NOT NULL,
  `humidity` double(4,2) NOT NULL,
  `weatherStationId` int(11) DEFAULT NULL,
  `timestamp` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `timestamp_station` (`timestamp`,`weatherStationId`),
  KEY `FK_259d134eda7bdc7f07f72267475` (`weatherStationId`),
  CONSTRAINT `FK_259d134eda7bdc7f07f72267475` FOREIGN KEY (`weatherStationId`) REFERENCES `weather_station` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=40824 DEFAULT CHARSET=utf8;

Ostatnia tabela przechowuje odczyty danych z poszczególnych stacji pogodowych:

  • temperature – temperatura w oC
  • humidity – wilgotność w %
  • timestamp – wartość liczbowa czasu w którym dokonany został pomiar
  • weatherStationId – wskazanie na stację pogodową

Myślę, że ogólny zarys struktury przechowywania danych jest zrozumiały zatem przejdziemy do mechanizmu zapisywania danych.

MQTT – zbieranie danych

Tak jak pisałem w artykule Stacja pogodowa – v3.0.0, moje stacje pogodowe mogą komunikować się z serwerem na 2 sposoby: za pomocą protokołu HTTP lub MQTT. Oczywiście z wielu aspektów ten drugi jest wygodniejszy i szybszy, toteż skupię się tylko na tym aspekcie, gdyż cały projekt zamierzam rozwijać właśnie w tym kierunku.

Mechanizm zbierania danych składa się z nasłuchiwania na dwa zdarzenia:

  • przedstawienie się urządzenia
  • przesłanie danych do zapisania

Przedstawienie się urządzenia

Po to budujemy systemy informatyczne by automatyzować pewne powtarzające się procesy jak tylko się da. Tak by nie musieć nic konfigurować, a jedynie dostawiać nowe urządzenia i oczekiwać, że wszystko zadziała samo jak za dotknięciem czarodziejskiej różdżki.

Właśnie na taki krok zdecydowałem się i ja. Jak wiecie z porzednich artykułów moje Stacje Pogodowe wysyłają na topicu ws/XXXXXX_Y/INFO podstawowe informacje o sobie.

{"ip":"192.168.1.2","uniqId":"999999","sensors":[{"symbol":0,"name":"11"},{"symbol":1,"name":"22"}]}

Dzięki takiemu rozwiązaniu mogę sprawdzić czy posiadam w swojej bazie urządzenie (Entity) o takim uniqId i mogę zaktualizować jego dane lub utworzyć je od nowa. To podejście nie zmusza mnie do nadawania statycznych IP dla moich stacji pogodowych, gdyż za każdym razem gdy ono się wybudza z DeepSleep to wysyła powyższe informacje, dzięki czemu wszystko się aktualizuje na bieżąco. W tym przypadku wszystkie dane ustawiane są domyślnie i można je później edytować w bazie danych (niestety jeszcze nie przez żaden interfejs API, myślę, że to niedługo się zmieni).

Warto nadmienić w tym, miejscu, że takiej funkcjonalności nie posiadają stacje pogodowe pracujące z wykorzystaniem protokołu HTTP.

Zapisanie danych

Kolejnym zdarzeniem jest nasłuchiwanie na pojawienie się danych z poszczególnych stacji pogodowych ws/XXXXXX_Y/SENSOR.

{"uniqId":"08A3A9","sensor":0,"payload": {"time":1596545956,"temp":"24.10","hum":"60.00"}}

Zadaniem tego mechanizmu jest zapisanie danych. Dodatkowo, jeśli dane nie są archiwalne, to ustawienie ostatniego znanego pomiaru jako bieżącej wartości.

API serwer

Ostatnią częścią mojego serwera do przechowywania stacji pogodowych jest API Server, którego zadaniem jest serwowanie zebranych danych dla systemów trzecich.

Do dyspozycji mamy następujące zapytania:

  • /api/weather-stations (GET) – zwraca listę wszystkich stacji pogodowych wraz z ostatnim znanym pomiarem
  • /api/weather-stations/:ID/data?from=FROM_TIMESTAM&to=TO_TIMESTAM (GET) – zwraca średnią wartość pomiaru dla każdego dnia dla danej stacji pogodowej na zadanym przedziale czasu (FROM_TIMESTAMP i TO_TIMESTAMP to wartości timestamp w milisecundach)
  • /api/weather-stations/:ID/data/month?from=YEAR&month=MONTH – zwraca średnią wartość pomiaru dla każdego dnia podanego roku i miesiąca dla danej stacji pogodowej (miesiąc zaczynamy numerować od 0)
  • /api/weather-stations/:ID/data/year?from=YEAR – zwraca średnią wartość pomiaru dla każdego miesiąca podanego roku dla danej stacji pogodowej
  • /api/weather-stations/:ID/data/week?from=YEAR&month=MONTH&day=DAY – zwraca średnią wartość pomiaru dla każdego dnia tygodnia począwszy od dnia YEAR-MONTH-DAY dla danej stacji pogodowej (miesiąc zaczynamy numerować od 0)
  • /api/weather-stations/:ID/data/day?from=YEAR&month=MONTH&day=DAY – zwraca średnią wartość pomiaru dla każdej godziny w dniu YEAR-MONTH-DAY dla danej stacji pogodowej (miesiąc zaczynamy numerować od 0)

Istnieje jeszcze jeden request, który pozwala na zapisanie danych stacji pogodowej (odczytu z sensora), o ile używa ona protokołu HTTP.

  • /api/weather-stations/sync (POST) – zapisuje przesłane dane, należy jako content przesłać JSON’a
{ 
  "ip": "10.10.10.11", 
  "sensor": "0", 
  "data": [
    {
      "time": timestamp_in_seconds,
      "temp": "24.14",
      "hum": "80.92"
    },
    ...
  ]
}

System na podstawie adresu IP i parametru sensor będzie wiedział do której stacji pogodowej należy dopisać przesłane dane.

Statyczne pliki

Dodatkową funkcjonalnością API Serwera jest to, że pozwala on serwować statyczne pliki.

//main.ts
...
async function bootstrap() {
  ...
  app.useStaticAssets(join(__dirname, 'app'));

  ...
}

bootstrap();

Dzięki tej jednej linijce system serwuje statyczne pliki z katalogu app jak z root’a. Może to rozwiązanie posłużyć do wielu celów, mi jednak służy do tego by skompilować Angular’ową aplikację do plików wynikowych i umieścić w tym folderze. Powoduje to, że uruchamiając ten serwer mam dwie ścieżki:

  • /serwuje statyczne pliki (uruchamia stronę, która korzysta z poniższego api)
  • /api – wywołanie rożnych usług backendowych

Podsumowanie

To już właściwie wszystko, mógłbym zagłębić się bardziej w kod całej aplikacji, ale nie chcę zaciemniać ogólnego zarysu i odpowiedzialności poszczególnych jego części. Każdy pewnie zapyta, jak ja to uruchamiam – odpowiedź jest prosta – na Docker chodzącym na Raspberry Pi 4. Jak to skonfigurowałem? Jak wygląda aplikacja frontendowa? O tym wszystkim postaram się napisać już w niedalekiej przyszłości, gdyż są to tematy na kolejne artykuły.

Stacja Pogodowa – aplikacja po stronie serwera
Przewiń na górę