🎉 Initial commit

This commit is contained in:
Markus Benjamin Tabler 2026-06-26 19:48:04 +02:00
commit 392c495944
68 changed files with 18997 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
secrets.h
.env

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Benji
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

120
README.md Normal file
View file

@ -0,0 +1,120 @@
# ClimaCore
by BenjiG70
## Overview
This repo is build for a weatherstation powered by a WEMOS D1 Mini or ESP32.
For web- and database server we use a Raspberry Pi. Here you can get 3D-Models of Serverracks for this.
## Version logs
| Version | Description | Date|
|--------:|------------:|----:|
|1.0| basic functionality (webserver with charts and database connectivity); Arduino / ESP8266 tesscript | 2024.11.07|
|2.0 | completely reworked ui/ux, databasefunctions and backend | 2025.06.23|
## Getting started
### Install Nginx
`sudo apt update`
`sudo apt install nginx`
start nginx server
`sudo systemctl start nginx`
autostart nginx server on boot
`sudo systemctl enable nginx`
check status
`sudo systemctl status nginx`
Output shall be:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled)
Active: active (running)
To restart nginx:
`sudo systemctl restart nginx`
Test configurationfile
`sudo nginx -t`
if the firewall hit problems
`sudo ufw allow 2292`
### Setup Nginx Server
Nginx configuration File (nginx.conf)
```
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
include mime.types;
server {
listen 2292; #define port of webserver
root /var/www/html/dist/web/browser;
# Optionally, define an index file (like index.html)
index index.html;
# Add additional configuration, like handling 404 errors if necessary
location / {
try_files $uri $uri/ /index.html;
}
}
}
```
copy angular build file to nginx location
`cp -r [path_to_dist_folder] var/www/html/`
Install all needed librarys for the database
`sudo npm install sqlite3 cors express`
test database
`node path/to/your/database/folder/database.js`
### Setup automatic start of the DB server after boot
build a file for the database service
`sudo nano /etc/systemd/system/dbserver.service`
write your paths and user into the file
```
[Unit]
Description=Start Database Server via JavaScript
After=network.target
[Service]
ExecStart=path/to/your/node/installation path/to/your/database/folder/database.js
WorkingDirectory=/path/to/your/database/folder
Restart=always
User=your_user
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
```
enable dbserver.service
`sudo systemctl enable dbserver.service`
check if enabling is succeed
`systemctl is-enabled dbserver.service`
output shall be "enabled"
reboot raspy
`sudo reboot`
check status of db server
`sudo systemctl status dbserver.service`
## wiring the esp32
![](./src/esp32/Steckplatine_Climacore.png)

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "WeatherGuardian",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

102
src/esp32/esp32.ino Normal file
View file

@ -0,0 +1,102 @@
// zu installierende Bibliotheken:
// DHT sensor library by Adafruit
// ESP32-Tutorial: https://randomnerdtutorials.com/installing-the-esp32-board-in-arduino-ide-windows-instructions/
#include "secrets.h"
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <WiFi.h>
#include <HTTPClient.h>
#define DHTPIN 25
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
// define the db server -> keep the secrets.h in mind
const char* baseURL = DATABASE_SERVER;
// definition of the sensor
const char* sensorName = "Uniriese";
// definition of the led
const int led = 26;
void setup() {
Serial.begin(115200);
// starte den Sensor
dht.begin();
// start networkconnection
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
// initialize LED(s)
pinMode(led, OUTPUT);
}
void loop() {
digitalWrite(led, HIGH);
delay(1000);
digitalWrite(led, LOW);
/**
* connect the esp32 with teh network
* then pull the temperature and humidity data and put both togheter with the sensorname into a string.
* send this json type string to the database server and catch the answer.
* write the answer and data in the serial output.
* sleep for 5 minutes (900.000 ms)
*/
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
WiFiClient wifiClient;
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
Serial.println("Fehler beim Auslesen des Sensors");
return;
}
String jsonPayload = "{ \"sensor\": \"" + String(sensorName) + "\"" + ", \"temperature\": " + String(temperature) + ", \"humidity\": " + String(humidity) + ", \"air_pressure\": " + NULL + " }";
String url = String(baseURL) + "/insert/data/";
http.begin(wifiClient, url);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(jsonPayload);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("HTTP-Antwort:");
Serial.println(response);
} else {
Serial.print("Fehler bei der Anfrage: ");
Serial.println(httpResponseCode);
}
http.end();
//optional
Serial.println("------------------------------------");
Serial.println("Auslesen erfolgreich!");
Serial.print("Temperatur: ");
Serial.print(temperature);
Serial.println(" °C ");
Serial.print("Luftfeuchte: ");
Serial.print(humidity);
Serial.println(" % ");
Serial.println("Ausgabe erfolgreich!");
Serial.println("------------------------------------");
}
delay(900000);
}

3
src/secrets.h.example Normal file
View file

@ -0,0 +1,3 @@
#define WIFI_SSID "<ssid>"
#define WIFI_PASSWORD "<passwort>"
#define DATABASE_SERVER "127.0.0.1"

View file

@ -0,0 +1,116 @@
#include <Adafruit_NeoPixel.h>
#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
// WLAN-Zugangsdaten
const char* ssid = "SSID"; // Name/SSID des WLANs
const char* password = "PASSWORT"; // Passwort des WLANs
// URL der Website
const char* baseURL = "http://127.0.0.1"; // IP-Adresse
// Erstelle ein WiFiClient-Objekt
WiFiClient wifiClient;
// Pin-Definitionen
#define LEDPIN D1 //LED PIN
#define RST_PIN D3 // Reset-Pin (GPIO0)
#define SS_PIN D8 // SDA-Pin (GPIO15)
#define LEDCOUNT 8
Adafruit_Neopixel pixels(LEDCOUNT, LEDPIN, NEO_GRB + NEO_KHZ8000);
MFRC522 rfid(SS_PIN, RST_PIN); // Initialisiere MFRC522-Objekt
void setup() {
// Serielle Konsole starten
Serial.begin(115200);
while (!Serial);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
// SPI starten (Pins sind hardwaremäßig vorgegeben)
SPI.begin();
rfid.PCD_Init();
pixels.begin();
}
void loop() {
if (WiFi.status() == WL_CONNECTED) { // Prüfen, ob WLAN verbunden ist
HTTPClient http;
pixels.clear();
// Überprüfen, ob eine neue Karte in Reichweite ist
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
delay(50);
return;
}
updateLoadingBar();
// UID auslesen und auf der seriellen Konsole anzeigen
uid = "{\"uid\":" + String(rfidReader0.uid.uidByte[0]) + " " + String(rfidReader0.uid.uidByte[1]) + " " + String(rfidReader0.uid.uidByte[2]) + " " + String(rfidReader0.uid.uidByte[3]) +"}";
String url = String(baseURL) + "/check/uid";
http.begin(wifiClient, url); // Verbindung zur URL herstellen
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.GET(); // GET-Anfrage senden
if (httpResponseCode > 0) {
// Antwort erfolgreich empfangen
String response = http.getString(); // Antwort als String lesen
Serial.println("HTTP-Antwort:");
Serial.println(response);
} else {
Serial.print("Fehler bei der Anfrage: ");
Serial.println(httpResponseCode);
}
http.end(); // Verbindung schließen
} else {
}
// Gerät "freigeben" für den nächsten Lesevorgang
rfid.PICC_HaltA();
}
// Funktion zur Steuerung der LED-Ladebalken-Animation
void updateLoadingBar() {
unsigned long now = millis();
// Aktualisierung nur nach Ablauf der Animation-Delay-Zeit
if (now - lastUpdate >= animationDelay) {
lastUpdate = now;
// Aktuelle LED an und alle anderen aus
strip.clear();
strip.setPixelColor(currentLED, strip.Color(0, 0, 255)); // Blau
strip.show();
// Ladebalken-Animation vorwärts oder rückwärts
if (forward) {
currentLED++;
if (currentLED >= NUM_LEDS) {
currentLED = NUM_LEDS - 1;
forward = false;
}
} else {
currentLED--;
if (currentLED < 0) {
currentLED = 0;
forward = true;
}
}
}
}

16
web/.editorconfig Normal file
View file

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

45
web/.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
#/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
# Database files
*.sqlite

4
web/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
web/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
web/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

27
web/README.md Normal file
View file

@ -0,0 +1,27 @@
# Web
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.1.1.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

109
web/angular.json Normal file
View file

@ -0,0 +1,109 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"web": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss",
"standalone": false
},
"@schematics/angular:directive": {
"standalone": false
},
"@schematics/angular:pipe": {
"standalone": false
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/web",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "3MB",
"maximumError": "6MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "20kB",
"maximumError": "30kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "web:build:production"
},
"development": {
"buildTarget": "web:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}

13
web/dbserver.service Normal file
View file

@ -0,0 +1,13 @@
[Unit]
Description=Start Database Server via JavaScript
After=network.target
[Service]
ExecStart=/home/benji/.nvm/versions/node/v22.9.0/bin/node /home/benji/git/WeatherGuardian/web/src/app/server/database.js
WorkingDirectory=/home/benji/git/ClimaCore/web/src/app/server/
Restart=always
User=benji
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

466
web/dist/web/3rdpartylicenses.txt vendored Normal file
View file

@ -0,0 +1,466 @@
--------------------------------------------------------------------------------
Package: @angular/core
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: rxjs
License: "Apache-2.0"
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--------------------------------------------------------------------------------
Package: tslib
License: "0BSD"
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/common
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/platform-browser
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/animations
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/cdk
License: "MIT"
The MIT License
Copyright (c) 2024 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: @angular/router
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: @kurkle/color
License: "MIT"
The MIT License (MIT)
Copyright (c) 2018-2024 Jukka Kurkela
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
Package: chart.js
License: "MIT"
The MIT License (MIT)
Copyright (c) 2014-2024 Chart.js Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
Package: primeng
License: "MIT"
--------------------------------------------------------------------------------
Package: @angular/material
License: "MIT"
The MIT License
Copyright (c) 2024 Google LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Package: zone.js
License: "MIT"
The MIT License
Copyright (c) 2010-2024 Google LLC. https://angular.io/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------

BIN
web/dist/web/browser/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

13
web/dist/web/browser/index.html vendored Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en" data-critters-container>
<head>
<meta charset="utf-8">
<title>Web</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>body{background-color:#b3daee;margin:0;font-family:Open Sans,sans-serif}</style><link rel="stylesheet" href="styles-OEHUR5AR.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-OEHUR5AR.css"></noscript></head>
<body>
<app-root></app-root>
<script src="polyfills-FFHMD2TL.js" type="module"></script><script src="main-OCIY5XNU.js" type="module"></script></body>
</html>

10
web/dist/web/browser/main-OCIY5XNU.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
body{background-color:#b3daee;margin:0;font-family:Open Sans,sans-serif}

15106
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

47
web/package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "web",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.1.0",
"@angular/common": "^18.1.0",
"@angular/compiler": "^18.1.0",
"@angular/core": "^18.1.0",
"@angular/forms": "^18.1.0",
"@angular/material": "^18.2.6",
"@angular/platform-browser": "^18.1.0",
"@angular/platform-browser-dynamic": "^18.1.0",
"@angular/router": "^18.2.6",
"@primeng/themes": "^19.1.3",
"apexcharts": "^4.7.0",
"chart.js": "^4.5.0",
"dotenv": "^16.5.0",
"primeicons": "^7.0.0",
"primeng": "^17.18.15",
"rxjs": "~7.8.0",
"sqlite3": "^5.1.7",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.1.1",
"@angular/cli": "^18.1.1",
"@angular/compiler-cli": "^18.1.0",
"@types/jasmine": "~5.1.0",
"@types/jquery": "^3.5.32",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.5.2"
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LandingComponent } from './components/landing/landing.component';
import { StatComponentComponent } from './components/stat-component/stat-component.component';
import { ErrorComponent } from './components/error/error.component';
const routes: Routes = [
{path: '', component:LandingComponent},
{path: 'home', component:LandingComponent},
{path: 'stats', component:StatComponentComponent},
{path: 'stats/:sensor', component:StatComponentComponent},
{path: '**', component: ErrorComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<router-outlet></router-outlet>
</body>
</html>

View file

View file

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterModule.forRoot([])
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'web'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('web');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, web');
});
});

View file

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = '';
}

41
web/src/app/app.module.ts Normal file
View file

@ -0,0 +1,41 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LandingComponent } from './components/landing/landing.component';
import { WeathercardsComponent } from './components/weathercards/weathercards.component';
import { DetailsComponent } from './components/details/details.component';
import { HttpClientModule } from '@angular/common/http';
import { ChartModule } from 'primeng/chart';
import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import { StatComponentComponent } from './components/stat-component/stat-component.component';
import { ErrorComponent } from './components/error/error.component';
@NgModule({
declarations: [
AppComponent,
LandingComponent,
WeathercardsComponent,
DetailsComponent,
StatComponentComponent,
ErrorComponent,
],
imports: [
BrowserAnimationsModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
ChartModule,
DragDropModule
],
providers: [
{provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: {hasBackdrop: false}}
],
bootstrap: [AppComponent],
exports: [],
})
export class AppModule { }

View file

@ -0,0 +1,7 @@
{{sensor}}
<div class="card">
<div class="chart">
<p-chart [type]="style" [data]="src" [options]="options" class="h-[30rem]"></p-chart>
</div>
</div>

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DetailsComponent } from './details.component';
describe('DetailsComponent', () => {
let component: DetailsComponent;
let fixture: ComponentFixture<DetailsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DetailsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,124 @@
import { Component, Input} from '@angular/core';
/**
* Represents a single dataset used in a chart.
*/
interface StatData {
/**
* The label for the dataset (e.g. "Temperature", "Humidity").
* This will typically appear in the chart legend and tooltips.
*/
label: string;
/**
* An array of numerical values corresponding to the dataset.
* Each value represents a point on the chart.
*/
data: number[];
}
@Component({
selector: 'app-details',
templateUrl: './details.component.html',
styleUrl: './details.component.scss',
})
export class DetailsComponent {
/**
* The sensor object associated with the chart.
* Can contain metadata or configuration related to the data source.
*/
@Input() sensor: any;
/**
* The data to be displayed in the chart.
* Can be an array of strings (e.g., labels or values) or an array of `StatData` objects.
*/
@Input() data: string[] | StatData[] = [];
/**
* The type of chart to render.
* Supports standard chart types like 'bar', 'line', 'scatter', 'bubble', 'pie', 'doughnut', 'polarArea', and 'radar'.
* Defaults to 'bar' if not specified.
*/
@Input() style?:
| 'bar'
| 'line'
| 'scatter'
| 'bubble'
| 'pie'
| 'doughnut'
| 'polarArea'
| 'radar';
/**
* Optional chart configuration options, passed directly to the charting library.
* Structure depends on the charting library used (e.g., Chart.js).
*/
@Input() options: any;
/**
* Labels corresponding to the data entries.
* Can be a simple array of strings or an array of `StatData` objects, depending on how the chart interprets labels.
*/
@Input() labels?: string[] | StatData[];
/**
* A hex or CSS color string used for chart styling.
* If not provided, a default color (`#424242`) is used.
*/
@Input() chartColor?: string;
src: any;
/**
* Initialize the chart
*/
ngOnInit() {
this.updateChart();
}
/**
* if something changes, the charts woll be updated
*/
ngOnChanges() {
this.updateChart();
}
/**
* Updates the chart configuration (`this.src`) with the latest data and style settings.
*
* - Sets chart labels to `this.labels` or an empty array if none are provided.
* - Uses `getColorArray()` to assign uniform background and border colors based on the data length.
* - Sets the border width to 1.
* - Determines whether the chart should be filled based on the `style` (`'line'` charts are not filled).
* - Sets the chart type based on `this.style`, defaulting to `'bar'`.
* - Assigns `this.data` to the datasets field for rendering.
*
* This method is typically called after data or style updates to refresh the chart display.
*/
updateChart() {
this.src = {
labels: this.labels || [],
backgroundColor: this.getColorArray(),
borderColor: this.getColorArray(),
borderWidth: 1,
fill: this.style === 'line' ? false : true,
type: this.style || 'bar',
datasets: this.data,
};
}
/**
* Returns an array of color values to be used in a chart.
* The length of the array matches the number of data points (`this.data.length`).
* If no data is available, the array will contain a single entry.
*
* Each entry in the array contains the same color, either defined by `this.chartColor`
* or falling back to the default color `#424242`.
*
* @returns {string[]} An array of color strings for use in chart visualizations.
*/
getColorArray(): string[] {
const length = this.data?.length || 1;
return Array(length).fill(this.chartColor || '#424242');
}
}

View file

@ -0,0 +1,145 @@
<div id="wrap">
<div id="wordsearch">
<ul>
<li>k</li>
<li>v</li>
<li>n</li>
<li>z</li>
<li>i</li>
<li class="one">4</li>
<li>m</li>
<li>e</li>
<li>t</li>
<li>a</li>
<li>x</li>
<li>l</li>
<li>x</li>
<li>y</li>
<li class="two">0</li>
<li>k</li>
<li>y</li>
<li>w</li>
<li>v</li>
<li>b</li>
<li>o</li>
<li>q</li>
<li>d</li>
<li class="three">4</li>
<li>p</li>
<li>a</li>
<li class="four">p</li>
<li class="five">a</li>
<li class="six">g</li>
<li class="seven">e</li>
<li>v</li>
<li>j</li>
<li>a</li>
<li class="eight">n</li>
<li class="nine">o</li>
<li class="ten">t</li>
<li>s</li>
<li>c</li>
<li>e</li>
<li>w</li>
<li>v</li>
<li>x</li>
<li>e</li>
<li>p</li>
<li>c</li>
<li>t</li>
<li>h</li>
<li>e</li>
<li>e</li>
<li class="eleven">f</li>
<li class="twelve">o</li>
<li class="thirteen">u</li>
<li class="fourteen">n</li>
<li class="fifteen">d</li>
<li>s</li>
<li>w</li>
<li>q</li>
<li>v</li>
<li>r</li>
<li>i</li>
<li>c</li>
<li>h</li>
<li>f</li>
<li>u</li>
</ul>
</div>
<div id="main-content">
<h1>Wir konnten nicht finden, wonach du suchst :/</h1>
<p> Leider konnte die von dir aufgerufene Seite nicht gefunden werden.
Sie ist temporär nicht verfügbar, umgezogen oder gelöscht.</p>
<div id="navigation">
<a class="navigation" href="">zurück zur Startseite</a>
</div>
</div>
</div>

View file

@ -0,0 +1,233 @@
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300);
$light_blue: #B3DAEE;
body {
background-color: #335B67;
background: -ms-radial-gradient(ellipse at center, #335B67 0%, #2C3E50 100%) fixed no-repeat;
background: -moz-radial-gradient(ellipse at center, #335B67 0%, #2C3E50 100%) fixed no-repeat;
background: -o-radial-gradient(ellipse at center, #335B67 0%, #2C3E50 100%) fixed no-repeat;
background: -webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, #335B67), color-stop(1, #2C3E50));
background: -webkit-radial-gradient(ellipse at center, #335B67 0%, #2C3E50 100%) fixed no-repeat;
background: radial-gradient(ellipse at center, #335B67 0%, #2C3E50 100%) fixed no-repeat;
font-family: 'Source Sans Pro', sans-serif;
-webkit-font-smoothing: antialiased;
margin: 0px;
}
::selection {
background-color: rgba(0,0,0,0.2);
}
::-moz-selection {
background-color: rgba(0,0,0,0.2);
}
a {
color: white;
text-decoration: none;
border-bottom: 1px solid rgba(255,255,255,0.5);
transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
-webkit-transition: all 0.5s ease;
margin-right: 10px;
}
a:last-child { margin-right: 0px; }
a:hover {
text-shadow: 0px 0px 1px $light_blue;
border-bottom: 1px solid rgba(255,255,255,1);
}
#noscript-warning {
width: 100%;
text-align: center;
background-color: rgba(0,0,0,0.2);
font-weight: 300;
color: white;
padding-top: 10px;
padding-bottom: 10px;
}
/* === WRAP === */
#wrap {
width: 80%;
max-width: 1400px;
margin:0 auto;
height: auto;
position: relative;
margin-top: 8%;
}
/* === MAIN TEXT CONTENT === */
#main-content {
float: right;
max-width: 45%;
color: white;
font-weight: 300;
font-size: 18px;
padding-bottom: 40px;
line-height: 28px;
}
#main-content h1 {
margin: 0px;
font-weight: 400;
font-size: 42px;
margin-bottom: 40px;
line-height: normal;
}
/* === NAVIGATION BUTTONS === */
#navigation { margin-top: 2%; }
#navigation a.navigation {
display: block;
float: left;
background-color: rgba(0,0,0,0.2);
padding-left: 15px;
padding-right: 15px;
color: white;
height: 41px;
line-height: 41px;
text-decoration: none;
font-size: 16px;
transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
-webkit-transition: all 0.5s ease;
margin-right: 2%;
margin-bottom: 2%;
border-bottom: none;
}
#navigation a.navigation i { line-height: 41px; }
#navigation a.navigation:hover {
background-color:$light_blue;
border-bottom: none;
}
/* === WORDSEARCH === */
#wordsearch {
width: 45%;
float: left;
}
#wordsearch ul {
margin: 0px;
padding: 0px;
}
#wordsearch ul li {
float: left;
width: 12%;
background-color: rgba(0,0,0,.2);
list-style: none;
margin-right: 0.5%;
margin-bottom: 0.5%;
padding: 0;
display: block;
text-align: center;
color: rgba(255,255,255,0.7);
text-transform: uppercase;
overflow: hidden;
font-size: 24px;
font-size: 1.6vw;
font-weight: 300;
transition: background-color 0.75s ease;
-moz-transition: background-color 0.75s ease;
-webkit-transition: background-color 0.75s ease;
-o-transition: background-color 0.75s ease;
}
#wordsearch ul li.selected {
background-color: $light_blue;
color: rgba(255,255,255,1);
font-weight: 400;
}
/* === SEARCH FORM === */
#search { margin-top: 30px; }
#search input[type='text'] {
width: 88%;
height: 41px;
padding-left: 15px;
padding-right: 15px;
box-sizing: border-box;
-moz-box-sizing: border-box;
background-color: rgba(0,0,0,0.2);
border: none;
outline: none;
font-size: 16px;
font-weight: 300;
color: white;
font-family: 'Source Sans Pro', sans-serif;
transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
-webkit-transition: all 0.5s ease;
border-radius: 0px;
}
#search .input-search {
width: 10%;
float: right;
height: 41px;
background-color: rgba(0,0,0,0.2);
outline: none;
border: none;
font-family: 'Source Sans Pro', sans-serif;
color: white;
font-weight: 300;
font-size: 16px;
cursor: pointer;
transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
-webkit-transition: all 0.5s ease;
text-align: center;
}
#search .input-search:hover {
background-color: rgba(26,188,156,0.7);
}
/* === RESPONSIVE CSS === */
@media all and (max-width: 899px) {
#wrap { width: 90%; }
}
@media all and (max-width: 799px) {
#wrap { width: 90%; height: auto; margin-top: 40px; top: 0%; }
#wordsearch { width: 90%; float: none; margin:0 auto; }
#wordsearch ul li { font-size: 4vw; }
#main-content { float: none; width: 90%; max-width: 90%; margin:0 auto; margin-top: 30px; text-align: justify; }
#main-content h1 { text-align: left; }
#search input[type='text'] { width: 84%; }
#search .input-search { width: 15%; }
}
@media all and (max-width: 499px) {
#main-content h1 { font-size: 28px; }
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorComponent } from './error.component';
describe('ErrorComponent', () => {
let component: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ErrorComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,109 @@
import {
Component,
AfterViewInit,
HostListener,
ElementRef,
ViewChildren,
QueryList
} from '@angular/core';
/**
* The ErrorComponent displays a visual error screen, styled as a grid-based word search.
* It dynamically adjusts the layout of grid tiles to maintain a uniform square shape,
* and sequentially highlights specific tiles with a CSS animation for user feedback.
*/
@Component({
selector: 'app-error',
templateUrl: './error.component.html',
styleUrl: './error.component.scss'
})
export class ErrorComponent implements AfterViewInit {
/**
* A QueryList containing references to all <li> elements marked with the `#tile` template reference.
* These tiles represent individual squares in the error word search grid.
*/
@ViewChildren('tile') tiles!: QueryList<ElementRef<HTMLLIElement>>;
/**
* Constructs the component instance and injects the native element reference.
* @param elRef A reference to the root DOM element of this component, used for querying children.
*/
constructor(private elRef: ElementRef) {}
/**
* Angular lifecycle hook that runs after the component's view and its children have been fully initialized.
* It ensures all tiles and the word search container have correct dimensions, and triggers
* the tile highlight animation.
*/
ngAfterViewInit(): void {
this.adjustLayout();
this.animateSelection();
}
/**
* HostListener that listens to browser window resize events.
* Automatically re-applies the layout logic to ensure the grid remains square and responsive.
*/
@HostListener('window:resize')
onResize(): void {
this.adjustLayout();
}
/**
* Adjusts the height and line-height of each tile to match its width, making them square.
* Also resizes the main `#wordsearch` container to be a square based on its width.
*
* This ensures a visually consistent layout across different screen sizes and prevents layout
* shifts caused by dynamic resizing or initial render timing differences.
*/
private adjustLayout(): void {
const tiles = this.tiles.toArray();
if (tiles.length > 0) {
// Use the first tile's width as the base for height and line-height
const width = tiles[0].nativeElement.offsetWidth + 'px';
tiles.forEach(tile => {
tile.nativeElement.style.height = width;
tile.nativeElement.style.lineHeight = width; // vertical centering
});
// Make the wordsearch container a square
const wordsearch = this.elRef.nativeElement.querySelector('#wordsearch') as HTMLElement;
if (wordsearch) {
const width = wordsearch.offsetWidth + 'px';
wordsearch.style.height = width;
}
}
}
/**
* Sequentially adds the CSS class `selected` to a predefined list of tiles.
* Each tile is targeted by a class name (`one`, `two`, ..., `fifteen`) and highlighted with a staggered delay.
*
* This animation simulates a scanning or selection effect on the error grid, enhancing user experience
* by drawing attention to the visual error pattern.
*
* - Delay starts at 1500ms after component view initialization.
* - Each subsequent tile is selected with an additional 500ms delay.
*/
private animateSelection(): void {
const classList = [
'one', 'two', 'three', 'four', 'five',
'six', 'seven', 'eight', 'nine', 'ten',
'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen'
];
let delay = 1500; // Initial delay before first tile selection
classList.forEach((className, index) => {
setTimeout(() => {
const el = this.elRef.nativeElement.querySelector(`.${className}`);
if (el) {
el.classList.add('selected'); // Trigger CSS animation
}
}, delay);
delay += 500; // Increase delay for next tile
});
}
}

View file

@ -0,0 +1,142 @@
<div class="body" id="overlay">
<div class="sensor-wrapper">
<button class="nav-button left" (click)="scrollLeft()"></button>
<div #scrollContainer class="sensor-container">
<div *ngFor="let sensor of sensorsData" class="sensor-data">
<app-weathercards class="card"
[sensor]="sensor.sensor"
[temperature]="sensor.temperature"
[humidity]="sensor.humidity"
[date]="sensor.DATE_TIME"
(click)="openDialog(sensor)">
</app-weathercards>
</div>
</div>
<button class="nav-button right" (click)="scrollRight()"></button>
</div>
<div class="details-container">
<div class="details-block">
<p>Temperatur der letzten 24 Stunden</p>
<app-details
*ngIf="dayLabel.length"
[labels]="dayLabel"
[data]="dailyTemp"
[style]="'line'"
>
</app-details>
</div>
<div class="details-block">
<p>Luftfeuchtigkeit der letzten 24 Stunden</p>
<app-details
*ngIf="dayLabel.length"
[labels]="dayLabel"
[data]="dailyHum"
[style]="'line'"
>
</app-details>
</div>
</div>
<div class="details-container">
<div class="details-block">
<p>Temperatur der letzten 7 Tage</p>
<app-details
*ngIf="weekLabel.length"
[labels]="weekLabel"
[data]="weeklyTemp"
[style]="'line'"
>
</app-details>
</div>
<div class="details-block">
<p>Luftfeuchtigkeit der letzten 7 Tage</p>
<app-details
*ngIf="weekLabel.length"
[labels]="weekLabel"
[data]="weeklyHum"
[style]="'line'"
>
</app-details>
</div>
</div>
<div class="details-container">
<div class="details-block">
<p>Temperatur des letzten Monats</p>
<app-details
*ngIf="monthLabel.length"
[labels]="monthLabel"
[data]="monthlyTemp"
[style]="'line'"
>
</app-details>
</div>
<div class="details-block">
<p>Luftfeuchtigkeit des letzten Monats</p>
<app-details
*ngIf="monthLabel.length"
[labels]="monthLabel"
[data]="monthlyHum"
[style]="'line'"
>
</app-details>
</div>
</div>
<div class="details-container">
<div class="details-block">
<p>Temperatur des letzten Jahres</p>
<app-details
*ngIf="yearLabel.length"
[labels]="yearLabel"
[data]="yearlyTemp"
[style]="'line'"
>
</app-details>
</div>
<div class="details-block">
<p>Luftfeuchtigkeit des letzten Jahres</p>
<app-details
*ngIf="yearLabel.length"
[labels]="yearLabel"
[data]="yearlyHum"
[style]="'line'"
>
</app-details>
</div>
</div>
<!-- loading container: https://uiverse.io/AnnixArt/wonderful-liger-82 -->
<div *ngIf="sensorsData.length === 0">
<div class="container">
<div class="coffee-header">
<div class="coffee-header__buttons coffee-header__button-one"></div>
<div class="coffee-header__buttons coffee-header__button-two"></div>
<div class="coffee-header__display"></div>
<div class="coffee-header__details"></div>
</div>
<div class="coffee-medium">
<div class="coffe-medium__exit"></div>
<div class="coffee-medium__arm"></div>
<div class="coffee-medium__liquid"></div>
<div class="coffee-medium__smoke coffee-medium__smoke-one"></div>
<div class="coffee-medium__smoke coffee-medium__smoke-two"></div>
<div class="coffee-medium__smoke coffee-medium__smoke-three"></div>
<div class="coffee-medium__smoke coffee-medium__smoke-for"></div>
<div class="coffee-medium__cup"></div>
</div>
<div class="coffee-footer"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,144 @@
$another_blue: #8cabbb;
p{
text-align: center;
}
/* Container für die Sensoren nebeneinander ausrichten */
.sensor-wrapper {
position: relative;
display: flex;
align-items: center;
}
.sensor-container {
display: flex;
overflow-x: hidden; /* Scrollbar wird versteckt */
scroll-behavior: smooth;
gap: 10px;
width: 100%;
}
.sensor-data {
flex: 0 0 calc(100% / 4); /* Desktop: 4 Karten sichtbar */
max-width: calc(100% / 4);
}
.card {
width: 100%;
}
.nav-button {
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
z-index: 1;
padding: 0 10px;
user-select: none;
}
.left {
margin-right: 10px;
}
.right {
margin-left: 10px;
}
.details-container {
display: flex;
gap: 20px;
justify-content: center;
align-items: flex-start;
flex-wrap: nowrap;
}
.details-block {
flex: 1 1 0;
min-width: 0;
padding: 10px;
}
.charts {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
align-items: center;
padding: 20px;
}
#yearly, #alltime {
display: flex;
justify-content: space-around;
width: 100%;
}
.container {
width: 300px;
height: 280px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Weitere Styles für Coffee bleiben unverändert ... */
/* --- MEDIA QUERIES FÜR SMARTPHONES --- */
@media screen and (max-width: 768px) {
.sensor-data {
flex: 0 0 90%; /* fast volle Breite auf kleineren Displays */
max-width: 90%;
}
.details-container {
flex-direction: column; /* Elemente untereinander */
align-items: center; /* zentrieren */
gap: 10px;
flex-wrap: wrap;
}
.charts {
padding: 10px;
}
#yearly, #alltime {
flex-direction: column;
align-items: center;
gap: 15px;
}
.container {
width: 90vw;
height: auto;
position: static;
transform: none;
margin: 20px auto;
}
.coffee-header,
.coffee-medium,
.coffee-footer {
position: relative;
}
.coffee-medium {
top: 0;
left: 0;
width: 100%;
}
.coffee-medium:before {
left: 0;
}
.coffee-header__buttons,
.coffee-header__display,
.coffee-header__details,
.coffee-medium__arm,
.coffee-medium__cup,
.coffee-footer {
transform: scale(0.8);
}
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LandingComponent } from './landing.component';
describe('LandingComponent', () => {
let component: LandingComponent;
let fixture: ComponentFixture<LandingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LandingComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LandingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,308 @@
/**
* LandingComponent
*
* This component acts as the landing page of the application. It displays the latest sensor data and statistical chart data
* (daily, weekly, monthly, and yearly) for multiple sensors. Data is refreshed automatically in intervals.
*
* Features:
* - Fetches and processes sensor data from a database.
* - Aggregates statistical data for various time periods.
* - Displays the latest values and trends.
* - Provides a scrollable view and navigation to detailed statistics.
*/
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DatabaseService } from '../../services/database.service';
import { Subscription, interval } from 'rxjs';
import { Router } from '@angular/router';
import { SensorService } from '../../services/sensor.service';
import { ChartPreperationService } from '../../services/chart-preperation.service';
/**
* Represents statistical data sets for each time period.
*/
interface StatsPeriod {
dailyStats: (string[] | StatData[]);
weeklyStats: (string[] | StatData[]);
monthlyStats: (string[] | StatData[]);
yearlyStats: (string[] | StatData[]);
}
/**
* Represents a single dataset used in a chart.
*/
interface StatData {
label: string;
data: number[];
}
@Component({
selector: 'app-landing',
templateUrl: './landing.component.html',
styleUrl: './landing.component.scss',
})
export class LandingComponent implements OnInit, OnDestroy {
sensorsData: any[] = [];
private sensors: string[] = [];
private updateSubscription?: Subscription;
private sensorDataforStats: any;
// Statistical chart data placeholders
statTempData: any;
statHumData: any;
statLabel: any;
statTempDataCurrentYear: any;
statHumDataCurrentYear: any;
statLabelCurrentYear: any;
statTempDataAllTime: any;
statHumDataAllTime: any;
statLabelAllTime: any;
statData: { [key: string]: StatsPeriod } = {};
dailyStats: string[] | StatData[] = [];
dayLabel: string[] | StatData[] = [];
weeklyStats: string[] | StatData[] = [];
weekLabel: string[] | StatData[] = [];
monthlyStats: string[] | StatData[] = [];
monthLabel: string[] | StatData[] = [];
yearlyStats: string[] | StatData[] = [];
yearLabel: string[] | StatData[] = [];
// Final chart data arrays grouped by period and type
dailyTemp: StatData[] = [];
dailyHum: StatData[] = [];
weeklyTemp: StatData[] = [];
weeklyHum: StatData[] = [];
monthlyTemp: StatData[] = [];
monthlyHum: StatData[] = [];
yearlyTemp: StatData[] = [];
yearlyHum: StatData[] = [];
@ViewChild('scrollContainer', { static: true }) scrollContainer!: ElementRef;
constructor(
private dbService: DatabaseService,
private router: Router,
private sensorService: SensorService,
private charts: ChartPreperationService
) {}
/**
* Angular lifecycle method that runs on component initialization.
* Starts sensor data loading and sets up the auto-refresh interval.
*/
async ngOnInit(): Promise<void> {
this.loadAllSensors();
this.startDataRefresh();
}
/**
* Fetches all sensor metadata and initializes data loading.
* Retries periodically if the connection fails.
*/
loadAllSensors(): void {
const reloadInterval = 5000; // 5 seconds
let retryInterval: any;
const fetchData = () => {
this.dbService.getAllData().subscribe(
async (data: unknown) => {
if (this.isSensorData(data)) {
if (retryInterval) {
clearInterval(retryInterval);
}
this.sensors = Object.values(data).map(sensorData => sensorData.sensor);
this.updateSensorsData();
}
},
(error: any) => {
this.sensorsData = [];
if (!retryInterval) {
retryInterval = setInterval(fetchData, reloadInterval);
}
}
);
};
fetchData();
}
/**
* Retrieves and prepares chart data for all sensors.
* Populates the data used in daily, weekly, monthly, and yearly statistics.
*/
async updateSensorsData(): Promise<void> {
this.dailyTemp = [];
this.dailyHum = [];
this.weeklyTemp = [];
this.weeklyHum = [];
this.monthlyTemp = [];
this.monthlyHum = [];
this.yearlyTemp = [];
this.yearlyHum = [];
try {
const uniqueSensors = [...new Set(this.sensors)];
const sensorDataPromises = uniqueSensors.map(sensor =>
this.dbService.getLastWeatherDataBySensor(sensor).toPromise()
);
const allData = await Promise.all(sensorDataPromises);
this.sensorsData = [];
for (const data of allData) {
if (this.isWeatherData(data)) {
const dataArray = Object.values(data);
const latestData = dataArray[0];
if (!this.sensorsData.some(d => d.sensor === latestData.sensor)) {
this.sensorsData.push(latestData);
}
} else {
console.error(`Unexpected data format:`, data);
}
}
for (const sens of uniqueSensors) {
[
this.dayLabel, this.dailyStats,
this.weekLabel, this.weeklyStats,
this.monthLabel, this.monthlyStats,
this.yearLabel, this.yearlyStats
] = await this.charts.loadData(sens);
if (!this.statData[sens]) {
this.statData[sens] = {} as any;
}
this.statData[sens].dailyStats = this.dailyStats;
this.statData[sens].weeklyStats = this.weeklyStats;
this.statData[sens].monthlyStats = this.monthlyStats;
this.statData[sens].yearlyStats = this.yearlyStats;
}
for (const key in this.statData) {
let newStat: StatData = {
label: key,
data: (this.statData[key].dailyStats as StatData[])[0].data
};
this.dailyTemp.push(newStat);
newStat = {
label: key,
data: (this.statData[key].dailyStats as StatData[])[1].data
};
this.dailyHum.push(newStat);
newStat = {
label: key,
data: (this.statData[key].weeklyStats as StatData[])[0].data
};
this.weeklyTemp.push(newStat);
newStat = {
label: key,
data: (this.statData[key].weeklyStats as StatData[])[1].data
};
this.weeklyHum.push(newStat);
newStat = {
label: key,
data: (this.statData[key].monthlyStats as StatData[])[0].data
};
this.monthlyTemp.push(newStat);
newStat = {
label: key,
data: (this.statData[key].monthlyStats as StatData[])[1].data
};
this.monthlyHum.push(newStat);
newStat = {
label: key,
data: (this.statData[key].yearlyStats as StatData[])[0].data
};
this.yearlyTemp.push(newStat);
newStat = {
label: key,
data: (this.statData[key].yearlyStats as StatData[])[1].data
};
this.yearlyHum.push(newStat);
}
} catch (error) {
console.error('Error updating sensors data:', error);
}
}
/**
* Starts a 60-second interval that continuously updates the sensor data.
*/
startDataRefresh(): void {
this.updateSubscription = interval(60000).subscribe(() => {
this.updateSensorsData();
});
}
/**
* Opens the statistics dialog for a specific sensor by navigating to the stats route.
* @param sensor The selected sensor to show detailed statistics for.
*/
openDialog(sensor: any): void {
this.sensorService.setSensor(sensor);
this.router.navigate(['/stats']);
}
/**
* Cleans up any active subscriptions when the component is destroyed.
*/
ngOnDestroy(): void {
if (this.updateSubscription) {
this.updateSubscription.unsubscribe();
}
}
/**
* Type guard to check whether fetched data is valid sensor data.
* @param data The data to check.
* @returns True if valid sensor data, otherwise false.
*/
private isSensorData(data: unknown): data is { [key: string]: { sensor: string } } {
return (
typeof data === 'object' &&
data !== null &&
Object.values(data).every((item) => 'sensor' in item)
);
}
/**
* Type guard to check whether fetched data is valid weather data.
* @param data The data to check.
* @returns True if valid weather data, otherwise false.
*/
private isWeatherData(data: unknown): data is { [key: string]: any } {
return (
typeof data === 'object' &&
data !== null &&
Object.values(data).every((item) => 'sensor' in item)
);
}
/**
* Scrolls the chart container to the right by the width of the container.
*/
scrollRight(): void {
const container = this.scrollContainer.nativeElement;
const scrollAmount = container.offsetWidth;
container.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
/**
* Scrolls the chart container to the left by the width of the container.
*/
scrollLeft(): void {
const container = this.scrollContainer.nativeElement;
const scrollAmount = container.offsetWidth;
container.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
}
}

View file

@ -0,0 +1,51 @@
<div class="body">
<header>
<div class="header-container">
<button class="button" (click)="goBack()">
zurück
</button>
<div class="header" *ngIf="sensor">
<span>Aufrufzeit: {{ formattedDate }} </span>
<span>Abgefragter Sensor: {{ sensor.sensor }}</span> <br />
<span>Temperatur: {{ sensor.temperature }} &deg;C </span>
<span>Luftfeuchtigkeit: {{ sensor.humidity }} %</span>
</div>
</div>
</header>
<div class="content">
<p> Temperatur und Luftfeuchtigkeit des Sensors "{{sensor.sensor}}" der letzten 24 Stunden</p>
<app-details
*ngIf="dayLabel.length"
[labels]="dayLabel"
[data]="dailyStats"
[style]="'line'"
>
</app-details>
<p> Temperatur und Luftfeuchtigkeit des Sensors "{{sensor.sensor}}"" der letzten Woche</p>
<app-details
*ngIf="weekLabel.length"
[labels]="weekLabel"
[data]="weeklyStats"
[style]="'line'"
>
</app-details>
<p> Temperatur und Luftfeuchtigkeit des Sensors "{{sensor.sensor}}" des letzten Monats</p>
<app-details
*ngIf="monthLabel.length"
[labels]="monthLabel"
[data]="monthlyStats"
[style]="'line'"
>
</app-details>
<p> Temperatur und Luftfeuchtigkeit des Sensors "{{sensor.sensor}}" des letzten Jahres</p>
<app-details
*ngIf="yearLabel.length"
[labels]="yearLabel"
[data]="yearlyStats"
[style]="'line'"
>
</app-details>
</div>
</div>

View file

@ -0,0 +1,70 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,700);
$light_blue: #B3DAEE;
$another_blue: #8cabbb;
.button {
width: 120px;
height: 45px;
font-family: 'Open Sans', sans-serif;
font-size: 14px;
background-color: $another_blue; /* Grüner Hintergrund */
color: white; /* Weißer Text */
border: none; /* Kein Rand */
border-radius: 8px; /* Abgerundete Ecken */
cursor: pointer; /* Zeiger-Cursor bei Hover */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* weicher Schatten */
transition: all 0.3s ease; /* Sanfte Übergänge */
}
.button:hover {
transform: scale(1.05); /* Leichtes Vergrößern beim Hover */
}
.button:active {
transform: scale(0.98); /* Leichtes Schrumpfen beim Klicken */
}
.content {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.header {
background-color: $another_blue;
font-family: 'Open Sans';
height: 60px;
width: 100%;
font-size: 20px;
color: white;
text-align: center;
}
@media screen and (max-width: 768px) {
.button {
width: 100%; // Volle Breite für bessere Usability
height: 50px; // Etwas mehr Höhe für Touch-Ziel
font-size: 16px; // Größere Schrift für bessere Lesbarkeit
}
.content {
padding: 0 15px; // Innenabstand an den Seiten
}
.header {
font-size: 18px; // Etwas kleinere Schrift für kompaktere Darstellung
height: auto; // Höhe flexibel
padding: 10px 0; // Vertikales Padding statt fixer Höhe
}
}
@media screen and (max-width: 480px) {
.button {
font-size: 14px;
height: 45px;
}
.header {
font-size: 16px;
}
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StatComponentComponent } from './stat-component.component';
describe('StatComponentComponent', () => {
let component: StatComponentComponent;
let fixture: ComponentFixture<StatComponentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [StatComponentComponent]
})
.compileComponents();
fixture = TestBed.createComponent(StatComponentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,127 @@
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { SensorService } from '../../services/sensor.service';
import { ChartPreperationService } from '../../services/chart-preperation.service';
/**
* Interface representing a dataset used in statistics.
*/
interface StatData {
/**
* Label for the dataset, e.g., "Temperature", "Humidity".
*/
label: string;
/**
* Array of numerical values representing the data points.
*/
data: number[];
}
/**
* StatComponentComponent
*
* This component displays statistical evaluations for a single sensor.
* It loads and prepares data for different time periods (daily, weekly, monthly, yearly)
* and presents these statistics accordingly.
*/
@Component({
selector: 'app-stat-component',
templateUrl: './stat-component.component.html',
styleUrl: './stat-component.component.scss'
})
export class StatComponentComponent implements OnInit {
/**
* Sensor object containing sensor data and metadata.
*/
sensor!: any;
/**
* Formatted date string representing the timestamp of the latest data.
*/
formattedDate: any;
/**
* General statistics data (unspecified).
*/
stat: any;
/**
* Array of labels for charts.
*/
label: string[] = [];
/**
* Statistical data as nested arrays (e.g., measurement values).
*/
statData: number[][] = [];
/**
* Daily statistics; can be an array of strings or `StatData` objects.
*/
dailyStats: string[] | StatData[] = [];
dayLabel: string[] | StatData[] = [];
/**
* Weekly statistics.
*/
weeklyStats: string[] | StatData[] = [];
weekLabel: string[] | StatData[] = [];
/**
* Monthly statistics.
*/
monthlyStats: string[] | StatData[] = [];
monthLabel: string[] | StatData[] = [];
/**
* Yearly statistics.
*/
yearlyStats: string[] | StatData[] = [];
yearLabel: string[] | StatData[] = [];
/**
* Constructor with Dependency Injection.
*
* @param router Angular Router for navigation
* @param route ActivatedRoute for route parameters
* @param sensorService Service to access sensor data
* @param charts Service to prepare chart data
*/
constructor(
private router: Router,
private route: ActivatedRoute,
private sensorService: SensorService,
private charts: ChartPreperationService
) {}
/**
* Component initialization lifecycle hook.
*
* Retrieves the current sensor object from the SensorService.
* Formats the date of the latest measurement.
* Loads statistical data for various time intervals.
*/
async ngOnInit(): Promise<void> {
this.sensor = this.sensorService.getSensor();
if (!this.sensor) {
// Fallback: add navigation or error handling here if needed
}
this.formattedDate = new Date(Number(this.sensor.DATE_TIME)).toLocaleString();
[
this.dayLabel, this.dailyStats,
this.weekLabel, this.weeklyStats,
this.monthLabel, this.monthlyStats,
this.yearLabel, this.yearlyStats
] = await this.charts.loadData(this.sensor.sensor);
}
/**
* Navigates back to the home page.
*/
goBack(): void {
this.router.navigate(['']);
}
}

View file

@ -0,0 +1,14 @@
<div id="weather_wrapper">
<div class="weatherCard">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="75" height="75" viewBox="0 0 50 50">
<path fill="white" d="M 25 1 C 11.757813 1 1 11.757813 1 25 C 1 38.242188 11.757813 49 25 49 C 38.242188 49 49 38.242188 49 25 C 49 11.757813 38.242188 1 25 1 Z M 25 3 C 37.164063 3 47 12.835938 47 25 C 47 37.160156 37.164063 47 25 47 C 12.839844 47 3 37.160156 3 25 C 3 12.835938 12.839844 3 25 3 Z M 23.46875 3.71875 L 21.375 3.96875 L 21.15625 10.28125 L 22.78125 10.03125 L 22.9375 5.8125 L 24.40625 10.03125 L 25.59375 10.03125 L 27.0625 5.8125 L 27.21875 10.03125 L 28.84375 10.28125 L 28.625 3.96875 L 26.53125 3.71875 L 25 8.15625 Z M 38.78125 8.5625 L 35 13.5625 L 36.15625 14.625 L 40.34375 12.71875 L 37.5625 16.375 L 38.375 17.75 L 44.0625 15.125 L 43.25 13.75 L 39.96875 15.25 L 42.3125 12.1875 L 41.28125 10.90625 L 37.78125 12.53125 L 39.96875 9.65625 Z M 10.9375 9.3125 C 10.796875 9.324219 10.636719 9.355469 10.5 9.40625 C 10.242188 9.503906 10.015625 9.660156 9.84375 9.875 C 9.746094 9.996094 9.660156 10.128906 9.5625 10.25 C 9.054688 10.886719 8.539063 11.519531 8.03125 12.15625 C 7.59375 12.703125 7.15625 13.234375 6.71875 13.78125 C 6.691406 13.816406 6.652344 13.871094 6.625 13.90625 C 6.621094 13.910156 6.683594 13.933594 6.6875 13.9375 C 6.859375 14.074219 7.050781 14.207031 7.21875 14.34375 C 7.859375 14.855469 8.484375 15.363281 9.125 15.875 C 9.757813 16.382813 10.394531 16.898438 11.03125 17.40625 C 11.191406 17.535156 11.371094 17.683594 11.53125 17.8125 C 11.539063 17.820313 11.554688 17.851563 11.5625 17.84375 C 11.59375 17.804688 11.625 17.757813 11.65625 17.71875 C 12.089844 17.175781 12.535156 16.636719 12.96875 16.09375 C 13.492188 15.441406 14.007813 14.777344 14.53125 14.125 C 14.800781 13.789063 15.089844 13.484375 15.21875 13.0625 C 15.285156 12.84375 15.300781 12.597656 15.28125 12.375 C 15.195313 11.777344 14.835938 11.285156 14.28125 11.03125 C 13.726563 10.777344 13.082031 10.871094 12.59375 11.21875 C 12.554688 11.246094 12.507813 11.28125 12.46875 11.3125 C 12.550781 11.035156 12.601563 10.753906 12.53125 10.46875 C 12.464844 10.191406 12.296875 9.949219 12.09375 9.75 C 11.78125 9.445313 11.363281 9.28125 10.9375 9.3125 Z M 10.8125 10.90625 C 11.027344 10.890625 11.257813 10.960938 11.40625 11.125 C 11.566406 11.300781 11.613281 11.550781 11.5625 11.78125 C 11.503906 11.984375 11.347656 12.121094 11.21875 12.28125 C 10.960938 12.605469 10.695313 12.925781 10.4375 13.25 C 10.175781 13.574219 9.914063 13.894531 9.65625 14.21875 C 9.390625 14.007813 9.140625 13.804688 8.875 13.59375 C 8.773438 13.511719 8.664063 13.425781 8.5625 13.34375 C 8.558594 13.339844 8.679688 13.226563 8.6875 13.21875 C 9.179688 12.605469 9.664063 11.988281 10.15625 11.375 C 10.285156 11.214844 10.394531 11.019531 10.59375 10.9375 C 10.660156 10.910156 10.742188 10.910156 10.8125 10.90625 Z M 25 11 C 17.28125 11 11 17.28125 11 25 C 11 32.71875 17.28125 39 25 39 C 32.71875 39 39 32.71875 39 25 C 39 17.28125 32.71875 11 25 11 Z M 13 12.3125 C 13.289063 12.304688 13.535156 12.460938 13.65625 12.71875 C 13.761719 12.96875 13.730469 13.257813 13.5625 13.46875 C 13.394531 13.679688 13.226563 13.886719 13.0625 14.09375 C 12.605469 14.667969 12.144531 15.238281 11.6875 15.8125 C 11.664063 15.792969 11.617188 15.800781 11.59375 15.78125 C 11.46875 15.679688 11.34375 15.570313 11.21875 15.46875 C 11.003906 15.296875 10.808594 15.109375 10.59375 14.9375 C 11.007813 14.417969 11.398438 13.925781 11.8125 13.40625 C 12.015625 13.152344 12.234375 12.878906 12.4375 12.625 C 12.582031 12.445313 12.757813 12.320313 13 12.3125 Z M 25 13 C 31.640625 13 37 18.359375 37 25 L 25 25 L 25 37 C 18.359375 37 13 31.640625 13 25 L 25 25 Z"></path>
</svg>
<span class="temp">{{temperature}}&deg;C</span>
</span>
<!-- in my case C for Celsius could be changed to F (Fahrenheit) -->
<span class="location">{{sensor}}</span>
<span class="humidity">{{humidity}}%</span>
<span class="date"> {{formattedDate}}</span>
</div>
</div>

View file

@ -0,0 +1,75 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,700);
@import url(https://cdnjs.cloudflare.com/ajax/libs/weather-icons/1.2/css/weather-icons.min.css);
$violett: #7474BF;
$tuerkis: #97C9C0;
$light_blue: #B3DAEE;
$another_blue: #8cabbb;
$colorlist: ($violett, $tuerkis, $light_blue);
$key: random(length($colorlist));
body {
background: linear-gradient(90deg, #7474BF 10%, #348AC7 90%);
}
#weather_wrapper{
width: 200px;
height: 210px;
margin: 100px auto;
display: flex;
}
.weatherCard{
width: 200px;
height: 210px;
font-family: 'Open Sans';
position: relative;
display: grid;
background-color: $another_blue;
}
.currentWeather{
width: 200px;
height: 200px;
background: rgba(237, 237, 237, 0.4);
margin: 10;
}
.location{
color: rgb(255, 255, 255);
text-align: center;
text-transform: uppercase;
font-weight: 700;
font-size: 25px;
display: block;
}
.temp{
color: rgb(255, 255, 255);
text-align: center;
margin-top: 7%;
margin-left: 5%;
text-transform: uppercase;
font-weight: 700;
font-size: 35px;
display: block;
}
.humidity {
color: rgb(255, 255, 255);
text-align: center;
text-transform: uppercase;
font-weight: 700;
font-size: 20px;
display: block;
}
.date {
color: rgb(255, 255, 255);
text-align: center;
text-transform: uppercase;
font-weight: 700;
font-size: 20px;
display: block;
}
.icon{
display: flex;
margin-top: 10px;
margin-left: 10px;
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WeathercardsComponent } from './weathercards.component';
describe('WeathercardsComponent', () => {
let component: WeathercardsComponent;
let fixture: ComponentFixture<WeathercardsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WeathercardsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(WeathercardsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,142 @@
import { TypeModifier } from '@angular/compiler';
import { Component, Input } from '@angular/core';
/**
* WeathercardsComponent
*
* This component displays weather data such as temperature, humidity,
* air pressure, rain status, and a formatted date for a given sensor.
*/
@Component({
selector: 'app-weathercards',
templateUrl: './weathercards.component.html',
styleUrl: './weathercards.component.scss'
})
export class WeathercardsComponent {
/**
* Private backing field for the raw date input.
*/
private _date: any = "NaN";
/**
* Formatted date string for display purposes.
* Initialized to the current date.
*/
formattedDate: any = new Date();
/**
* Sensor identifier string.
* Defaults to "default" if not provided.
*/
@Input() sensor: string = "default";
/**
* Private backing field for temperature value.
*/
private _temperature: number = 0;
/**
* Private backing field for humidity value.
*/
private _humidity: number = 0;
/**
* Private backing field for air pressure value.
*/
private _airPressure: number = 0;
/**
* Temperature input property.
* Setter rounds the value to one decimal place.
*/
@Input()
set temperature(value: number) {
this._temperature = this.round(value);
}
/**
* Temperature getter.
*/
get temperature(): number {
return this._temperature;
}
/**
* Humidity input property.
* Setter rounds the value to one decimal place.
*/
@Input()
set humidity(value: number) {
this._humidity = this.round(value);
}
/**
* Humidity getter.
*/
get humidity(): number {
return this._humidity;
}
/**
* Air pressure input property.
* Setter rounds the value to one decimal place.
*/
@Input()
set air_pressure(value: number) {
this._airPressure = this.round(value);
}
/**
* Air pressure getter.
*/
get air_pressure(): number {
return this._airPressure;
}
/**
* Rounds a number to one decimal place.
*
* @param value The number to round.
* @returns The rounded number.
*/
private round(value: number): number {
return Math.round(value * 10) / 10;
}
/**
* Rain status input property.
* Indicates whether it is raining (true or false).
*/
@Input() rain: boolean = false;
/**
* Date input property.
* Setter converts the input value to a Date and formats it as a localized string.
*/
@Input()
set date(value: any) {
this.formattedDate = new Date(Number(value)).toLocaleString();
}
/**
* Date getter.
*/
get date(): any {
return this._date;
}
/**
* Helper method to format a Date object into a German locale date string.
* Returns 'Ungültiges Datum' if the date is invalid.
*
* @param value Date object to format.
* @returns Formatted date string or error message.
*/
private formatDate(value: Date): string {
if (isNaN(value.getTime())) {
return 'Ungültiges Datum';
}
return value.toLocaleDateString('de-DE');
}
}

View file

@ -0,0 +1,79 @@
/**
* Represents a single weather data record from the database or API.
*/
export interface WeatherData {
/** The date and time of the record as a string (e.g., "2024-06-23T15:00:00Z"). */
DATE_TIME: string;
/** Unique identifier for the data entry. */
ID: number;
/** Temperature value in degrees Celsius. */
temperature: number;
/** Humidity value as a percentage. */
humidity: number;
/** Air pressure value in hPa (hectopascals). */
air_pressure: number;
/** The sensor identifier or name that recorded this data. */
sensor: string;
/** Rain indicator, usually 0 for no rain and 1 (or higher) for rain. */
regen: number;
}
/**
* Represents a collection of weather data entries indexed by a string key.
*/
export interface apiData {
/**
* A mapping where keys are strings (e.g., IDs or timestamps)
* and values are WeatherData objects.
*/
[key: string]: WeatherData;
}
/**
* Represents a simplified weather data entry used in UI or reporting.
*/
export interface WeatherEntry {
/** The date of the weather entry in string format. */
date: string;
/** Rain indicator value (e.g., 0 for no rain, 1 for rain). */
regen: number;
/** Temperature value in degrees Celsius. */
temperature: number;
/** Air pressure value in hPa. */
air_pressure: number;
/** Humidity value as a percentage. */
humidity: number;
}
/**
* Represents detailed weather statistics data including date and time.
*/
export interface statsData {
/** The date of the record in string format. */
date: string;
/** The time of the record in string format. */
time: string;
/** Temperature value in degrees Celsius. */
temperature: number;
/** Humidity value as a percentage. */
humidity: number;
/** Air pressure value in hPa. */
air_pressure: number;
/** Rain indicator value (0 = no rain, 1 = rain). */
regen: number;
}

View file

@ -0,0 +1,190 @@
const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const cors = require('cors');
const app = express();
const port = 4202;
app.use(cors());
app.use(bodyParser.json());
// connect to database
const db = new sqlite3.Database('weather.sqlite');
/**
* Creates the HISTORY table if it does not exist.
* The table stores weather data records.
*/
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS HISTORY (
ID INTEGER PRIMARY KEY,
DATE_TIME TEXT,
temperature NUMBER,
humidity NUMBER,
air_pressure NUMBER,
sensor TEXT,
regen INTEGER
);
`);
});
/**
* Executes a SELECT SQL query and sends the result as JSON.
* Responds with an error status if the query fails or no data is found.
*
* @param {string} sql - The SQL query string to execute.
* @param {import('express').Response} res - The Express response object.
*/
function getData(sql, res) {
db.all(sql, [], (err, rows) => {
if (err) {
res.status(400).send(err.message);
return;
}
if (!rows || rows.length === 0) {
res.status(404).send('No data found');
return;
}
res.status(200).json(rows);
});
}
/**
* Executes a modifying SQL query (INSERT, UPDATE, DELETE) and sends the result.
* Responds with an error status if the query fails.
*
* Note: The current implementation attempts to iterate `rows` which is
* not standard for db.run, might need adjustment.
*
* @param {string} sql - The SQL query string to execute.
* @param {import('express').Response} res - The Express response object.
*/
function changeData(sql, res){
db.run(sql, [], (err, rows) => {
if (err) {
res.status(400).send(err.message);
return;
}
const result = {};
rows.forEach((row) => {
result[row.ID] = row;
});
res.status(200).json(result);
}
);
}
/**
* GET endpoint to retrieve all weather data records.
*/
app.get('/get/all/data', (req, res) => {
const sql = `SELECT * FROM HISTORY`;
getData(sql, res);
});
/**
* GET endpoint to retrieve all distinct sensors.
*/
app.get('/get/all/sensors', (req, res) => {
const sql = `SELECT DISTINCT(sensor) FROM HISTORY`;
getData(sql, res);
});
/**
* GET endpoint to retrieve all data for a specific sensor.
*/
app.get('/get/:sensor/data/all', (req, res) => {
const sensor = req.params.sensor;
const sql = `SELECT * FROM HISTORY WHERE sensor = "${sensor}"`;
getData(sql, res);
});
/**
* GET endpoint to retrieve the latest data record for a specific sensor.
*/
app.get('/get/:sensor/data/last', (req, res) => {
const sensor = req.params.sensor;
const sql = `SELECT * FROM HISTORY WHERE sensor = "${sensor}" ORDER BY DATE_TIME DESC LIMIT 1`;
getData(sql, res);
});
/**
* GET endpoint to retrieve aggregated weather data for a sensor
* over a date range with a specified format (H, W, M, Y).
*
* @param {string} sensor - Sensor identifier.
* @param {string} format - Aggregation format: 'H' (hourly), 'W' (weekly), 'M' (monthly), 'Y' (yearly).
* @param {string} start - Start timestamp or date.
* @param {string} end - End timestamp or date.
*/
app.get('/get/:sensor/data/:format/:start/:end', (req, res) => {
const sensor = req.params.sensor;
const enddate = req.params.end;
const format = req.params.format;
const startdate = req.params.start;
let sql;
switch(format){
case('H'):
sql = `SELECT
strftime('%d.%m.%Y %H', datetime(DATE_TIME / 1000, 'unixepoch', 'localtime')) || ' Uhr' AS time,
AVG(temperature) AS temp,
AVG(humidity) AS hum
FROM HISTORY
WHERE sensor = '${sensor}'
AND DATE_TIME BETWEEN ${startdate} AND ${enddate}
GROUP BY time;`;
break;
case('W'):
case('M'):
sql = `SELECT
strftime('%Y-%m-%d', datetime(DATE_TIME / 1000, 'unixepoch', 'localtime')) AS time,
AVG(temperature) AS temp,
AVG(humidity) AS hum
FROM HISTORY
WHERE sensor = '${sensor}'
AND DATE_TIME BETWEEN ${startdate} AND ${enddate}
GROUP BY time
ORDER BY time;`;
break;
case('Y'):
sql = `SELECT
strftime('%Y-%m', datetime(DATE_TIME / 1000, 'unixepoch', 'localtime')) AS time,
AVG(temperature) AS temp,
AVG(humidity) AS hum
FROM HISTORY
WHERE sensor = '${sensor}'
AND DATE_TIME BETWEEN ${startdate} AND ${enddate}
GROUP BY time
ORDER BY time;`;
break;
}
getData(sql, res);
});
/**
* POST endpoint to insert a new weather data record.
* Expects JSON body with temperature, humidity, air_pressure, sensor.
*/
app.post('/insert/data', (req, res) => {
const sqlInsert = `INSERT INTO HISTORY (temperature, humidity, air_pressure, sensor, date_time) VALUES (?, ?, ?, ?, ?)`;
const values = [req.body.temperature, req.body.humidity, req.body.air_pressure, req.body.sensor, new Date()];
db.run(sqlInsert, values, function (err) {
if (err) {
// If error occurs, respond with status 500 and error message
return res.status(500).json({ error: "Database error: " + err.message });
}
// On successful insert, respond with success message and inserted row ID
res.status(200).json({ message: 'Data inserted successfully', id: this.lastID });
});
});
/**
* Start the Express server and listen on the specified port.
*/
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

View file

@ -0,0 +1,32 @@
import requests
import random
import time
from datetime import datetime, timedelta
# Basis-URL der API (ersetzen Sie "http://localhost:3000" durch Ihre Server-URL)
url = 'http://127.0.0.1:4202/insert/data'
# Funktion, die ein zufälliges Datum innerhalb des letzten Jahres bis heute als Unix-Timestamp erstellt
def generate_random_timestamp():
start_date = datetime.now() - timedelta(days=1) # Vor einem Jahr
end_date = datetime.now()
random_date = start_date + (end_date - start_date) * random.random()
return int(random_date.timestamp()) # Unix-Timestamp als Ganzzahl zurückgeben
# Generiere und sende 100 Datensätze
for _ in range(100):
data = {
"temperature": round(random.uniform(-10, 30), 2), # Zufällige Temperatur zwischen -10°C und 30°C
"humidity": round(random.uniform(0, 100), 2), # Luftfeuchtigkeit zwischen 0% und 100%
"air_pressure": round(random.uniform(950, 1050), 2), # Luftdruck zwischen 950 und 1050 hPa
"sensor": f"MDR-Turm", #{random.randint(1, 10)} # Sensorname z.B. sensor_1 bis sensor_10
"date_time": generate_random_timestamp() # Zufälliges Datum als Unix-Timestamp
}
# POST-Anfrage an den API-Endpunkt
response = requests.post(url, json=data)
if response.status_code == 200:
print(f"Datensatz eingefügt: {data}")
else:
print(f"Fehler beim Einfügen: {response.status_code}, {response.text}")

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ChartPreperationService } from './chart-preperation.service';
describe('ChartPreperationService', () => {
let service: ChartPreperationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ChartPreperationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,167 @@
import { Injectable } from '@angular/core';
import { DatabaseService } from './database.service';
interface StatData {
label: string;
data: number[];
}
@Injectable({
providedIn: 'root'
})
export class ChartPreperationService {
/**
* Holds the formatted date, used internally.
*/
formattedDate: any;
/**
* Holds the raw statistical data retrieved from the database.
*/
stat: any;
/**
* Labels used for charts.
*/
label: string[] = [];
/**
* 2D array for numeric statistical data.
*/
statData: number[][] = [];
/**
* Daily statistics array of objects with label and data.
*/
dailyStats: StatData[] = [];
/**
* Labels for daily statistics.
*/
dayLabel: string[] = [];
/**
* Weekly statistics array of objects with label and data.
*/
weeklyStats: StatData[] = [];
/**
* Labels for weekly statistics.
*/
weekLabel: string[] = [];
/**
* Monthly statistics array of objects with label and data.
*/
monthlyStats: StatData[] = [];
/**
* Labels for monthly statistics.
*/
monthLabel: string[] = [];
/**
* Yearly statistics array of objects with label and data.
*/
yearlyStats: StatData[] = [];
/**
* Labels for yearly statistics.
*/
yearLabel: string[] = [];
/**
* Constructor to inject the DatabaseService.
* @param db The database service used for fetching sensor data.
*/
constructor(private db: DatabaseService) { }
/**
* Loads and prepares weather data for different intervals (daily, weekly, monthly, yearly)
* for a given sensor. Fetches temperature and humidity data and organizes it for chart usage.
*
* @param sensor The sensor identifier string.
* @returns A promise resolving to an array containing labels and stats arrays for each interval.
*/
async loadData(sensor: any): Promise<(string[] | StatData[])[]> {
let [label, temp, hum] = await this.getData('H', sensor);
this.dayLabel = label;
this.dailyStats = [
{ label: 'Temperatur', data: temp },
{ label: 'Luftfeuchtigkeit', data: hum }
];
[label, temp, hum] = await this.getData('W', sensor);
this.weekLabel = label;
this.weeklyStats = [
{ label: 'Temperatur', data: temp },
{ label: 'Luftfeuchtigkeit', data: hum }
];
[label, temp, hum] = await this.getData('M', sensor);
this.monthLabel = label;
this.monthlyStats = [
{ label: 'Temperatur', data: temp },
{ label: 'Luftfeuchtigkeit', data: hum }
];
[label, temp, hum] = await this.getData('Y', sensor);
this.yearLabel = label;
this.yearlyStats = [
{ label: 'Temperatur', data: temp },
{ label: 'Luftfeuchtigkeit', data: hum }
];
return [
this.dayLabel, this.dailyStats,
this.weekLabel, this.weeklyStats,
this.monthLabel, this.monthlyStats,
this.yearLabel, this.yearlyStats
];
}
/**
* Fetches weather data for a given interval and sensor from the database service.
*
* @param intervall The aggregation interval string ('H', 'W', 'M', 'Y').
* @param sensor The sensor identifier string.
* @returns A promise resolving to a tuple with arrays for labels, temperature, and humidity.
*/
getData(intervall: string, sensor: string): Promise<[string[], number[], number[]]> {
return new Promise((resolve, reject) => {
this.db.getSensorDataByRange(sensor, intervall, Date.now()).subscribe(
(answer: unknown) => {
if (typeof answer === 'object' && answer !== null) {
this.stat = Object.values(answer as Record<string, unknown>);
const result = this.prepareData(this.stat);
resolve(result);
} else {
reject('Response is not a valid object');
}
},
(error) => {
reject(error);
}
);
});
}
/**
* Converts raw chart data into separate arrays of labels, temperature values, and humidity values.
*
* @param chartData An array of objects containing time, temperature, and humidity properties.
* @returns A tuple with three arrays: labels, temperatures, and humidities.
*/
prepareData(chartData: { time: string; temp: number; hum: number }[]): [string[], number[], number[]] {
const label: string[] = [];
const temp: number[] = [];
const hum: number[] = [];
for (const dataKey in chartData) {
label.push(chartData[dataKey].time);
temp.push(chartData[dataKey].temp);
hum.push(chartData[dataKey].hum);
}
return [label, temp, hum];
}
}

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { DatabaseService } from './database.service';
describe('DatabaseService', () => {
let service: DatabaseService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DatabaseService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,151 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { apiData } from '../datatypes/database_interaction'
@Injectable({
providedIn: 'root'
})
export class DatabaseService {
/**
* Base URL for the API including the port.
*/
private apiUrl = 'http://192.168.178.8:4202';
//private apiUrl = 'http://127.0.0.1:4202';
/**
* Constructor to inject the HttpClient service.
* @param http Angular HttpClient for making HTTP requests.
*/
constructor(private http: HttpClient) {}
/**
* Retrieves all sensor data from the API.
* @returns An Observable emitting all sensor data.
*/
getAllData(): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/all/data`);
}
/**
* Retrieves a list of all sensors from the API.
* @returns An Observable emitting sensor information.
*/
getSensors(): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/all/sensors`);
}
/**
* Retrieves sensor data for a specific sensor and time range.
*
* @param sensor The sensor identifier.
* @param format The aggregation interval format: 'H' (hour), 'W' (week), 'M' (month), or 'Y' (year).
* @param enddate The end date timestamp (milliseconds since epoch).
* @returns An Observable emitting the sensor data within the specified range.
*/
getSensorDataByRange(sensor: string, format: string, enddate: number): Observable<apiData> {
const date = new Date(enddate);
const end = date.getTime();
let start: number;
switch (format) {
case 'H':
start = date.setDate(date.getDate() - 1);
break;
case 'W':
start = date.setDate(date.getDate() - 7);
break;
case 'M':
start = date.setMonth(date.getMonth() - 1);
break;
case 'Y':
start = date.setFullYear(date.getFullYear() - 1);
break;
default:
start = date.getTime();
break;
}
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/data/${format}/${start}/${end}`);
}
/**
* Retrieves all weather data for a specific sensor.
* @param sensor The sensor identifier.
* @returns An Observable emitting all weather data for the sensor.
*/
getWeatherDataBySensor(sensor: string): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/data/all/`);
}
/**
* Retrieves the latest weather data entry for a specific sensor.
* @param sensor The sensor identifier.
* @returns An Observable emitting the latest weather data for the sensor.
*/
getLastWeatherDataBySensor(sensor: string): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/data/last`);
}
/**
* Retrieves weather data from the API starting from a specific date.
* @param startDate The start date in ISO string format.
* @returns An Observable emitting weather data from the specified start date.
*/
getWeatherDataSince(startDate: string): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/data/since/${startDate}`);
}
/**
* Retrieves sensor data for a specific sensor starting from a given date.
* @param startDate The start date in ISO string format.
* @param sensor The sensor identifier.
* @returns An Observable emitting sensor data from the specified start date.
*/
getSensorDataSince(startDate: string, sensor: string): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/since/${startDate}`);
}
/**
* Retrieves monthly ordered data for a given sensor and year.
* @param sensor The sensor identifier.
* @param year The year for which the data is requested.
* @returns An Observable emitting monthly ordered data.
*/
getDataOrderedByMonthBySensor(sensor: string, year: number): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/monthlyordered/${year}`);
}
/**
* Retrieves monthly ordered data for all sensors for a given year.
* @param year The year for which the data is requested.
* @returns An Observable emitting monthly ordered data for all sensors.
*/
getDataOrderedByMonth(year: number): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/monthlyordered/${year}`);
}
/**
* Retrieves monthly logs for a specific sensor.
* @param sensor The sensor identifier.
* @returns An Observable emitting monthly log data.
*/
getMonthlyDataBySensor(sensor: string): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/${sensor}/monthly/log`);
}
/**
* Retrieves monthly logs for all sensors.
* @returns An Observable emitting monthly log data for all sensors.
*/
getMonthlyData(): Observable<apiData> {
return this.http.get<apiData>(`${this.apiUrl}/get/all/monthly/log`);
}
/**
* Retrieves average temperature data for a specific sensor.
* @param sensor The sensor identifier.
* @returns An Observable emitting average temperature data.
*/
getTempAVGDataBySensor(sensor: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/get/${sensor}/data/avg/temp`);
}
}

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SensorService } from './sensor.service';
describe('SensorService', () => {
let service: SensorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SensorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
/**
* Service to store and retrieve sensor data temporarily.
*/
@Injectable({ providedIn: 'root' })
export class SensorService {
private sensorData: any = null;
/**
* Stores the sensor data.
* @param data The sensor data to store.
*/
setSensor(data: any): void {
this.sensorData = data;
}
/**
* Retrieves the stored sensor data.
* @returns The stored sensor data, or null if none is set.
*/
getSensor(): any {
return this.sensorData;
}
}

13
web/src/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Web</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

8
web/src/main.ts Normal file
View file

@ -0,0 +1,8 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule, {
ngZoneEventCoalescing: true
})
.catch(err => console.error(err));

7
web/src/styles.scss Normal file
View file

@ -0,0 +1,7 @@
$light_blue: #B3DAEE;
$another_blue: #8cabbb;
body {
background-color: $light_blue; /* Beispiel: helles Blau-Grau */
margin: 0; /* Entfernt Standardabstand */
font-family: 'Open Sans', sans-serif;
}

15
web/tsconfig.app.json Normal file
View file

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

32
web/tsconfig.json Normal file
View file

@ -0,0 +1,32 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

15
web/tsconfig.spec.json Normal file
View file

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}