Compare commits

...
6 Commits
Author SHA1 Message Date
adminandClaude Opus 5 64445e95fe Zugangsdaten nur noch aus restricted/mysql.php, Datei nicht mehr versioniert
authServer.php und ajax/getForecastData.php hatten Server, Benutzer, Passwort
und Datenbank noch einmal fest eingetragen und damit die Werte ueberschrieben,
die restricted/mysql.php ohnehin liefert. Beide Dateien binden die Konfiguration
bereits ein; die Doppelung ist entfallen.

getForecastData.php benutzt jetzt $mysql_solarUser/-Pass/-DB wie die uebrigen
Endpunkte, statt dieselben Werte unter den generischen Namen zu setzen.

restricted/mysql.php steht jetzt in .gitignore, restricted/mysql.php.example
dient als Vorlage fuer neue Arbeitskopien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:50:45 +02:00
adminandClaude Opus 5 6b8556567b Dashboard: Doppelungen zusammengelegt, Lade- und Renderlast gesenkt
Zusammenlegung
- Die sechs Endpunkte getConsData_/getProdData_{month,year,decade}.php sind
  ajax/energyHistory.php?series=&range= gewichen, der gemeinsame Aufbau der
  Chart-Daten steht in ajax/chartData.php. Die erzeugte SQL wurde gegen alle
  sechs Originale abgeglichen.
- Die 13 Raeume der Home-Ansicht stehen nur noch in restricted/rooms.php.
  home.php rendert die SVG-Kacheln daraus, homeMQTT.js bekommt dieselbe
  Tabelle als JSON (346 -> 222 bzw. 428 -> 197 Zeilen).
- Gemeinsame JS-Helfer in js/solar/common.js; geteilte Bausteine fuer
  Wetterkacheln, Chart-Skripte und das Formular der Ladesteuerung.
- Seitenvorlagen sind jetzt .php und werden eingebunden, statt per
  file_get_contents zusammengesetzt zu werden.

Fehlerbehebungen
- ?action=weather fuehrte auf eine leere Seite, es gibt jetzt weather.php.
- homeMQTT.js griff auf ein #meteogram zu, das home.php nie hatte, und brach
  damit den readystatechange-Handler ab.
- Die Heizungsseite abonnierte weatherStation/#, zeigte die Werte aber nie an.

Ladelast
- Chart.js, MQTT und Meteogramm werden nur noch auf den Seiten geladen, die
  sie brauchen; die Seitentabelle dafuer steht in index.php.
- mqtt.js durch die minifizierte Fassung ersetzt (859 -> 359 KB). MQTT.js
  5.14.1, byte-identisch mit dem signierten npm-Tarball.
- Rund 3,1 MB ungenutzte Vendor-Reste und AdminLTE-Demobilder entfernt.
- Solar 1796 -> 1240 KB, Home 1796 -> 873 KB, Historie 1796 -> 843 KB.

Speicher im Browser
- Chart.defaults.devicePixelRatio auf 1.5 gedeckelt. Die Zeichenpuffer der
  Diagramme waren mit 20-45 MB der groesste Posten der Seite.
- Das Meteogramm-iframe laedt erst beim Hinscrollen.

Realtime-SVG
- Filterbereiche auf das noetige Mass verkleinert und den wirkungslosen
  feOffset-Durchgang entfernt: 60 % weniger gerasterte Filterflaeche bei
  pixelgleichem Ergebnis.
- Die Fluss-Animation laeuft ueber transform statt stroke-dashoffset, mit
  Punkten statt Strichmuster; sie ruht, wo kein Fluss anliegt.
- Der Wertupdate schreibt nur noch bei echter Aenderung und benutzt
  textContent statt innerHTML: 9 statt 86 Elemente pro Update, davon keines
  mit Filter. describeArc rundet auf volle Grad.
- waterInfo auf scale(0.90) wie die uebrigen Info-Gruppen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 22:54:28 +02:00
adminandClaude Sonnet 5 29f752eafe Regenbasierte automatische Gartenbewässerung + homeMesh-Anbindung für AutoAction-Regeleditor
- Neues stündliches Python-Script (restricted/gartenbewaesserung/auto_watering.py): holt Niederschlags-
  und Sonnenuntergangsdaten von Open-Meteo, wertet je Zone (Hochbeete/Tröge/Garten vorn) individuelle
  Regen-Schwellenwerte und Mindest-Bewässerungsdauern aus und startet bei Bedarf per MQTT den passenden
  Automatik-Modus der Ventilsteuerung im Zeitraum vor Sonnenuntergang; veröffentlicht den Regen-Status
  zusätzlich stündlich als retained MQTT-Nachricht je Zone.
- Web-UI: neue Bewässerungs-Steuerung/-Anzeige (ajax/watering.php, js/solar/solarMQTT.js,
  assets/img/realtime.svg) inkl. Live-Status-Icons (aus/geplant/pausiert/aktiv per Farbe und Symbol).
- AutoAction-Regeleditor (ajax/actorDetails.php, sensorDetails.php, fillActorDD.php, fillSensorDD.php,
  tahoma.php, AutoAction.php) liest Actor-/Sensor-Parameter jetzt live aus der homeMesh-Datenbank statt
  aus stark vereinfachten Altfeldern, inkl. zugehöriger Anpassungen am Device-Discovery-Tooling
  (restricted/deviceDiscovery/*, neues logic_module.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 20:19:27 +02:00
admin 4de95c1a29 prevent alert if nothing is returned on modal form sent. 2026-02-14 20:36:43 +01:00
admin ae455dba20 modify filemode for linux 2026-02-14 20:08:34 +01:00
admin 0e78302640 Initial commit 2026-02-14 19:47:21 +01:00
423 changed files with 57401 additions and 1024 deletions
+50
View File
@@ -0,0 +1,50 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
env/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Konfiguration (enthält Credentials!)
config.ini
restricted/mysql.php
# Logs
*.log
logs/
# Betriebssystem
.DS_Store
Thumbs.db
# Temporäre Dateien
*.tmp
*.bak
*.backup
-245
View File
@@ -1,245 +0,0 @@
# Normalisierte Datenbankstruktur für Somfy Tahoma
## Übersicht
Die Datenbank wurde von einer denormalisierten Struktur (mit JSON in `parameters`)
in eine vollständig normalisierte relationale Struktur überführt.
## Datenbankschema
### Haupttabellen
#### `actors`
Speichert alle Aktoren (Geräte mit Steuerungsfunktion)
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| type | VARCHAR(50) | Gerätetyp (z.B. RollerShutter) |
| name | VARCHAR(70) | Name des Geräts |
| parameters | TEXT (nullable) | Optionale Meta-Informationen |
| url | VARCHAR(100) UNIQUE | Tahoma Device URL |
#### `sensors`
Speichert alle Sensoren (Geräte die Werte melden)
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| type | VARCHAR(50) | Sensortyp (z.B. TemperatureSensor) |
| name | VARCHAR(70) | Name des Sensors |
| parameters | TEXT (nullable) | Optionale Meta-Informationen |
| url | VARCHAR(100) UNIQUE | Tahoma Device URL |
### Aktor-Detailtabellen
#### `actor_commands`
Speichert alle verfügbaren Commands für jeden Aktor
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| actor_id | INT (FK → actors.id) | Referenz zum Aktor |
| command_name | VARCHAR(100) | Name des Commands (z.B. setPosition, open) |
**Beispieldaten:**
```
actor_id | command_name
---------|-------------
1 | open
1 | close
1 | setPosition
2 | on
2 | off
```
#### `command_parameters`
Speichert die Parameter für jeden Command
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| command_id | INT (FK → actor_commands.id) | Referenz zum Command |
| parameter_name | VARCHAR(100) | Name des Parameters (z.B. position) |
| parameter_type | VARCHAR(50) | Datentyp (z.B. integer, string) |
| min_value | DECIMAL(10,2) | Minimaler Wert (nullable) |
| max_value | DECIMAL(10,2) | Maximaler Wert (nullable) |
| possible_values | TEXT | JSON Array mit möglichen Werten (nullable) |
**Beispieldaten:**
```
command_id | parameter_name | parameter_type | min_value | max_value
-----------|----------------|----------------|-----------|----------
3 | position | integer | 0 | 100
```
#### `actor_states`
Speichert die aktuellen States von Aktoren
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| actor_id | INT (FK → actors.id) | Referenz zum Aktor |
| state_name | VARCHAR(100) | Name des State (z.B. core:ClosureState) |
| state_type | INT | State-Typ Code aus Tahoma API |
| current_value | VARCHAR(255) | Aktueller Wert |
| unit | VARCHAR(20) | Einheit (nullable) |
| last_updated | TIMESTAMP | Zeitpunkt der letzten Aktualisierung |
### Sensor-Detailtabellen
#### `sensor_states`
Speichert alle verfügbaren States für jeden Sensor
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| id | INT (PK, AUTO_INCREMENT) | Eindeutige ID |
| sensor_id | INT (FK → sensors.id) | Referenz zum Sensor |
| state_name | VARCHAR(100) | Name des State (z.B. core:TemperatureState) |
| state_type | INT | State-Typ Code aus Tahoma API |
| current_value | VARCHAR(255) | Aktueller Wert |
| unit | VARCHAR(20) | Einheit (z.B. °C, %) (nullable) |
| last_updated | TIMESTAMP | Zeitpunkt der letzten Aktualisierung |
**Beispieldaten:**
```
sensor_id | state_name | state_type | current_value | unit
----------|-------------------------|------------|---------------|------
1 | core:TemperatureState | 1 | 21.5 | °C
2 | core:LuminanceState | 1 | 350 | lux
```
## Beziehungen (Foreign Keys)
```
actors (1) ──< (N) actor_commands
└──< (N) command_parameters
actors (1) ──< (N) actor_states
sensors (1) ──< (N) sensor_states
```
Alle Foreign Keys mit `ON DELETE CASCADE` → Wenn ein Aktor/Sensor gelöscht wird,
werden automatisch alle zugehörigen Commands, Parameter und States gelöscht.
## Hilfreiche Views
### `view_actors_with_commands`
Zeigt alle Aktoren mit ihren Commands und Parametern in einer flachen Ansicht
```sql
SELECT * FROM view_actors_with_commands WHERE actor_name = 'Wohnzimmer Rollo';
```
### `view_sensors_with_states`
Zeigt alle Sensoren mit ihren aktuellen States
```sql
SELECT * FROM view_sensors_with_states WHERE sensor_type = 'TemperatureSensor';
```
### `view_all_devices`
Zeigt eine Übersicht aller Geräte (Aktoren und Sensoren)
```sql
SELECT * FROM view_all_devices ORDER BY name;
```
## Beispiel-Queries
### Alle Commands eines bestimmten Aktors anzeigen
```sql
SELECT
a.name as aktor_name,
ac.command_name,
cp.parameter_name,
cp.min_value,
cp.max_value
FROM actors a
JOIN actor_commands ac ON a.id = ac.actor_id
LEFT JOIN command_parameters cp ON ac.id = cp.command_id
WHERE a.name = 'Wohnzimmer Rollo';
```
### Alle Temperatursensoren mit aktuellem Wert
```sql
SELECT
s.name as sensor_name,
ss.current_value as temperatur,
ss.unit,
ss.last_updated
FROM sensors s
JOIN sensor_states ss ON s.id = ss.sensor_id
WHERE s.type = 'TemperatureSensor'
AND ss.state_name LIKE '%Temperature%';
```
### Alle Aktoren eines bestimmten Typs
```sql
SELECT
name,
type,
COUNT(DISTINCT ac.id) as anzahl_commands
FROM actors a
LEFT JOIN actor_commands ac ON a.id = ac.actor_id
WHERE a.type = 'RollerShutter'
GROUP BY a.id, a.name, a.type;
```
### Commands ohne Parameter finden
```sql
SELECT
a.name as aktor,
ac.command_name
FROM actors a
JOIN actor_commands ac ON a.id = ac.actor_id
LEFT JOIN command_parameters cp ON ac.id = cp.command_id
WHERE cp.id IS NULL;
```
## Vorteile der normalisierten Struktur
1. **Keine Datenduplizierung**: Jeder Command und Parameter wird nur einmal gespeichert
2. **Einfache Queries**: SQL-Joins statt JSON-Parsing
3. **Flexible Erweiterung**: Neue Spalten können einfach hinzugefügt werden
4. **Referentielle Integrität**: Foreign Keys garantieren Konsistenz
5. **Performance**: Indizes auf relevanten Spalten für schnelle Suchen
6. **Typsicherheit**: Min/Max als DECIMAL statt String
## Migration von alter zu neuer Struktur
Falls Sie bereits Daten in der alten Struktur haben:
```sql
-- Backup erstellen
CREATE TABLE actors_old AS SELECT * FROM actors;
CREATE TABLE sensors_old AS SELECT * FROM sensors;
-- Alte Tabellen löschen
DROP TABLE actors;
DROP TABLE sensors;
-- Neue Struktur erstellen (database_schema.sql ausführen)
SOURCE database_schema.sql;
-- Python-Script ausführen um Daten neu zu importieren
```
## Wartung
### Regelmäßige Aktualisierung der States
Das Script kann regelmäßig ausgeführt werden. Bei `CLEAR_TABLES = True` werden
alle Daten neu importiert. Bei `CLEAR_TABLES = False` können Updates implementiert werden.
### Veraltete Geräte entfernen
```sql
-- Geräte finden die nicht mehr in der Tahoma Box vorhanden sind
-- (nach erneutem Import)
```
### Index-Optimierung prüfen
```sql
SHOW INDEX FROM actors;
SHOW INDEX FROM actor_commands;
```
+16
View File
@@ -0,0 +1,16 @@
<?php
require_once("restricted/mysql.php");
require_once("helper.php");
$mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db);
if(!mysqli_query($mysql,"DELETE FROM addUser WHERE datetime < DATE_SUB(NOW(), INTERVAL 1 MINUTE);")){
echo mysqli_error($mysql);
}
if(isLocal()){
$accessKey = strval(random_int(0,99999999));
if(!mysqli_query($mysql,"INSERT INTO addUser SET accesskey=".$accessKey.", datetime=NOW();")){
echo mysqli_error($mysql);
}
header('Location: https://nas.el-wa.org/smart?addUser='.$accessKey);
echo 'https://nas.el-wa.org/smart?addUser='.$accessKey;
}
?>
+263
View File
@@ -0,0 +1,263 @@
<?php
require_once("../helper.php");
$close = 0;
if (checkLogin()) {
if (isset($_POST["sensorSelect1"])) { //if form war sent add an action
if(isset($_POST["floor"])){
switch($_POST["floor"]){
case "UG":
$floor = "UG";
break;
case "EG":
$floor = "EG";
break;
case "OG":
$floor = "OG";
break;
default:
$floor = "";
}
}else{
$floor = "";
}
if(isset($_POST["tSpanFrom"])){
$tFrom = date("Y-m-d H:i:s",strtotime($_POST["tSpanFrom"]));
}else{
$tFrom = "00:10:00";
}
if(isset($_POST["tSpanTo"])){
$tTo = date("Y-m-d H:i:s",strtotime($_POST["tSpanTo"]));
}else{
$tTo = "23:59:00";
}
if(isset($_POST["runOnce"])){
$force = true;
}else{
$force = false;
}
if(isset($_POST["actMo"])){$mo = true;}else{$mo = false;}
if(isset($_POST["actDi"])){$di = true;}else{$di = false;}
if(isset($_POST["actMi"])){$mi = true;}else{$mi = false;}
if(isset($_POST["actDo"])){$do = true;}else{$do = false;}
if(isset($_POST["actFr"])){$fr = true;}else{$fr = false;}
if(isset($_POST["actSa"])){$sa = true;}else{$sa = false;}
if(isset($_POST["actSo"])){$so = true;}else{$so = false;}
if(isset($_POST["actFerien"])){$ferien = true;}else{$ferien = false;}
if(isset($_POST["actFeier"])){$feiertag = true;}else{$feiertag = false;}
$close = 0;
if(isset($_POST["changeID"])){
$id = intval($_POST["changeID"]);
if($_POST["changeID"] != "0" && $id == 0){
$id = -1;
}
}else{
$id = -1;
}
$id = 2;
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
if($id > -1){
$qry = "UPDATE autoActions SET
`floor` = '".$floor."', `window_from`= '".$tFrom."', `window_to` = '".$tTo."',
`force_once` = '".$force."', `mo` = '".$mo."', `di` = '".$di."', `mi` = '".$mi."',
`do` = '".$do."', `fr` = '".$fr."', `sa` = '".$sa."', `so` = '".$so."',
`ferien` = '".$ferien."', `feiertag` = '".$feiertag."' WHERE id=".$id.";";
mysqli_query($mysql, $qry);
mysqli_query($mysql, "DELETE FROM autoactionsActors WHERE actionID=".$id.";");
mysqli_query($mysql, "DELETE FROM autoactionsSensors WHERE actionID=".$id.";");
$actionID = $id;
}else{
$qry = "INSERT into autoActions
(`id`, `floor`, `window_from`, `window_to`, `force_once`, `mo`, `di`, `mi`, `do`, `fr`, `sa`, `so`, `ferien`, `feiertag`, `last_run`)
VALUES (NULL, '".$floor."', '".$tFrom."', '".$tTo."', '".$force."', '".$mo."', '".$di."', '".$mi."', '".$do."', '".$fr."', '".$sa."', '".$so."', '".$ferien."', '".$feiertag."', '2020-01-01 12:00:00.000000')";
mysqli_query($mysql, $qry);
$actionID = mysqli_insert_id($mysql);
}
//Get all Sensors:
$num=1;
while(isset($_POST["sensorSelect".$num])){
$id = intval($_POST["sensorSelect".$num]);
if($_POST["sensorSelect".$num] != "0" && $id == 0){
$id = -1;
}
if(isset($_POST["btnLogic".$num])){
if(strtolower($_POST["btnLogic".$num]) == "oder"){
$weight = "or";
}else{
$weight = "and";
}
}else{
$weight = "and";
}
if (in_array($_POST["btnOperator".$num], array('+','-','=','&lt;','&gt;','!='))) {
$cond = $_POST["btnOperator".$num];
}else{
$cond = "=";
echo $_POST["btnOperator".$num];
}
echo $_POST["btnOperator".$num];
$valID = intval($_POST["paramSelect".$num]);
if($_POST["paramSelect".$num] != "0" && $id == 0){
$valID = -1;
}
$state = mysqli_real_escape_string($mysql, $_POST["threshold".$num]);
$qry = "INSERT INTO `autoactionsSensors`
(`id`, `sensorID`, `state`, `valID`, `condType`, `link`, `actionID`)
VALUES (NULL, '".$id."', '".$state."', '".$valID."', '".$cond."', '".$weight."', '".$actionID."')";
mysqli_query($mysql, $qry);
$num++;
}
$close=1;
}
} else {
$close = 1;
}
if (!$close) {
echo <<<ENDE
<!--begin::Form-->
<form id="heater_form">
<div class="accordion" id="actionAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="bi bi-lightning-fill"></i> &nbsp;&nbsp;Auslöser
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#actionAccordion">
<div class="accordion-body" id="sensorsList">
<div class="input-group mb-1" id="sensorSettings1">
<div class="form-floating">
<select class="form-select" id="sensorSelect1" name="sensorSelect1" aria-label="Default select example">
</select>
<label for="sensorSelect1">Sensor</label>
</div>
<div class="form-floating" id="paramBlock1">
<select class="form-select" id="paramSelect1" name="paramSelect1" aria-label="Default select example">
</select>
<label for="paramSelect1">Messwert</label>
</div>
<button class="btn btn-outline-secondary" style="min-width: 40px;" type="button" id="btnOperator1" name="btnOperator1">&gt;</button>
<div class="form-floating" id="valBlock1">
<input type="number" class="form-control" id="threshold1" placeholder="0" value="0" name="threshold1"></input>
<select class="form-select" id="thresholdOpt1" name="thresholdOpt1" placeholder="0" value="0"></select>
<label for="threshold1">Wert/Schwelle</label>
</div>
</div>
</div>
<button class="btn btn-outline-success btn-sm" type="button" id="btnAddSensor"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="bi bi-calendar-check-fill"></i> &nbsp;&nbsp;Bedingungen
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#actionAccordion">
<div class="accordion-body">
<div class="input-group mb-3" id="timespan">
<span class="input-group-text">Aktiver Zeitraum: </span>
<div class="form-floating">
<input type="time" class="form-control" id="tSpanFrom" name="tSpanFrom" placeholder="0" value="00:00"></input>
<label>Von</label>
</div>
<div class="form-floating">
<input type="time" class="form-control" id="tSpanTo" name="tSpanTo" placeholder="0" value="23:59"></input>
<label>Bis</label>
</div>
</div>
Aktive Wochentage:
<div class="input-group mb-3" id="timespan">
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actMo" name="actMo"><label class="form-check-label" for="actMo">Mo</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actDi" name="actDi"><label class="form-check-label" for="actDi">Di</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actMi" name="actMi"><label class="form-check-label" for="actMi">Mi</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actDo" name="actDo"><label class="form-check-label" for="actDo">Do</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actFr" name="actFr"><label class="form-check-label" for="actFr">Fr</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actSa" name="actSa"><label class="form-check-label" for="actSa">Sa</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-1" type="checkbox" value="1" id="actSo" name="actSo"><label class="form-check-label" for="actSo">So</label>
</div>
</div>
Ferien/Feiertage:
<div class="input-group mb-3" id="timespan">
<div class="input-group-text">
<input class="form-check-input mt-0 me-2" type="checkbox" value="1" id="actFerien" name="actFerien"><label class="form-check-label" for="actFerien">In den Ferien ausführen</label>
</div>
<div class="input-group-text">
<input class="form-check-input mt-0 me-2" type="checkbox" value="1" id="actFeier" name="actFeier"><label class="form-check-label" for="actFeier">An Feiertagen ausführen</label>
</div>
</div>
Falls die Bedingung bis zuletzt nicht erfüllt wurde:
<div class="input-group mb-3" id="timespan">
<div class="input-group-text">
<input class="form-check-input mt-0 me-2" type="checkbox" value="1" id="runOnce" name="runOnce"><label class="form-check-label" for="runOnce">Auf jeden Fall zu Ende des aktiven Zeitraums ausführen</label>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<i class="bi bi-play-fill"></i> &nbsp;&nbsp;Aktionen
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#actionAccordion">
<div class="accordion-body">
<div class="accordion-body" id="actorsList">
<div class="input-group mb-3" id="actorSettings1">
<div class="form-floating">
<select class="form-select" id="actorSelect1" name="actorSelect1">
</select>
<label for="actorSelect1">Aktor</label>
</div>
<div class="form-floating" id="actParamBlock1">
<select class="form-select" id="actParamSelect1" name="actParamSelect1">
</select>
<label for="paramSelect1">Eigenschaft</label>
</div>
<button class="btn btn-outline-secondary" style="min-width: 40px;" type="button" id="btnActOperator1" name="btnActOperator1">&gt;</button>
<div class="form-floating" id="act1ValBlock1">
<input type="number" class="form-control" id="act1Value1" name="act1Value1" value="0"></input>
<select class="form-select" id="act1ValueOpt1" name="act1ValueOpt1"></select>
<label for="act1Value1">Wert 1</label>
</div>
<div class="form-floating" id="act2ValBlock1">
<input type="number" class="form-control" id="act2Value1" name="act2Value1" value="0"></input>
<label for="act2Value1">Wert 2</label>
</div>
<div class="form-floating" id="act3ValBlock1">
<input type="number" class="form-control" id="act3Value1" name="act3Value1" value="0"></input>
<label for="act3Value1">Wert 3</label>
</div>
<div class="form-floating" id="act4ValBlock1">
<input type="number" class="form-control" id="act4Value1" name="act4Value1" value="0"></input>
<label for="act4Value1">Wert 4</label>
</div>
</div>
</div>
</div>
<button class="btn btn-outline-success btn-sm" type="button" id="btnAddActor"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
</div>
</div>
</form>
ENDE;
}
+81
View File
@@ -0,0 +1,81 @@
<?php
require_once("../helper.php");
if(isset($_GET["actorID"])){
$actorID = intval($_GET["actorID"]);
$qry = "SELECT command_name, command_url, parameter_type, min_value, max_value, possible_values, command_parameters.url AS parameter_url, parameter_name FROM actor_commands LEFT JOIN command_parameters ON actor_commands.id = command_id LEFT JOIN state_types ON parameter_type = state_types.id WHERE actor_id=".$actorID;
$qry = "SELECT id, command_name, command_url FROM actor_commands WHERE actor_id = ".$actorID;
$mysql = new mysqli($mysql_server, $mysql_MeshUser, $mysql_MeshPass, $mysql_MeshDB);
$result = mysqli_query($mysql, $qry);
$operators = "";
//while($row = $result->fetch_assoc()){
// print_r($row);
// echo "<br />";
//}
echo '{"commands": [';
$iterated = 0;
while($row = $result->fetch_assoc()){
$items = "[]";
$qry2 = "SELECT parameter_type, min_value, max_value, possible_values, command_parameters.url AS parameter_url, parameter_name FROM actor_commands LEFT JOIN command_parameters ON actor_commands.id = command_id LEFT JOIN state_types ON parameter_type = state_types.id WHERE command_id = ".$row["id"];
$result2 = mysqli_query($mysql, $qry2);
if($iterated++){
echo ", ";
}
echo '{"name" : "'.$row["command_name"].'", "operators" : ["="], "parameters": [';
$iterated2 = 0;
$type = "hidden";
while($row2 = $result2->fetch_assoc()){
switch($row2["parameter_type"]){
case "integer":
if($row2["possible_values"]){
$items = $row2["possible_values"];
$type = "dropdown";
}else{
$type = "number";
}
break;
case "float":
$type = "number";
break;
case "time":
$type = "time";
break;
case "bool":
$type = "button";
break;
case "string":
if($row2["possible_values"]){
$items = $row2["possible_values"];
$type = "dropdown";
}else{
$type = "text";
}
break;
case "":
$type = "hidden";
default:
$type = "text";
break;
}
if($iterated2++){
echo ", ";
}
echo '{"name" : "'.$row2["parameter_name"].'", "url" : "'.$row2["parameter_url"].'", "type" : "'.$type.'", "options" : '.$items.'}';
}
echo '], "type" : "'.$type.'"}';
}
echo "]}";
}
/*
require_once("../helper.php");
if(isset($_GET["actorID"])){
$ID = intval($_GET["actorID"]);
$qry = "SELECT parameters FROM actors WHERE id=".$ID;
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$result = mysqli_query($mysql, $qry);
echo $result->fetch_array()[0];
}
*/
?>
+60
View File
@@ -0,0 +1,60 @@
<?php
require_once("../helper.php");
if (checkLogin()) {
chdir("/volume1/homes/wagner/");
putenv('PYTHONPATH="/var/services/homes/wagner/.local/lib/python3.8/site-packages/');
$close = 0;
if (isset($_POST["evStart/Stop"])) {
if ($_POST["evStart/Stop"] == "Laden starten") {
$close = 1;
exec("python wattpilot.py -start 2>&1", $output, $return_var);
} elseif ($_POST["evStart/Stop"] == "Laden stoppen") {
$close = 1;
exec("python wattpilot.py -stop 2>&1", $output, $return_var);
}
$close = 1;
} else if (isset($_POST["evMode"])) {
if ($_POST["fte"] > 100)
$_POST["fte"] = 100;
else if ($_POST["fte"] < 1)
$_POST["fte"] = 1;
if ($_POST["evAmp"] > 16)
$_POST["evAmp"] = 16;
if ($_POST["evAmp"] < 6)
$_POST["evAmp"] = 6;
$_POST["fte"] = $_POST["fte"] * 140;
if ($_POST["evMode"] == "eco") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
exec("python wattpilot.py -eco -time " . $_POST["ftt"] . " -energy " . $_POST["fte"] . " -maxCurr " . $_POST["evAmp"] . " -setTime " . gmdate("Y-m-d\TH:i:s", time()) . ".000 2>&1", $output, $return_var);
} else {
exec("/bin/python wattpilot.py -e 2>&1", $output, $return_var);
}
} else if ($_POST["evMode"] == "default") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
exec("python wattpilot.py -default -time " . $_POST["ftt"] . " -energy " . $_POST["fte"] . " -maxCurr " . $_POST["evAmp"] . " -setTime " . gmdate("Y-m-d\TH:i:s", time()) . ".000 2>&1", $output, $return_var);
} else {
exec("python wattpilot.py -d 2>&1", $output, $return_var);
}
} else if ($_POST["evMode"] == "NextTrip") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
exec("python wattpilot.py -trip -time " . $_POST["ftt"] . " -energy " . $_POST["fte"] . " -maxCurr " . $_POST["evAmp"] . " -setTime " . gmdate("Y-m-d\TH:i:s", time()) . ".000 2>&1", $output, $return_var);
} else {
exec("python wattpilot.py -t 2>&1", $output, $return_var);
}
}
$close = 1;
}
} else {
$close = 1;
}
if (!$close) {
$formId = "carEG_form";
include __DIR__ . "/carForm.php";
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/*
* Formular des Auto-Lademodals. carEG.php und carOG.php zeigen dasselbe
* Formular; nur die Ansteuerung der Wallbox dahinter unterscheidet sich.
* $formId trennt die beiden Formulare fuer das JavaScript.
*/
?>
<div class="d-grid gap-2 col-6 mx-auto my-4">
<button class="btn btn-primary" id="evStart/Stop">Laden starten</button>
</div>
<!--begin::Form-->
<form id="<?= $formId ?>">
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Modus</div>
</div>
<!--begin::Body-->
<div class="card-body">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="evMode" id="EV_Default" value="default">
<label class="form-check-label" for="EV_Default"> Immer Laden </label></input>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="evMode" id="EV_Eco" value="eco">
<label class="form-check-label" for="EV_Eco"> Überschuss </label></input>
</div>
<div class="form-check disabled">
<input class="form-check-input" type="radio" name="evMode" id="EV_NextTrip" value="NextTrip">
<label class="form-check-label" for="EV_NextTrip"> Ladeplanung </label></input>
</div>
</div>
</div>
</div>
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Max. Ladestrom</div>
</div>
<div class="card-body">
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="evAmp" id="EV_6A" value="6">
<label class="form-check-label" for="EV_6A"> 6A </label></input>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="evAmp" id="EV_10A" value="10">
<label class="form-check-label" for="EV_10A"> 10A </label></input>
</div>
<div class="form-check disabled">
<input class="form-check-input" type="radio" name="evAmp" id="EV_16A" value="16">
<label class="form-check-label" for="EV_16A"> 16A </label></input>
</div>
</div>
</div>
</div>
<div class="card m-3">
<div class="card-header py-1 px-3">
<div class="card-title">Ladeplanung</div>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="form-check">
<label class="form-label" for="addedCharge">Zusätzlich benötigte Ladung (%): </label>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">sdf</div>
<input type="range" id="modal-slider" name="fte" class="form-range range-color-track" min="1" max="100" />
<div class="d-flex justify-content-between">
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">1%</span>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">100%</span>
</div>
<!--<input name="fte" class="form-control" type="number" max="100" min="1" value="" />-->
</div>
</div>
<div class="row mb-3">
<div class="form-check">
<label class="form-label" for="planedTime">Zeitpunkt zudem geladen sein soll: </label>
<input id="EV_NextTripTime" name="ftt" class="form-control" size="2" type="time" value=""/>
</div>
</div>
</div>
</div>
</form>
+58
View File
@@ -0,0 +1,58 @@
<?php
require_once("../helper.php");
if (checkLogin()) {
$close = 0;
if (isset($_POST["evStart/Stop"])) {
if ($_POST["evStart/Stop"] == "Laden starten") {
$close = 1;
httpGetJson("http://192.168.179.122/api/set?frc=2");
} else if ($_POST["evStart/Stop"] == "Laden stoppen") {
$close = 1;
httpGetJson("http://192.168.179.122/api/set?frc=1");
}
$close = 1;
} else if (isset($_POST["evMode"])) {
if ($_POST["fte"] > 100)
$_POST["fte"] = 100;
else if ($_POST["fte"] < 0)
$_POST["fte"] = 0;
if ($_POST["evAmp"] > 16)
$_POST["evAmp"] = 16;
if ($_POST["evAmp"] < 6)
$_POST["evAmp"] = 6;
$_POST["fte"] = $_POST["fte"] * 90;
$seconds = strtotime('1970-01-01 ' . $_POST["ftt"] . ':00GMT');
if ($_POST["evMode"] == "eco") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&amp=" . $_POST["evAmp"] . "&fup=true&lmo=4");
} else {
httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=4");
}
} else if ($_POST["evMode"] == "default") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&amp=" . $_POST["evAmp"] . "&fup=true&lmo=3");
} else {
httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=3");
}
} else if ($_POST["evMode"] == "NextTrip") {
$close = 1;
if ($_POST["ftt"] && $_POST["fte"] > 0) {
httpGetJson("http://192.168.179.122/api/set?ate=" . $_POST["fte"] . "&att=" . $seconds . "&amp=" . $_POST["evAmp"] . "&fup=true&lmo=5");
} else {
httpGetJson("http://192.168.179.122/api/set?amp=" . $_POST["evAmp"] . "&fup=true&lmo=5");
}
}
$close = 1;
}
} else {
$close = 1;
}
if (!$close) {
$formId = "carOG_form";
include __DIR__ . "/carForm.php";
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/*
* Gemeinsame Bausteine für die Chart-Endpunkte in ajax/.
*
* Alle Endpunkte liefern Chart.js-Daten im gleichen Format: eine Abfrage mit
* einer Spalte "time" (Beschriftung) und je einer Spalte pro Datenreihe.
* chartDataset() legt das Styling einer Reihe fest, buildChartData() baut
* daraus das fertige {labels, datasets}-Objekt.
*/
/**
* Styling für eine Datenreihe. $filled merkt sich über die Reihen hinweg,
* ob die unterste Fläche des Stapels schon gegen die Nulllinie gefüllt wurde.
*/
function chartDataset($name, $linecolors, &$filled)
{
$color = isset($linecolors[$name]) ? $linecolors[$name] : "#888888";
$dataset = (object)[];
$dataset->label = $name;
$dataset->data = [];
$dataset->borderColor = $color;
$dataset->backgroundColor = $color . "55";
$dataset->borderWidth = 1;
$dataset->pointRadius = 0;
$dataset->pointHoverRadius = 5;
$dataset->tension = 0.2;
if ($name == "Solarleistung") {
$dataset->stack = "SolarPwr";
$dataset->fill = "none";
$dataset->yAxisID = 'y';
} else if ($name == "Ladestand") {
$dataset->stack = "Charge";
$dataset->fill = "none";
$dataset->yAxisID = 'y1';
} else {
$dataset->stack = "Consumers";
if (!$filled) {
$filled = true;
$dataset->fill = "origin";
} else {
$dataset->fill = "-1";
}
$dataset->yAxisID = 'y';
}
return $dataset;
}
/**
* Führt $query aus und baut daraus das Chart.js-Objekt.
* $labelFactor multipliziert die Zeitspalte (1 für Textbeschriftungen,
* 1000 für Unix-Zeitstempel, die Chart.js in Millisekunden erwartet).
*/
function buildChartData($mysql, $query, $linecolors, $labelFactor = 1)
{
$obj = (object)[];
$obj->labels = [];
$obj->datasets = [];
$result = mysqli_query($mysql, $query);
if (!$result) {
// Nicht ins Ergebnis schreiben - der Aufrufer liefert JSON aus.
error_log("chartData: " . mysqli_error($mysql));
return $obj;
}
if ($result->num_rows < 2) {
return $obj;
}
$column = []; // Spaltenname -> Position in $obj->datasets
$filled = false;
foreach ($result->fetch_fields() as $field) {
if ($field->name == "time") {
continue;
}
$column[$field->name] = count($obj->datasets);
$obj->datasets[] = chartDataset($field->name, $linecolors, $filled);
}
while ($row = $result->fetch_assoc()) {
foreach ($row as $name => $value) {
if ($name == "time") {
$obj->labels[] = $labelFactor == 1 ? $value : $value * $labelFactor;
} else {
$obj->datasets[$column[$name]]->data[] = $value;
}
}
}
return $obj;
}
+74
View File
@@ -0,0 +1,74 @@
<?php
/*
* Aggregierte Energiedaten für die Historie-Seite.
* Ersetzt getConsData_{month,year,decade}.php und getProdData_{month,year,decade}.php.
*
* Aufruf: energyHistory.php?series=prod|cons&range=month|year|decade
*/
require_once("../helper.php");
require_once("./chartData.php");
// Zeitraum -> [Format der Beschriftung, Filter auf datetime]
$ranges = [
"month" => ["'%d.%b.'", "DATE_FORMAT(datetime,'%Y%m%d') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 1 MONTH),'%Y%m%d')"],
"year" => ["'%b. %y'", "DATE_FORMAT(datetime,'%Y%m') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 13 MONTH),'%Y%m')"],
"decade" => ["'%Y'", "DATE_FORMAT(datetime,'%Y') >= DATE_FORMAT(DATE_SUB(now(),INTERVAL 10 YEAR),'%Y')"],
];
// Datenreihen je Diagramm. Beide teilen sich Solarertrag und Heizstab und
// unterscheiden sich nur darin, wohin die Energie fließt (prod) bzw. woher
// sie kommt (cons).
$series = [
"prod" => "SUM(pvP/12 - IF(gridP < 0, -gridP/12,0) - IF(battP < 0, -battP/12,0) - heaterPwr/12) AS Direktverbrauch,
SUM(heaterPwr/12) AS Heizstab,
SUM(IF(battP < 0, -battP/12,0)) AS Batterieladung,
SUM(gridPfeed/12) AS Einspeisung",
"cons" => "SUM(pvP/12 - IF(gridP < 0, -gridP/12,0) - IF(battP < 0, -battP/12,0) - heaterPwr/12) AS Solarverbrauch,
SUM(heaterPwr/12) AS Heizstab,
SUM(IF(battP > 0, battP/12,0)) AS Batteriebezug,
SUM(gridPcons/12) AS Netzbezug",
];
$linecolors["Direktverbrauch"] = "#FFFF00";
$linecolors["Solarverbrauch"] = "#FFFF00";
$linecolors["UG"] = "#FFaa00";
$linecolors["OG"] = "#FF4400";
$linecolors["Auto UG"] = "#00aaFF";
$linecolors["Auto OG"] = "#0044FF";
$linecolors["Heizstab"] = "#FF6600";
$linecolors["Batterieladung"] = "#00aa00";
$linecolors["Batteriebezug"] = "#00aa00";
$linecolors["Einspeisung"] = "#FF0000";
$linecolors["Netzbezug"] = "#FF0000";
$linecolors["Ladestand"] = "#00aa00";
$linecolors["Vorhersage"] = "#2222FF";
header('Content-Type: application/json; charset=utf-8');
$rangeKey = isset($_GET["range"]) ? $_GET["range"] : "";
$seriesKey = isset($_GET["series"]) ? $_GET["series"] : "";
if (!isset($ranges[$rangeKey]) || !isset($series[$seriesKey])) {
http_response_code(400);
echo json_encode(["error" => "Unbekannter Zeitraum oder Datenreihe."]);
exit;
}
list($labelFormat, $filter) = $ranges[$rangeKey];
$obj = (object)[];
$obj->labels = [];
$obj->datasets = [];
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$query = "SELECT
DATE_FORMAT(datetime, $labelFormat) AS 'time',
" . $series[$seriesKey] . "
FROM EnergyFlow
WHERE $filter
GROUP BY DATE_FORMAT(datetime, $labelFormat)
ORDER BY datetime ; ";
$obj = buildChartData($mysql, $query, $linecolors);
}
echo json_encode($obj);
+11
View File
@@ -0,0 +1,11 @@
<?php
require_once("../helper.php");
$qry = "SELECT name, actors.id FROM actors INNER JOIN actor_commands ON actor_id = actors.id GROUP BY actors.id";
$mysql = new mysqli($mysql_server, $mysql_MeshUser, $mysql_MeshPass, $mysql_MeshDB);
$result = mysqli_query($mysql, $qry);
while($row = $result->fetch_assoc()){
echo "<option value='".$row["id"]."'>".$row["name"]."</option>";
}
?>
+11
View File
@@ -0,0 +1,11 @@
<?php
require_once("../helper.php");
$qry = "SELECT name, actors.id FROM actors INNER JOIN actor_states ON actor_id = actors.id GROUP BY actors.id";
$mysql = new mysqli($mysql_server, $mysql_MeshUser, $mysql_MeshPass, $mysql_MeshDB);
$result = mysqli_query($mysql, $qry);
while($row = $result->fetch_assoc()){
echo "<option value='".$row["id"]."'>".$row["name"]."</option>";
}
?>
+162
View File
@@ -0,0 +1,162 @@
<?php
require_once("../helper.php");
if(!isset($_GET["TO"])){
$_GET["TO"] = 12;
}
$_GET["TO"] = intval($_GET["TO"]);
if(!isset($_GET["FROM"])){
$_GET["FROM"] = -24;
}
$_GET["FROM"] = intval($_GET["FROM"]);
//-totalConsumption-PL1_EV*1000-PL2_EV*1000-PL3_EV*1000-PL1_EVog*1000-PL2_EVog*1000-PL3_EVog*1000+PL1_OG+PL2_OG+PL3_OG-heaterPwr AS 'UG',
$consQuery = "SELECT
UNIX_TIMESTAMP(EnergyFlow.datetime) AS time,
pvP AS 'Solarleistung',
soc AS Ladestand,
-totalConsumption-PL1_EV*1000-PL2_EV*1000-PL3_EV*1000-PL1_EVog*1000-PL2_EVog*1000-PL3_EVog*1000+PL1_OG+PL2_OG+PL3_OG-heaterPwr-PL1_UG-PL2_UG-PL3_UG-PL1_EG-PL2_EG-PL3_EG AS 'Gemein',
(PL1_UG+PL2_UG+PL3_UG) AS 'UG',
(PL1_EG+PL2_EG+PL3_EG) AS 'EG',
-(PL1_OG+PL2_OG+PL3_OG) AS 'OG',
(PL1_EV+PL2_EV+PL3_EV)*1000 AS 'Auto UG',
(PL1_EVog+PL2_EVog+PL3_EVog)*1000 AS 'Auto OG',
heaterPwr AS 'Heizstab',
IF(battP<0, -battP, 0) AS Batterieladung,
gridPfeed AS Einspeisung
FROM solarLog.EnergyFlow
WHERE EnergyFlow.datetime BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR)
ORDER BY EnergyFlow.datetime";
$simQuery = "SELECT
UNIX_TIMESTAMP(simPower.period_end) AS time,
power*1000 AS 'Vorhersage'
FROM solarLog.simPower
WHERE simPower.period_end BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR)
ORDER BY simPower.period_end";
$linecolors["Solarleistung"] = "#FFFF00";
$linecolors["Gemein"] = "#FF9900";
$linecolors["UG"] = "#FF8800";
$linecolors["EG"] = "#FF6600";
$linecolors["OG"] = "#FF4400";
$linecolors["Auto UG"] = "#00aaFF";
$linecolors["Auto OG"] = "#0044FF";
$linecolors["Heizstab"] = "#FF0000";
$linecolors["Batterieladung"] = "#00aa00";
$linecolors["Einspeisung"] = "#b0b0b0";
$linecolors["Ladestand"] = "#00aa00";
$linecolors["Vorhersage"] = "#2222FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$result = mysqli_query($mysql, $consQuery);
$simRes = mysqli_query($mysql,$simQuery);
if(!$result){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
$obj = (object)[]; // Cast empty array to object
$obj->labels = [];
$obj->datasets = [];
$i = 0;
$filled = 0;
if ($simRes->num_rows > 1) {
$dataset = (object)[];
$row1 = $simRes->fetch_assoc();
$dataset->borderColor = $linecolors["Vorhersage"];
$dataset->backgroundColor = $linecolors["Vorhersage"]."55";
$dataset->borderWidth=1.5;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->stack = "sim";
$dataset->fill = "none";
$dataset->yAxisID = 'y';
$dataset->label = "Vorhersage";
/*$pt = (object)[];
$pt->x = $row1["time"]*1000;
$pt->y = $row1["Vorhersage"];
$dataset->data[] = clone $pt;
while ($row1 = $simRes->fetch_assoc()) {
$pt = (object)[];
$pt->x = $row1["time"]*1000 + 30*60*1000;
$pt->y = $row1["Vorhersage"];
$dataset->data[] = clone $pt;
}*/
$obj->datasets[] = clone $dataset;
$rownext = $simRes->fetch_assoc();
$nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000;
}
if ($result->num_rows > 1) {
$ii = 1;
$row = $result->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."55";
$dataset->borderWidth=1;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
if ($name == "Solarleistung") {
$dataset->stack = "SolarPwr";
$dataset->fill = "none";
$dataset->yAxisID = 'y';
} else if ($name == "Ladestand") {
$dataset->stack = "Charge";
$dataset->fill = "none";
$dataset->yAxisID = 'y1';
} else {
$dataset->stack = "Consumers";
if ($filled == 0) {
$filled = 1;
$dataset->fill = "origin";
} else {
$dataset->fill = "-1";
}
$dataset->yAxisID = 'y';
}
$dataset->label = $name;
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
$ii++;
} else {
$obj->labels[] = $value * 1000;
}
}
while ($row = $result->fetch_assoc()) {
$ii = 1;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
} else {
if(($value * 1000) < $nextSimTimestamp){
$obj->datasets[0]->data[] = $row1["Vorhersage"];
}else{
$row1 = $rownext;
$rownext = $simRes->fetch_assoc();
$nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000;
$obj->datasets[0]->data[] = $row1["Vorhersage"];
}
$obj->labels[] = $value * 1000;
}
}
}
}
$obj->labels[] = $nextSimTimestamp; //Draw future forecast
$obj->datasets[0]->data[] = $rownext["Vorhersage"];
while($rownext = $simRes->fetch_assoc()){
$obj->labels[] = $rownext["time"]*1000 + 30*60*1000;
$obj->datasets[0]->data[] = $rownext["Vorhersage"];
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
?>
+171
View File
@@ -0,0 +1,171 @@
<?php
require_once("../helper.php");
$consEstQuery = "SELECT
UNIX_TIMESTAMP(DATE_ADD(datetime, INTERVAL 28 DAY)) AS 'time',
SUM(-(totalConsumption + heaterPwr)/48) AS 'Vorraussichtl. Verbrauch'
FROM EnergyFlow
WHERE
DATE(datetime) >= DATE(DATE_SUB(NOW(),INTERVAL 28 DAY)) And DATE(datetime) != DATE(NOW())
GROUP BY WEEKDAY(datetime)
ORDER BY datetime";
$prodEstQuery = "SELECT UNIX_TIMESTAMP(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) AS 'time',
SUM(power*500) AS 'Vorhersage'
FROM simPower
WHERE DATE(CONVERT_TZ(period_End,'GMT','Europe/Berlin')) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY))
GROUP BY DAY(CONVERT_TZ(period_End,'GMT','Europe/Berlin'))
ORDER BY period_End;";
$prodRealQuery = "SELECT
UNIX_TIMESTAMP(datetime) AS 'time',
SUM(pvP/12) AS 'Tatsächliche Erzeugung',
SUM(-totalConsumption/12) AS 'Tatsächlicher Verbrauch'
FROM EnergyFlow
WHERE
DATE(datetime) >= DATE(DATE_SUB(NOW(),INTERVAL 7 DAY))
GROUP BY DAY(datetime)
ORDER BY datetime";
$linecolors["Tatsächliche Erzeugung"] = "#cccc00";
$linecolors["Tatsächlicher Verbrauch"] = "#EE9900";
$linecolors["Vorraussichtl. Verbrauch"] = "#BB4400";
$linecolors["Auto UG"] = "#00aaFF";
$linecolors["Auto OG"] = "#0044FF";
$linecolors["Heizstab"] = "#FF0000";
$linecolors["Batterieladung"] = "#00aa00";
$linecolors["Einspeisung"] = "#b0b0b0";
$linecolors["Ladestand"] = "#00aa00";
$linecolors["Vorhersage"] = "#4444FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$consEst = mysqli_query($mysql, $consEstQuery);
$prodEst = mysqli_query($mysql,$prodEstQuery);
$prodReal = mysqli_query($mysql,$prodRealQuery);
if(!$consEst){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
if(!$prodEst){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
if(!$prodRealQuery){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
$obj = (object)[]; // Cast empty array to object
$obj->labels = [];
$obj->datasets = [];
$i = 0;
$filled = 0;
if($consEst->num_rows > 1){
$row = $consEst->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."66";
$dataset->borderWidth=1;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->stack = $name;
//$dataset->fill = "none";
$dataset->yAxisID = 'y';
$dataset->label = $name;
for($i=0;$i<7;$i++){
$dataset->data[] = NULL;
}
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
}
}
while ($row = $consEst->fetch_assoc()) {
$ii = 0;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
}
}
}
}
if ($prodEst->num_rows > 1) {
$row = $prodEst->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."55";
$dataset->borderWidth=1;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->stack = $name;
//$dataset->fill = "none";
$dataset->yAxisID = 'y';
$dataset->label = $name;
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
} else {
$obj->labels[] = $value * 1000;
}
}
while ($row = $prodEst->fetch_assoc()) {
$ii = 1;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
} else {
$obj->labels[] = $value * 1000;
}
}
}
}
if($prodReal->num_rows > 1){
$row = $prodReal->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."55";
$dataset->borderWidth=1;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->stack = $name;
//$dataset->fill = "none";
$dataset->yAxisID = 'y';
$dataset->label = $name;
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
}
}
while ($row = $prodReal->fetch_assoc()) {
$ii = 2;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
}
}
}
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
+105
View File
@@ -0,0 +1,105 @@
<?php
require_once("../helper.php");
$heatQuery = "SELECT
UNIX_TIMESTAMP(Heater.datetime) AS time,
PufferU AS 'Speicher unten',
PufferM AS 'Speicher mitte',
PufferO AS 'Speicher oben',
thermeVLfb AS 'Therme Vorlauf Fußboden',
thermeRL AS 'Therme Rücklauf',
heaterVL AS 'Heizstab Vorlauf',
heaterRL AS 'Heizstab Rücklauf',
fbVL AS 'Fußboden Vorlauf',
fbRL AS 'Fußboden Rücklauf'
FROM Heater
WHERE Heater.datetime BETWEEN DATE_SUB(NOW(),INTERVAL 24 HOUR) and NOW()
ORDER BY Heater.datetime";
$waterQuery = "SELECT UNIX_TIMESTAMP(wasser.datetime) AS time, rate AS 'Wasserverbrauch'
FROM solarLog.wasser
WHERE wasser.datetime BETWEEN DATE_SUB(NOW(),INTERVAL 24 HOUR) and NOW()
ORDER BY wasser.datetime";
$linecolors["Speicher oben"] = "#FF5500";
$linecolors["Speicher mitte"] = "#FFaa00";
$linecolors["Speicher unten"] = "#FFFF00";
$linecolors["Therme Vorlauf Fußboden"] = "#bb0000";
$linecolors["Therme Rücklauf"] = "#ee0000";
$linecolors["Heizstab Vorlauf"] = "#9900bb";
$linecolors["Heizstab Rücklauf"] = "#8800aa";
$linecolors["Fußboden Vorlauf"] = "#00FF00";
$linecolors["Fußboden Rücklauf"] = "#00aa00";
$linecolors["Wasserverbrauch"] = "#2222FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$result = mysqli_query($mysql, $heatQuery);
$simRes = mysqli_query($mysql,$waterQuery);
if(!$result){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
$obj = (object)[]; // Cast empty array to object
$obj->labels = [];
$obj->datasets = [];
$i = 0;
$filled = 0;
if ($result->num_rows > 1) {
$ii = 0;
$row = $result->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."22";
$dataset->borderWidth=2;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
if(strpos($name,"Speicher") === false) {
$dataset->fill = "none";
} else {
// $dataset->stack = "Consumers";
if ($filled == 0) {
$filled = 1;
$dataset->fill = "origin";
} else {
$dataset->fill = "-1";
}
$dataset->yAxisID = 'y';
}
$dataset->label = $name;
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
$ii++;
} else {
$obj->labels[] = $value * 1000;
}
}
while ($row = $result->fetch_assoc()) {
$ii = 0;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
} else {
$obj->labels[] = $value * 1000;
}
}
}
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
+165
View File
@@ -0,0 +1,165 @@
<?php
require_once("../helper.php");
if(!isset($_GET["TO"])){
$_GET["TO"] = 12;
}
$_GET["TO"] = intval($_GET["TO"]);
if(!isset($_GET["FROM"])){
$_GET["FROM"] = -24;
}
$_GET["FROM"] = intval($_GET["FROM"]);
$consQuery = "SELECT
UNIX_TIMESTAMP(EnergyFlow.datetime) AS time,
pvP AS Solarleistung,
IF(-totalConsumption - IF(gridP>0, gridP, 0) - IF(battP>0, battP, 0) > 0, -totalConsumption - IF(gridP>0, gridP, 0) - IF(battP>0, battP, 0), 0) AS Direktverbrauch,
IF(battP>0, battP, 0) AS Batteriebezug,
gridPcons AS Netzbezug,
-totalConsumption AS Verbrauch,".
//IF(battP<0, -battP, 0) AS Batterieladung,
"soc AS Ladestand
FROM solarLog.EnergyFlow
WHERE EnergyFlow.datetime BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR)
ORDER BY EnergyFlow.datetime";
$simQuery = "SELECT
UNIX_TIMESTAMP(simPower.period_end) AS time,
power*1000 AS 'Vorhersage'
FROM solarLog.simPower
WHERE simPower.period_end BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"])." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR)
ORDER BY simPower.period_end";
$linecolors["Solarleistung"] = "#FFFF00";
$linecolors["Direktverbrauch"] = "#FFcc00";
$linecolors["Verbrauch"] = "#FFaa44";
$linecolors["Auto UG"] = "#00aaFF";
$linecolors["Auto OG"] = "#0044FF";
$linecolors["Netzbezug"] = "#FF0000";
$linecolors["Batteriebezug"] = "#00aa00";
$linecolors["Batterieladung"] = "#0033aa";
$linecolors["Einspeisung"] = "#b0b0b0";
$linecolors["Ladestand"] = "#00aa00";
$linecolors["Vorhersage"] = "#2222FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$result = mysqli_query($mysql, $consQuery);
$simRes = mysqli_query($mysql,$simQuery);
if(!$result){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
$obj = (object)[]; // Cast empty array to object
$obj->labels = [];
$obj->datasets = [];
$i = 0;
$filled = 0;
if ($simRes->num_rows > 1) {
$dataset = (object)[];
$row1 = $simRes->fetch_assoc();
$dataset->borderColor = $linecolors["Vorhersage"];
$dataset->backgroundColor = $linecolors["Vorhersage"]."55";
$dataset->borderWidth=1.5;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->stack = "sim";
$dataset->fill = "none";
$dataset->yAxisID = 'y';
$dataset->label = "Vorhersage";
/*$pt = (object)[];
$pt->x = $row["time"]*1000;
$pt->y = $row["Vorhersage"];
$dataset->data[] = clone $pt;
while ($row = $simRes->fetch_assoc()) {
$pt = (object)[];
$pt->x = $row["time"]*1000 + 30*60*1000;
$pt->y = $row["Vorhersage"];
$dataset->data[] = clone $pt;
}*/
$obj->datasets[] = clone $dataset;
$rownext = $simRes->fetch_assoc();
$nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000;
}
if ($result->num_rows > 1) {
$ii = 1;
$row = $result->fetch_assoc();
foreach ($row as $name => $value) {
$dataset = (object)[];
if ($name != "time") {
$dataset->borderColor = $linecolors[$name];
$dataset->backgroundColor = $linecolors[$name]."22";
$dataset->borderWidth=1;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
if ($name == "Solarleistung") {
$dataset->stack = "SolarPwr";
$dataset->fill = "none";
$dataset->yAxisID = 'y';
}else if ($name == "Verbrauch") {
$dataset->stack = "ConsPwr";
$dataset->fill = "1";
$dataset->yAxisID = 'y';
}else if ($name == "Batterieladung") {
$dataset->stack = "ConsPwr";
$dataset->fill = "-1";
$dataset->backgroundColor = $linecolors[$name]."77";
$dataset->yAxisID = 'y';
} else if ($name == "Ladestand") {
$dataset->stack = "Charge";
$dataset->fill = "none";
$dataset->yAxisID = 'y1';
} else {
$dataset->stack = "Consumers";
if ($filled == 0) {
$filled = 1;
$dataset->fill = "origin";
} else {
$dataset->fill = "-1";
}
$dataset->yAxisID = 'y';
}
$dataset->label = $name;
$dataset->data[] = $value;
$obj->datasets[] = clone $dataset;
$ii++;
} else {
$obj->labels[] = $value * 1000;
}
}
while ($row = $result->fetch_assoc()) {
$ii = 1;
foreach ($row as $name => $value) {
if ($name != "time") {
$obj->datasets[$ii]->data[] = $value;
$ii++;
} else {
if(($value * 1000) < $nextSimTimestamp){
$obj->datasets[0]->data[] = $row1["Vorhersage"];
}else{
$row1 = $rownext;
$rownext = $simRes->fetch_assoc();
$nextSimTimestamp = $rownext["time"]*1000 + 30*60*1000;
$obj->datasets[0]->data[] = $row1["Vorhersage"];
}
$obj->labels[] = $value * 1000;
}
}
}
}
$obj->labels[] = $nextSimTimestamp; //Draw future forecast
$obj->datasets[0]->data[] = $rownext["Vorhersage"];
while($rownext = $simRes->fetch_assoc()){
$obj->labels[] = $rownext["time"]*1000 + 30*60*1000;
$obj->datasets[0]->data[] = $rownext["Vorhersage"];
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
?>
+155
View File
@@ -0,0 +1,155 @@
<?php
require_once("../helper.php");
if(!isset($_GET["year"])){
$_GET["year"] = date("Y");
}else{
$_GET["year"] = intval($_GET["year"]);
}
if($_GET["year"] < 2018 || $_GET["year"] > date("Y")){
exit;
}
if(isset($_GET["type"])){
if(strtolower($_GET["type"]) =="lastyear"){
$_GET["year"] = date("Y")-1;
}elseif(strtolower($_GET["type"]) =="prelastyear"){
$_GET["year"] = date("Y")-2;
}
}
$Query = "SELECT
SUM(gridPcons/12000) as 'Stromverbrauch',
SUM((gridPcons*cost)/12000)+140 as 'Stromkosten (140€ fix)',
SUM(pvP - IF(gridP < 0, -gridP,0) - IF(battP < 0, -battP,0)- heaterPwr + IF(battP > 0, battP,0))*cost/12000 as 'Verbrauchsersparnis',
SUM((gridPfeed*gain)/12000) as 'Einspeisevergütung',
SUM(IF(gridP < 500, heaterPwr, 0)*0.063/12000) as 'Ersparnis Heizung',
SUM(IF(battP > 0, battP, 0)*cost/12000) as 'Ersparnis Batterie',
AVG(autonomy) AS 'Ø Autarkie',
(SUM(PL1_EV+PL2_EV+PL3_EV))/12 AS 'Autoladung ges. EG',
(((SUM(IF(gridP>100,0,PL1_EV+PL2_EV+PL3_EV)))/(SUM(PL1_EV+PL2_EV+PL3_EV))))*100 AS 'Autoladung Solar EG',
(SUM(IF(gridP>100,0,(PL1_EV+PL2_EV+PL3_EV)*cost))/12) AS 'Ersparnis Solarladung EG',
(SUM(PL1_EV+PL2_EV+PL3_EV))/24.5 AS 'Benzin gespart EG',
(SUM(PL1_EVog+PL2_EVog+PL3_EVog))/12 AS 'Autoladung ges. OG',
(((SUM(IF(gridP>100,0,PL1_EVog+PL2_EVog+PL3_EVog)))/(SUM(PL1_EVog+PL2_EVog+PL3_EVog))))*100 AS 'Autoladung Solar OG',
(SUM(IF(gridP>100,0,(PL1_EVog+PL2_EVog+PL3_EVog)*cost))/12) AS 'Ersparnis Solarladung OG',
(SUM(PL1_EVog+PL2_EVog+PL3_EVog))/24.5 AS 'Benzin gespart OG'
FROM EnergyFlow JOIN gridCosts ON DATE(datetime) >= DATE(gridCosts.active_date) AND DATE(datetime) <= DATE(gridCosts.end_date)
WHERE year(datetime) = ".$_GET["year"].";";
$PrevQuery = "SELECT
SUM(gridPcons/12000) as 'Stromverbrauch',
SUM((gridPcons*cost)/12000)+140 as 'Stromkosten (140€ fix)',
SUM(pvP - IF(gridP < 0, -gridP,0) - IF(battP < 0, -battP,0)- heaterPwr + IF(battP > 0, battP,0))*cost/12000 as 'Verbrauchsersparnis',
SUM((gridPfeed*gain)/12000) as 'Einspeisevergütung',
SUM(IF(gridP < 500, heaterPwr, 0)*0.063/12000) as 'Ersparnis Heizung',
SUM(IF(battP > 0, battP, 0)*cost/12000) as 'Ersparnis Batterie',
AVG(autonomy) AS 'Ø Autarkie',
(SUM(PL1_EV+PL2_EV+PL3_EV))/12 AS 'Autoladung ges. EG',
(((SUM(IF(gridP>100,0,PL1_EV+PL2_EV+PL3_EV)))/(SUM(PL1_EV+PL2_EV+PL3_EV))))*100 AS 'Autoladung Solar EG',
(SUM(IF(gridP>100,0,(PL1_EV+PL2_EV+PL3_EV)*cost))/12) AS 'Ersparnis Solarladung EG',
(SUM(PL1_EV+PL2_EV+PL3_EV))/24.5 AS 'Benzin gespart EG',
(SUM(PL1_EVog+PL2_EVog+PL3_EVog))/12 AS 'Autoladung ges. OG',
(((SUM(IF(gridP>100,0,PL1_EVog+PL2_EVog+PL3_EVog)))/(SUM(PL1_EVog+PL2_EVog+PL3_EVog))))*100 AS 'Autoladung Solar OG',
(SUM(IF(gridP>100,0,(PL1_EVog+PL2_EVog+PL3_EVog)*cost))/12) AS 'Ersparnis Solarladung OG',
(SUM(PL1_EVog+PL2_EVog+PL3_EVog))/24.5 AS 'Benzin gespart OG'
FROM EnergyFlow JOIN gridCosts ON DATE(datetime) >= DATE(gridCosts.active_date) AND DATE(datetime) <= DATE(gridCosts.end_date)
WHERE year(datetime) = ".($_GET["year"]-1).";";
$units = Array("kWh","€","€","€","€","€","€","%","kWh","%","€","L","kWh","%","€","L");
$LessIsBetter = Array(true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false);
function array_insert($array,$values,$offset) {
return array_slice($array, 0, $offset, true) + $values + array_slice($array, $offset, NULL, true);
}
if (checkLogin()) {
$html = "<div class='row row-info'>";
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$Res = mysqli_query($mysql,$Query);
$ResPrev = mysqli_query($mysql,$PrevQuery);
if(!$Res || !$ResPrev){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
if ($Res->num_rows > 0) {
$i = 0;
$row = $Res->fetch_assoc();
$rowPrev = $ResPrev->fetch_assoc();
//$row["Vergütung+Einsparung Strom"] = $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"];
$row = array_insert($row,["Verg.+Einsp. Strom" => $row["Einspeisevergütung"]+$row["Verbrauchsersparnis"]],4);
$rowPrev = array_insert($rowPrev,["Verg.+Einsp. Strom" => $rowPrev["Einspeisevergütung"]+$rowPrev["Verbrauchsersparnis"]],4);
$i = 0;
foreach ($row as $name => $value) {
if(str_starts_with($name,"Autoladung ges.")){
$html .= "</div><hr class='mt-2 mb-3 border-light' />";
$html .= "<div class='row row-info'>";
}
/*$html .= "<div class='col-12 col-sm-4 col-md-2 col-xl-1'>
<div class='info-box'>
<div class='info-box-content'>
<span class='info-box-text'>".$name."</span>
<span class='info-box-number'>".
number_format(floatval($value),2,",",".")
."<small>".$units[$i++]."</small>
</span>
</div>
<!-- /.info-box-content -->
</div>
</div>";*/
$html .= "<div class='col'>
<div class='card stats border-0'>
<div class='card-body bg-dark bg-gradient rounded-top pb-1 pt-1'>
".$name."
</div>
<div class='card-footer text-center'>";
if($_GET["year"] != date("Y")){
if($LessIsBetter[$i]){
if($value == 0)
$dev=100;
else
$dev = $rowPrev[$name]/$value;
//$first = round($rowPrev[$name]*10/$rowPrev[$name]); //allow for 10% deviation for displaying no tendency
//$second = round($value); //allow for 10% deviation for displaying no tendency
$arrowGood = "down";
$arrowBad = "up";
}else{
if($rowPrev[$name] == 0)
$dev=100;
else
$dev = $value/$rowPrev[$name];
//$first = round($value); //allow for 10% deviation for displaying no tendency
//$second = round($rowPrev[$name]); //allow for 10% deviation for displaying no tendency
$arrowGood = "up";
$arrowBad = "down";
}
if($dev > 1.05){
$html .= "<span class='float-right text-success-emphasis'>
<i class='bi bi-arrow-".$arrowGood."' style='font-size: 0.9em;'></i> ";
}elseif($dev < 0.95){
$html .= "<span class='float-right text-danger-emphasis'>
<i class='bi bi-arrow-".$arrowBad."' style='font-size: 0.9em;'></i> ";
}else{
$html .= "<span class='float-right'>
<i class='bi bi-arrow-right' style='font-size: 0.9em;'></i> ";
}
}
else{
$html .= "<span class='float-right'>";
}
$html .= number_format(floatval($value),2,",",".")
."<small> ".$units[$i]."</small></span></div>
<!-- /.info-box-content -->
</div>
</div>";
$i++;
}
}
$html .= "</div>";
}
//header('Content-Type: application/json');
echo $html;
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once("../helper.php");
if(!isset($_GET["TO"])){
$_GET["TO"] = 12;
}
$_GET["TO"] = intval($_GET["TO"]);
if(!isset($_GET["FROM"])){
$_GET["FROM"] = -24;
}
$_GET["FROM"] = intval($_GET["FROM"]);
$Query = "SELECT
UNIX_TIMESTAMP(CONCAT(date,' ',sunrise)) AS sunrise,
UNIX_TIMESTAMP(CONCAT(date,' ',sunset)) AS sunset
FROM solarLog.daylight
WHERE date BETWEEN DATE_ADD(NOW(),INTERVAL ".($_GET["FROM"]-24)." HOUR) and DATE_ADD(NOW(),INTERVAL ".$_GET["TO"]." HOUR)
ORDER BY date";
$linecolors["Solarleistung"] = "#FFFF00";
$linecolors["UG"] = "#FFaa00";
$linecolors["OG"] = "#FF4400";
$linecolors["Auto UG"] = "#00aaFF";
$linecolors["Auto OG"] = "#0044FF";
$linecolors["Heizstab"] = "#FF0000";
$linecolors["Batterieladung"] = "#00aa00";
$linecolors["Einspeisung"] = "#b0b0b0";
$linecolors["Ladestand"] = "#00aa00";
$linecolors["Vorhersage"] = "#2222FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$result = mysqli_query($mysql, $Query);
if (!$result) {
echo "Error:<br>" . mysqli_error($mysql) . "<br />";
}
//$obj[] = (object)[]; // Cast empty array to object
while ($row = $result->fetch_assoc()) {
$ii = 1;
$anno = (object)[];
if($row["sunrise"] < (time()+$_GET["FROM"]*60*60)){
$anno->xMin = (time()+$_GET["FROM"]*60*60)*1000;
}else{
$anno->xMin = $row["sunrise"]*1000;
}
if($row["sunset"] > (time()+$_GET["TO"]*60*60)){
$anno->xMax = round(time()+$_GET["TO"]*60*60)*1000;
}else{
$anno->xMax = $row["sunset"]*1000;
}
$anno->borderWidth = 0;
if($row["sunset"] > (time()+$_GET["FROM"]*60*60)){
$obj[] = clone $anno;
}
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once("../helper.php");
$waterQuery = "SELECT UNIX_TIMESTAMP(datetime) AS time, rate AS 'Wasserverbrauch'
FROM solarLog.wasser
WHERE wasser.datetime BETWEEN DATE_SUB(NOW(),INTERVAL 24 HOUR) and NOW()
ORDER BY wasser.datetime";
$linecolors["Speicher oben"] = "#FF5500";
$linecolors["Speicher mitte"] = "#FFaa00";
$linecolors["Speicher unten"] = "#FFFF00";
$linecolors["Therme Vorlauf Fußboden"] = "#bb0000";
$linecolors["Therme Rücklauf"] = "#ee0000";
$linecolors["Heizstab Vorlauf"] = "#9900bb";
$linecolors["Heizstab Rücklauf"] = "#8800aa";
$linecolors["Fußboden Vorlauf"] = "#00FF00";
$linecolors["Fußboden Rücklauf"] = "#00aa00";
$linecolors["Wasserverbrauch"] = "#2222FF";
if (checkLogin()) {
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
$waterRes = mysqli_query($mysql,$waterQuery);
if(!$waterRes){
echo "Error:<br>".mysqli_error($mysql)."<br />";
}
$obj = (object)[]; // Cast empty array to object
$obj->labels = [];
$obj->datasets = [];
$i = 0;
$filled = 0;
if ($waterRes->num_rows > 1) {
$dataset = (object)[];
$row = $waterRes->fetch_assoc();
$dataset->borderColor = $linecolors["Wasserverbrauch"];
$dataset->backgroundColor = $linecolors["Wasserverbrauch"]."00";
$dataset->borderWidth=2;
$dataset->pointRadius= 0;
$dataset->pointHoverRadius= 5;
$dataset->tension=0.2;
$dataset->fill = "none";
$dataset->yAxisID = 'y1';
$dataset->label = "Wasserverbrauch";
$pt = (object)[];
$pt->x = $row["time"]*1000;
$pt->y = $row["Wasserverbrauch"];
$dataset->data[] = clone $pt;
while ($row = $waterRes->fetch_assoc()) {
$pt = (object)[];
$pt->x = $row["time"]*1000;
$pt->y = $row["Wasserverbrauch"];
$dataset->data[] = clone $pt;
}
$obj->datasets[] = clone $dataset;
}
}
//header('Content-Type: application/json');
echo json_encode($obj);
//echo '{"labels":[1761322682000,1761322782000,1761322882000,1761322982000,1761323082000,1761323182000,1761323282000],"datasets":[{"stack": "Stack 0","cubicInterpolationMode":"monotone","fill":"origin","label":"Acquisitions by year","data":[10,20,50,20,10,5,70]},{"fill": "false","stack": "Stack 1","cubicInterpolationMode": "monotone","label": "Acquisitions by year","data": [10,20,50,20,10,5,70]}]}';
+57
View File
@@ -0,0 +1,57 @@
<?php
require_once("../helper.php");
if (checkLogin()) {
$close = 0;
$hostname = "192.168.179.169";
if (isset($_POST["setMode"]) || isset($_GET["setMode"])) {
//send new value
$_POST["setMode"] = intval($_POST["setMode"]) + intval($_GET["setMode"]);
if($_POST["setMode"] > 0 && $_POST["setMode"] < 60){
$pwr = $_POST["setMode"]*100;
$headers[] = "GET /set?mode=man&pwr=".$pwr." HTTP/1.1";
$headers[] = "Host: ".$hostname;
$headers[] = "";
$remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5);
fwrite($remote, implode("\r\n", $headers)."\r\n");
$file = '';
$file .= fread($remote, 1024);
fclose($remote);
}elseif($_POST["setMode"] == 0){
$headers[] = "GET /set?mode=eco&pwr=0 HTTP/1.1";
$headers[] = "Host: ".$hostname;
$headers[] = "";
$remote = fsockopen("tcp://".$hostname, 80, $errno, $errstr, 5);
fwrite($remote, implode("\r\n", $headers)."\r\n");
$file = '';
$file .= fread($remote, 1024);
fclose($remote);
}
$close = 1;
}
} else {
$close = 1;
}
if (!$close) {
echo <<<ENDE
<!--begin::Form-->
<form id="heater_form">
<div class="col-sm-11">
<div class="form-check">
<label class="form-label" for="addedCharge">Leistung: </label>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">0</div>
<input type="range" id="modal-slider" name="setMode" class="form-range range-color-track" min="0" max="60" />
<div class="d-flex justify-content-between">
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">Auto</span>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">6.0 kW</span>
</div>
</div>
</div>
</form>
ENDE;
}
+671
View File
@@ -0,0 +1,671 @@
<?php
namespace Bluerhinos;
/*
phpMQTT
A simple php class to connect/publish/subscribe to an MQTT broker
*/
/*
Licence
Copyright (c) 2010 Blue Rhinos Consulting | Andrew Milsted
andrew@bluerhinos.co.uk | http://www.bluerhinos.co.uk
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.
*/
/* phpMQTT */
class phpMQTT
{
protected $socket; /* holds the socket */
protected $msgid = 1; /* counter for message id */
public $keepalive = 10; /* default keepalive timmer */
public $timesinceping; /* host unix time, used to detect disconects */
public $topics = []; /* used to store currently subscribed topics */
public $debug = false; /* should output debug messages */
public $address; /* broker address */
public $port; /* broker port */
public $clientid; /* client id sent to brocker */
public $will; /* stores the will of the client */
protected $username; /* stores username */
protected $password; /* stores password */
public $cafile;
protected static $known_commands = [
1 => 'CONNECT',
2 => 'CONNACK',
3 => 'PUBLISH',
4 => 'PUBACK',
5 => 'PUBREC',
6 => 'PUBREL',
7 => 'PUBCOMP',
8 => 'SUBSCRIBE',
9 => 'SUBACK',
10 => 'UNSUBSCRIBE',
11 => 'UNSUBACK',
12 => 'PINGREQ',
13 => 'PINGRESP',
14 => 'DISCONNECT'
];
/**
* phpMQTT constructor.
*
* @param $address
* @param $port
* @param $clientid
* @param null $cafile
*/
public function __construct($address, $port, $clientid, $cafile = null)
{
$this->broker($address, $port, $clientid, $cafile);
}
/**
* Sets the broker details
*
* @param $address
* @param $port
* @param $clientid
* @param null $cafile
*/
public function broker($address, $port, $clientid, $cafile = null): void
{
$this->address = $address;
$this->port = $port;
$this->clientid = $clientid;
$this->cafile = $cafile;
}
/**
* Will try and connect, if fails it will sleep 10s and try again, this will enable the script to recover from a network outage
*
* @param bool $clean - should the client send a clean session flag
* @param null $will
* @param null $username
* @param null $password
*
* @return bool
*/
public function connect_auto($clean = true, $will = null, $username = null, $password = null): bool
{
while ($this->connect($clean, $will, $username, $password) === false) {
sleep(10);
}
return true;
}
/**
* @param bool $clean - should the client send a clean session flag
* @param null $will
* @param null $username
* @param null $password
*
* @return bool
*/
public function connect($clean = true, $will = null, $username = null, $password = null): bool
{
if ($will) {
$this->will = $will;
}
if ($username) {
$this->username = $username;
}
if ($password) {
$this->password = $password;
}
if ($this->cafile) {
$socketContext = stream_context_create(
[
'ssl' => [
'verify_peer_name' => true,
'cafile' => $this->cafile
]
]
);
$this->socket = stream_socket_client('tls://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $socketContext);
} else {
$this->socket = stream_socket_client('tcp://' . $this->address . ':' . $this->port, $errno, $errstr, 60, STREAM_CLIENT_CONNECT);
}
if (!$this->socket) {
$this->_errorMessage("stream_socket_create() $errno, $errstr");
return false;
}
stream_set_timeout($this->socket, 5);
stream_set_blocking($this->socket, 0);
$i = 0;
$buffer = '';
$buffer .= chr(0x00);
$i++; // Length MSB
$buffer .= chr(0x04);
$i++; // Length LSB
$buffer .= chr(0x4d);
$i++; // M
$buffer .= chr(0x51);
$i++; // Q
$buffer .= chr(0x54);
$i++; // T
$buffer .= chr(0x54);
$i++; // T
$buffer .= chr(0x04);
$i++; // // Protocol Level
//No Will
$var = 0;
if ($clean) {
$var += 2;
}
//Add will info to header
if ($this->will !== null) {
$var += 4; // Set will flag
$var += ($this->will['qos'] << 3); //Set will qos
if ($this->will['retain']) {
$var += 32;
} //Set will retain
}
if ($this->username !== null) {
$var += 128;
} //Add username to header
if ($this->password !== null) {
$var += 64;
} //Add password to header
$buffer .= chr($var);
$i++;
//Keep alive
$buffer .= chr($this->keepalive >> 8);
$i++;
$buffer .= chr($this->keepalive & 0xff);
$i++;
$buffer .= $this->strwritestring($this->clientid, $i);
//Adding will to payload
if ($this->will !== null) {
$buffer .= $this->strwritestring($this->will['topic'], $i);
$buffer .= $this->strwritestring($this->will['content'], $i);
}
if ($this->username !== null) {
$buffer .= $this->strwritestring($this->username, $i);
}
if ($this->password !== null) {
$buffer .= $this->strwritestring($this->password, $i);
}
$head = chr(0x10);
while ($i > 0) {
$encodedByte = $i % 128;
$i /= 128;
$i = (int)$i;
if ($i > 0) {
$encodedByte |= 128;
}
$head .= chr($encodedByte);
}
fwrite($this->socket, $head, 2);
fwrite($this->socket, $buffer);
$string = $this->read(4);
if (ord($string[0]) >> 4 === 2 && $string[3] === chr(0)) {
$this->_debugMessage('Connected to Broker');
} else {
$this->_errorMessage(
sprintf(
"Connection failed! (Error: 0x%02x 0x%02x)\n",
ord($string[0]),
ord($string[3])
)
);
return false;
}
$this->timesinceping = time();
return true;
}
/**
* Reads in so many bytes
*
* @param int $int
* @param bool $nb
*
* @return false|string
*/
public function read($int = 8192, $nb = false)
{
$string = '';
$togo = $int;
if ($nb) {
return fread($this->socket, $togo);
}
while (!feof($this->socket) && $togo > 0) {
$fread = fread($this->socket, $togo);
$string .= $fread;
$togo = $int - strlen($string);
}
return $string;
}
/**
* Subscribes to a topic, wait for message and return it
*
* @param $topic
* @param $qos
*
* @return string
*/
public function subscribeAndWaitForMessage($topic, $qos): string
{
$this->subscribe(
[
$topic => [
'qos' => $qos,
'function' => '__direct_return_message__'
]
]
);
do {
$return = $this->proc();
} while ($return === true);
return $return;
}
/**
* subscribes to topics
*
* @param $topics
* @param int $qos
*/
public function subscribe($topics, $qos = 0): void
{
$i = 0;
$buffer = '';
$id = $this->msgid;
$buffer .= chr($id >> 8);
$i++;
$buffer .= chr($id % 256);
$i++;
foreach ($topics as $key => $topic) {
$buffer .= $this->strwritestring($key, $i);
$buffer .= chr($topic['qos']);
$i++;
$this->topics[$key] = $topic;
}
$cmd = 0x82;
//$qos
$cmd += ($qos << 1);
$head = chr($cmd);
$head .= $this->setmsglength($i);
fwrite($this->socket, $head, strlen($head));
$this->_fwrite($buffer);
$string = $this->read(2);
$bytes = ord(substr($string, 1, 1));
$this->read($bytes);
}
/**
* Sends a keep alive ping
*/
public function ping(): void
{
$head = chr(0xc0);
$head .= chr(0x00);
fwrite($this->socket, $head, 2);
$this->timesinceping = time();
$this->_debugMessage('ping sent');
}
/**
* sends a proper disconnect cmd
*/
public function disconnect(): void
{
$head = ' ';
$head[0] = chr(0xe0);
$head[1] = chr(0x00);
fwrite($this->socket, $head, 2);
}
/**
* Sends a proper disconnect, then closes the socket
*/
public function close(): void
{
$this->disconnect();
stream_socket_shutdown($this->socket, STREAM_SHUT_WR);
}
/**
* Publishes $content on a $topic
*
* @param $topic
* @param $content
* @param int $qos
* @param bool $retain
*/
public function publish($topic, $content, $qos = 0, $retain = false): void
{
$i = 0;
$buffer = '';
$buffer .= $this->strwritestring($topic, $i);
if ($qos) {
$id = $this->msgid++;
$buffer .= chr($id >> 8);
$i++;
$buffer .= chr($id % 256);
$i++;
}
$buffer .= $content;
$i += strlen($content);
$head = ' ';
$cmd = 0x30;
if ($qos) {
$cmd += $qos << 1;
}
if (empty($retain) === false) {
++$cmd;
}
$head[0] = chr($cmd);
$head .= $this->setmsglength($i);
fwrite($this->socket, $head, strlen($head));
$this->_fwrite($buffer);
}
/**
* Writes a string to the socket
*
* @param $buffer
*
* @return bool|int
*/
protected function _fwrite($buffer)
{
$buffer_length = strlen($buffer);
for ($written = 0; $written < $buffer_length; $written += $fwrite) {
$fwrite = fwrite($this->socket, substr($buffer, $written));
if ($fwrite === false) {
return false;
}
}
return $buffer_length;
}
/**
* Processes a received topic
*
* @param $msg
*
* @retrun bool|string
*/
public function message($msg)
{
$tlen = (ord($msg[0]) << 8) + ord($msg[1]);
$topic = substr($msg, 2, $tlen);
$msg = substr($msg, ($tlen + 2));
$found = false;
foreach ($this->topics as $key => $top) {
if (preg_match(
'/^' . str_replace(
'#',
'.*',
str_replace(
'+',
"[^\/]*",
str_replace(
'/',
"\/",
str_replace(
'$',
'\$',
$key
)
)
)
) . '$/',
$topic
)) {
$found = true;
if ($top['function'] === '__direct_return_message__') {
return $msg;
}
if (is_callable($top['function'])) {
call_user_func($top['function'], $topic, $msg);
} else {
$this->_errorMessage('Message received on topic ' . $topic . ' but function is not callable.');
}
}
}
if ($found === false) {
$this->_debugMessage('msg received but no match in subscriptions');
}
return $found;
}
/**
* The processing loop for an "always on" client
* set true when you are doing other stuff in the loop good for
* watching something else at the same time
*
* @param bool $loop
*
* @return bool | string
*/
public function proc(bool $loop = true)
{
if (feof($this->socket)) {
$this->_debugMessage('eof receive going to reconnect for good measure');
fclose($this->socket);
$this->connect_auto(false);
if (count($this->topics)) {
$this->subscribe($this->topics);
}
}
$byte = $this->read(1, true);
if ((string)$byte === '') {
if ($loop === true) {
usleep(100000);
}
} else {
$cmd = (int)(ord($byte) / 16);
$this->_debugMessage(
sprintf(
'Received CMD: %d (%s)',
$cmd,
isset(static::$known_commands[$cmd]) === true ? static::$known_commands[$cmd] : 'Unknown'
)
);
$multiplier = 1;
$value = 0;
do {
$digit = ord($this->read(1));
$value += ($digit & 127) * $multiplier;
$multiplier *= 128;
} while (($digit & 128) !== 0);
$this->_debugMessage('Fetching: ' . $value . ' bytes');
$string = $value > 0 ? $this->read($value) : '';
if ($cmd) {
switch ($cmd) {
case 3: //Publish MSG
$return = $this->message($string);
if (is_bool($return) === false) {
return $return;
}
break;
}
}
}
if ($this->timesinceping < (time() - $this->keepalive)) {
$this->_debugMessage('not had something in a while so ping');
$this->ping();
}
if ($this->timesinceping < (time() - ($this->keepalive * 2))) {
$this->_debugMessage('not seen a packet in a while, disconnecting/reconnecting');
fclose($this->socket);
$this->connect_auto(false);
if (count($this->topics)) {
$this->subscribe($this->topics);
}
}
return true;
}
/**
* Gets the length of a msg, (and increments $i)
*
* @param $msg
* @param $i
*
* @return float|int
*/
protected function getmsglength(&$msg, &$i)
{
$multiplier = 1;
$value = 0;
do {
$digit = ord($msg[$i]);
$value += ($digit & 127) * $multiplier;
$multiplier *= 128;
$i++;
} while (($digit & 128) !== 0);
return $value;
}
/**
* @param $len
*
* @return string
*/
protected function setmsglength($len): string
{
$string = '';
do {
$digit = $len % 128;
$len >>= 7;
// if there are more digits to encode, set the top bit of this digit
if ($len > 0) {
$digit |= 0x80;
}
$string .= chr($digit);
} while ($len > 0);
return $string;
}
/**
* @param $str
* @param $i
*
* @return string
*/
protected function strwritestring($str, &$i): string
{
$len = strlen($str);
$msb = $len >> 8;
$lsb = $len % 256;
$ret = chr($msb);
$ret .= chr($lsb);
$ret .= $str;
$i += ($len + 2);
return $ret;
}
/**
* Prints a sting out character by character
*
* @param $string
*/
public function printstr($string): void
{
$strlen = strlen($string);
for ($j = 0; $j < $strlen; $j++) {
$num = ord($string[$j]);
if ($num > 31) {
$chr = $string[$j];
} else {
$chr = ' ';
}
printf("%4d: %08b : 0x%02x : %s \n", $j, $num, $num, $chr);
}
}
/**
* @param string $message
*/
protected function _debugMessage(string $message): void
{
if ($this->debug === true) {
echo date('r: ') . $message . PHP_EOL;
}
}
/**
* @param string $message
*/
protected function _errorMessage(string $message): void
{
error_log('Error:' . $message);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
require_once("./phpMQTT.php");
require_once("../helper.php");
if (checkLogin()) {
$close = 0;
$hostname = "192.168.179.169";
$server = 'localhost'; // change if necessary
$port = 1883; // change if necessary
$username = ''; // set your username
$password = ''; // set your password
$client_id = 'RoomtempSetter'; // make sure this is unique for connecting to sever - you could use uniqid()
if (isset($_POST["setTemp"]) && isset($_GET["heater"])) {
$topic = "Raumtemp/".str_replace("_","/",$_GET["heater"])."/changeSetTemp";
$mqtt = new Bluerhinos\phpMQTT($server, $port, $client_id);
//send new value
$temp = intval($_POST["setTemp"]);
if($temp > 9 && $temp < 31){
if ($mqtt->connect(true, NULL, $username, $password)) {
$mqtt->publish($topic, $temp, 0, false);
$mqtt->close();
}
}
$close = 1;
}
} else {
$close = 1;
}
if (!$close) {
echo <<<ENDE
<!--begin::Form-->
<form id="heater_form">
<div class="col-sm-11">
<div class="form-check">
<label class="form-label" for="addedCharge">Temperatur: </label>
<div style="font-size: 15px;color: #297195;display: inline-block;" id="modal-slider-label">0</div>
<input type="range" id="modal-slider" name="setTemp" class="form-range range-color-track" min="10" max="30" />
<div class="d-flex justify-content-between">
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">10 °C</span>
<span class="max-amount f-16 font-weight-normal darkGray-color text-right mt-1">30 °C</span>
</div>
</div>
</div>
</form>
ENDE;
}
+69
View File
@@ -0,0 +1,69 @@
<?php
require_once("../helper.php");
if(isset($_GET["sensorID"])){
$sensorID = intval($_GET["sensorID"]);
$qry = "SELECT state_name, url, state_types.type, unit, current_value, possible_values FROM actor_states LEFT JOIN state_types ON state_type = state_types.id WHERE actor_id=".$sensorID;
$mysql = new mysqli($mysql_server, $mysql_MeshUser, $mysql_MeshPass, $mysql_MeshDB);
$result = mysqli_query($mysql, $qry);
$operators = "";
echo '{"parameters": [';
$iterated = 0;
$items = "[]";
while($row = $result->fetch_assoc()){
switch($row["type"]){
case "integer":
$operators = '["=","≠","&gt;","&lt;"]';
$type = "number";
break;
case "float":
$operators = '["=","≠","&gt;","&lt;"]';
$type = "number";
break;
case "time":
$operators = '["=","≠"]';
$type = "time";
break;
case "deltatime":
$operators = '["+","-"]';
$type = "time";
break;
case "date":
$operators = '["=","≠"]';
$type = "date";
break;
case "datetime":
$operators = '["=","≠"]';
$type = "datetime-local";
break;
case "bool":
$operators = '["= JA","= NEIN"]';
$type = "button";
break;
case "string":
$operators = '["=","≠"]';
if($row["possible_values"]){
$items = $row["possible_values"];
$type = "dropdown";
}else{
$operators = '["=","≠"]';
$type = "text";
}
break;
default:
$operators = '["=","≠"]';
$type = "text";
break;
}
if($iterated++){
echo ", ";
}
if($row["current_value"]){
$row["state_name"] .= " (=".$row["current_value"].")";
}
echo '{"name" : "'.$row["state_name"].'", "operators" : '.$operators.', "url" : "'.$row["url"].'", "type" : "'.$type.'", "options" : '.$items.'}';
}
echo "]}";
}
?>
+243
View File
@@ -0,0 +1,243 @@
<?php
set_time_limit(60);
ob_start();
require_once("../restricted/tahoma_EG.php");
//67ebd23e3a61763386d9
function getSSLPage($url, $tahoma_token)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', "Authorization: Bearer ".$tahoma_token));
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
if (curl_errno($ch)) {
$error_msg = curl_error($ch);
//print $error_msg;
}
curl_close($ch);
return $result;
}
function postSSLPage($url, $body, $tahoma_token)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', "Authorization: Bearer ".$tahoma_token));
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
if (curl_errno($ch)) {
$error_msg = curl_error($ch);
//print $error_msg;
}
curl_close($ch);
return $result;
}
function filter_devs($arr,$filter,$prefix=""){
$ret = Array();
$i = 0;
foreach($arr as $key => $dev){
$start = strpos(strtolower($dev["name"]),strtolower($filter));
if($start !== false){
$dev["name"] = str_replace("u_","",$dev["name"]);
$ret[$i]["name"] = $prefix.substr($dev["name"],strlen($filter));
$ret[$i]["id"] = $key;
$i++;
}
}
return $ret;
}
function searchForDevice($device){
$dev = false;
$devices = json_decode(file_get_contents($GLOBALS["tahoma_devlist"]), true);
if (strlen($device) < 3 && intval($device) >= 0 && intval($device) < sizeof($devices)) {
$dev = $devices[intval($device)];
} else if ($key = array_search($device, array_column($devices, "name"))) {
$dev = $devices[$key];
} else if ($key = array_search($device, array_column($devices, "id"))) {
$dev = $devices[$key];
} else {
return false;
}
return $dev;
}
if ($_GET["action"] == "devlist") {
$jalousien = array();
$url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices';
$ret = getSSLPage($url,$tahoma_token);
$devices = json_decode($ret, true);
$i = 0;
foreach ($devices as $device) {
if ($device["controllableName"] == "io:ExteriorVenetianBlindIOComponent") {
$jalousien[$i]["name"] = $device["label"];
$jalousien[$i]["id"] = $device["deviceURL"];
$i = $i + 1;
echo $device["label"] . "(".$device["deviceURL"].")<br />";
}
}
//$file = fopen($tahoma_devlist, 'w');
//fwrite($file, json_encode($jalousien));
//fclose($file);
echo json_encode($jalousien);
}elseif ($_GET["action"] == "pos" && isset($_GET["device"])) {
$dev = searchForDevice($_GET["device"])["id"];
$devices = json_decode(file_get_contents($tahoma_devlist), true);
$jalousien = array();
$url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states";
$ret = getSSLPage($url,$tahoma_token);
$states = json_decode($ret, true);
$out = "{";
foreach ($states as $state){
if ($state["name"] == "core:SlateOrientationState")
$out .= "\"rotation\":".$state["value"].",";
if ($state["name"] == "core:ClosureState")
$out .= "\"position\":".$state["value"].",";
}
$out = substr($out,0,-1);
$out .= "}";
echo $out;
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();
}elseif ($_GET["action"] == "moving" && isset($_GET["device"])) {
$dev = searchForDevice($_GET["device"])["id"];
$devices = json_decode(file_get_contents($tahoma_devlist), true);
$jalousien = array();
$url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState";
$ret = getSSLPage($url,$tahoma_token);
$moving = json_decode($ret, true);
if($moving["value"])
echo "true";
else
echo "false";
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();
}elseif ($_GET["action"] == "move" && isset($_GET["pos"]) && isset($_GET["angle"]) && isset($_GET["device"])) {
$angle = $_GET["angle"];
$pos = $_GET["pos"];
if ($pos < 0 || $pos > 100) {
echo "Position out of Range";
exit;
}
if ($angle < 0 || $angle > 100) {
echo "Angle out of Range";
exit;
}
$dev = searchForDevice($_GET["device"])["id"];
if($dev == false){
exit;
}
header('Connection: close');
header('Content-Length: '.ob_get_length());
$url = $url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/setup/devices/'.urlencode($dev)."/states/core:MovingState";
$ret = json_decode(getSSLPage($url,$tahoma_token),true);
$action = array();
$action["label"] = "myAction";
$action["actions"] = array();
$action["actions"][0]["deviceURL"] = $dev;
$action["actions"][0]["commands"] = array();
if(!isset($ret["value"])){
$ret = json_decode(getSSLPage($url,$tahoma_token),true);
}
if(!isset($ret["value"]) || $ret["value"] == false){
$url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply';
echo "start";
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();
$action["actions"][0]["commands"][0]["name"] = "setClosureAndOrientation";
$action["actions"][0]["commands"][0]["parameters"][0] = intval($pos);
$action["actions"][0]["commands"][0]["parameters"][1] = intval($angle);
$res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token));
if(!isset($res["execId"])){
sleep(1);
$res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try
}
}elseif($ret["value"] == true){
$url = 'https://gateway-'.$tahoma_PIN.':8443/enduser-mobile-web/1/enduserAPI/exec/apply';
echo "stop";
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();
$action["actions"][0]["commands"][0]["name"] = "stop";
$res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token));
if(!isset($res["execId"])){
sleep(1);
$res = json_decode(postSSLPage($url,json_encode($action),$tahoma_token)); //2nd try
}
}
}
if ($_GET["action"] == "myactors") {
if(isset($_GET["filter"])){
$host = $_GET["filter"];
$host = str_replace("-", "", strtolower($host));
}else{
$host = gethostbyaddr("192.168.179.32");
$host = str_replace("-", "", strtolower(substr($host, 0, strpos($host, "."))));
}
$devices = json_decode(file_get_contents($tahoma_devlist), true);
switch ($host) {
case "tmpegbad":
echo json_encode(filter_devs($devices, "bad "));
break;
case "tmpegwozi":
echo json_encode(filter_devs($devices, "wozi "));
break;
case "tmpegflorian": //flori
$ret = filter_devs($devices, "florian ","Flori ");
$ret = array_merge($ret, filter_devs($devices, "magdalena ","Magdalena "));
echo json_encode($ret);
break;
case "tmpegmagdalena":
echo json_encode(filter_devs($devices, "magdalena "));
break;
case "tmpegschlafzimmer":
echo json_encode(filter_devs($devices, "schlafzimmer "));
break;
default:
$ret2=Array();
//$ret2[0]["name"] = $host;
//$ret2[0]["id"] = 99;
$ret = filter_devs($devices, "");
//$ret = array_merge($ret, $ret2);
echo json_encode($ret);
break;
}
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();
//echo postSSLPage($url,json_encode($action));
}
+98
View File
@@ -0,0 +1,98 @@
<?php
require_once("./phpMQTT.php");
require_once("../helper.php");
if ($_SERVER["REQUEST_METHOD"] === "POST") {
header("Content-Type: application/json; charset=utf-8");
if (!checkLogin()) {
http_response_code(401);
echo json_encode(["success" => false, "message" => "Nicht angemeldet."]);
exit;
}
$topic = $_POST["topic"] ?? "";
$payload = $_POST["payload"] ?? "";
$allowedTopics = [
"Gartenwasser/vorn/set",
"Gartenwasser/vorn/auto",
"Gartenwasser/hinten/set",
"Gartenwasser/hinten/auto"
];
if (!in_array($topic, $allowedTopics, true)) {
http_response_code(400);
echo json_encode(["success" => false, "message" => "Nicht erlaubtes MQTT-Topic."]);
exit;
}
if (str_ends_with($topic, "/set")) {
$command = json_decode($payload, true);
$validValve = is_array($command)
&& isset($command["valve"], $command["set"])
&& in_array((int) $command["valve"], [1, 6], true)
&& in_array($command["set"], ["open", "close"], true);
if ($validValve && $command["set"] === "open") {
$validValve = isset($command["timer"])
&& is_numeric($command["timer"])
&& (int) $command["timer"] >= 1
&& (int) $command["timer"] <= 120;
}
if (!$validValve) {
http_response_code(400);
echo json_encode(["success" => false, "message" => "Ungültiger Ventilbefehl."]);
exit;
}
$payload = json_encode($command, JSON_THROW_ON_ERROR);
} elseif (!in_array($payload, ["Hoch", "Vorn", "Trog", "Stop"], true)) {
http_response_code(400);
echo json_encode(["success" => false, "message" => "Ungültiger Automatikbefehl."]);
exit;
}
try {
$mqtt = new Bluerhinos\phpMQTT("localhost", 1883, "WateringWeb-" . bin2hex(random_bytes(4)));
if (!$mqtt->connect(true, null, "", "")) {
http_response_code(503);
echo json_encode(["success" => false, "message" => "MQTT-Broker nicht erreichbar."]);
exit;
}
$mqtt->publish($topic, $payload, 0, false);
$mqtt->close();
} catch (Throwable $error) {
http_response_code(503);
echo json_encode(["success" => false, "message" => "MQTT-Fehler: " . $error->getMessage()]);
exit;
}
echo json_encode(["success" => true, "topic" => $topic, "payload" => $payload]);
exit;
}
if (!checkLogin()) {
exit;
}
echo <<<HTML
<div class="table-responsive p-3">
<div id="watering-feedback" class="alert d-none mb-3" role="status"></div>
<div class="mb-3">
<h6>Automatik</h6>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary" data-watering-auto-toggle data-watering-auto="Trog,Hoch" data-watering-topics="Gartenwasser/vorn,Gartenwasser/hinten" data-watering-label="Tröge und Hochbeete">Tröge und Hochbeete</button>
<button type="button" class="btn btn-outline-primary" data-watering-auto-toggle data-watering-auto="Trog" data-watering-topic="Gartenwasser/vorn" data-watering-label="Tröge">Tröge</button>
<button type="button" class="btn btn-outline-primary" data-watering-auto-toggle data-watering-auto="Hoch" data-watering-topic="Gartenwasser/hinten" data-watering-label="Hochbeete">Hochbeete</button>
<button type="button" class="btn btn-outline-primary" data-watering-auto-toggle data-watering-auto="Vorn" data-watering-topic="Gartenwasser/vorn" data-watering-label="Garten vorn">Garten vorn</button>
</div>
</div>
<h6>Manuell</h6>
<table class="table align-middle mb-3">
<thead><tr><th>Zone</th><th>Dauer (Min.)</th><th>Letzte Bewässerung</th><th>Dauer</th><th>Steuerung</th></tr></thead>
<tbody>
<tr><td>Tröge</td><td><input class="form-control" type="number" min="1" max="120" value="5" data-watering-timer="troughs"></td><td data-watering-last="troughs">--</td><td data-watering-duration="troughs">--</td><td><button type="button" class="btn btn-success" data-watering-toggle data-watering-zone="troughs">Ein</button></td></tr>
<tr><td>Beete</td><td><input class="form-control" type="number" min="1" max="120" value="30" data-watering-timer="raisedBeds"></td><td data-watering-last="raisedBeds">--</td><td data-watering-duration="raisedBeds">--</td><td><button type="button" class="btn btn-success" data-watering-toggle data-watering-zone="raisedBeds">Ein</button></td></tr>
<tr><td>Vorn</td><td><input class="form-control" type="number" min="1" max="120" value="30" data-watering-timer="frontGarden"></td><td data-watering-last="frontGarden">--</td><td data-watering-duration="frontGarden">--</td><td><button type="button" class="btn btn-success" data-watering-toggle data-watering-zone="frontGarden">Ein</button></td></tr>
</tbody>
</table>
</div>
HTML;
+144
View File
@@ -0,0 +1,144 @@
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 200;
src: url(pxiByp8kv8JHgFVrLFj_Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 200;
src: url(pxiByp8kv8JHgFVrLFj_Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 200;
src: url(pxiByp8kv8JHgFVrLFj_Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
src: url(pxiByp8kv8JHgFVrLDz8Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
src: url(pxiByp8kv8JHgFVrLDz8Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 300;
src: url(pxiByp8kv8JHgFVrLDz8Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: url(pxiEyp8kv8JHgFVrJJbecmNE.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: url(pxiEyp8kv8JHgFVrJJnecmNE.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: url(pxiEyp8kv8JHgFVrJJfecg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
src: url(pxiByp8kv8JHgFVrLEj6Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
src: url(pxiByp8kv8JHgFVrLEj6Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
src: url(pxiByp8kv8JHgFVrLEj6Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
src: url(pxiByp8kv8JHgFVrLCz7Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
src: url(pxiByp8kv8JHgFVrLCz7Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
src: url(pxiByp8kv8JHgFVrLCz7Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* devanagari */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 800;
src: url(pxiByp8kv8JHgFVrLDD4Z11lFc-K.woff2) format('woff2');
unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}
/* latin-ext */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 800;
src: url(pxiByp8kv8JHgFVrLDD4Z1JlFc-K.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 800;
src: url(pxiByp8kv8JHgFVrLDD4Z1xlFQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#fff"
class="bi bi-heart-arrow" viewBox="0 0 16 16">
<path
d="M6.707 9h4.364c-.536 1.573 2.028 3.806 4.929-.5-2.9-4.306-5.465-2.073-4.929-.5H6.707L4.854 6.146a.5.5 0 1 0-.708.708L5.293 8h-.586L2.854 6.146a.5.5 0 1 0-.708.708L3.293 8h-.586L.854 6.146a.5.5 0 1 0-.708.708L1.793 8.5.146 10.146a.5.5 0 0 0 .708.708L2.707 9h.586l-1.147 1.146a.5.5 0 0 0 .708.708L4.707 9h.586l-1.147 1.146a.5.5 0 0 0 .708.708z" />
</svg>

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+287
View File
@@ -0,0 +1,287 @@
<svg xmlns="http://www.w3.org/2000/svg" style="border:0px solid gray;background-color: #212529;" viewBox="0 0 630 750" width= "100%" height= "100%">
<g style="fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke:#666666;fill:#666666;" stroke-width="10" transform="scale (2) translate(0 -25)">
<path
id="Puffer"
d="m 227,68 c 0,0 0,-28 -74,-28 -75,0 -75,27 -75,27 V 353 c 0,0 10,31 76,31 66,0 75,-32 75,-32 z"
style="fill:#212529;fill-opacity:1;stroke-width:2;" />
<ellipse
ry="3"
rx="1"
cy="112"
cx="211"
id="path902" />
<ellipse
ry="5"
rx="3"
cy="163"
cx="211"
id="path904" />
<ellipse
ry="5"
rx="3"
cy="282"
cx="210"
id="path904-6" />
<ellipse
ry="5"
rx="3"
cy="345"
cx="213"
id="path904-2" />
<ellipse
ry="3"
rx="1"
cy="332"
cx="213"
id="path902-4" />
<ellipse
ry="3"
rx="1"
cy="121"
cx="100"
id="path902-0" />
<ellipse
ry="3"
rx="1"
cy="164"
cx="99" />
<ellipse
ry="3"
rx="1"
cy="175"
cx="99"
id="path902-8" />
<ellipse
ry="3"
rx="1"
cy="223"
cx="98"
id="path902-2" />
<ellipse
ry="3"
rx="1"
cy="257"
cx="98"
id="path902-07" />
<path
id="heatweRLtube1"
d="m 211,282 h 95 v -44"
style="fill:none;" />
<path
id="path976"
d="m 212,163 h 58"
style="fill:none;" />
<path
id="path978"
d="M 32,140 H 2 v 61 H 32 Z"
style="fill:none;stroke-width:1px;" />
<path
id="path980"
d="M 99,121 H 9 v 18"
style="fill:none;" />
<path
id="path982"
d="M 33,175 H 98"
style="fill:none;" />
<path
id="path984"
d="m 8,201 v 21 H 98"
style="fill:none;" />
<path
id="fbVLtube"
d="m 99,164 h 7 V 244 H 8 v 23"
style="fill:none;" />
<path
id="fbRLtube"
d="m 8,302 v 32 H 107 v -79 h -9"
style="fill:none;" />
<path
id="path990"
d="m 211,112 h 39 V 68"
style="fill:none;" />
<path
id="path992"
d="M 26,151 H 8 v 2 H 6 v 4 l -1,1 v 30 H 28 v -34 l -1,-1 v -4 z"
style="fill:none;stroke-width:1px;" />
<path
id="path994"
d="M 28,167 H 5"
style="fill:none;stroke-width:1px;" />
<path
id="path996"
d="M 5,161 H 28"
style="fill:none;stroke-width:1px;" />
<path
id="path998"
d="M 27,156 H 7"
style="fill:none;stroke-width:1px;s" />
<path
id="path1000"
d="m 10,152 1,2 H 24 l 1,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1002"
d="m 24,167 v 5 h 4"
style="fill:none;stroke-width:1px;" />
<path
id="path1004"
d="M 47,268 H 1 v 34 H 47 v -34 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1006"
d="m 8,269 v 2 c 0,0 -1,0 -1,1 0,1 1,1 1,1 H 40 c 0,0 1,0 1,1 0,1 -1,1 -1,1 H 8 c 0,0 -1,0 -1,1 0,1 1,1 1,1 H 40 c 0,0 1,0 1,1 0,1 -1,1 -1,1 H 8 c 0,0 -1,0 -1,1 0,1 1,1 1,1 H 40 c 0,0 1,0 1,1 0,1 -1,1 -1,1 H 8 c 0,0 -1,0 -1,1 0,1 1,1 1,1 H 40 c 0,0 1,0 1,1 0,1 -1,1 -1,1 H 8 c 0,0 -1,0 -1,1 -0,1 1,1 1,1 h 31 c 0,0 1,0 1,1 0,1 -1,1 -1,1 H 8 v 4"
style="fill:none;stroke-width:1px;" />
<path
id="path1008"
d="M 294,238 Z"
style="fill:none;stroke-width:1px;" />
<path
id="heizerRLtube2"
d="m 295,238 h 10"
style="fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path1012"
d="M 270,163 Z"
style="fill:none;stroke-width:1px;" />
<path
id="heizregler"
d="m 282,247 v 7 c 0,0 0,4 7,4 6,0 7,-4 7,-4 v -7 c 0,0 -0,3 -8,3 -7,0 -6,-3 -6,-3 z"
style="fill:none;stroke-width:1px;" />
<path
id="Heizstab"
d="m 293,246 v -74 c 0,0 -0,-2 -4,-2 -4,0 -4,2 -4,2 v 74 c 0,0 -0,1 4,1 4,-0 4,-1 4,-1 z"
style="fill:#212529;stroke-width:1px;" />
<path
id="path1020"
d="m 270,163 h 19 v 5"
style="fill:none;" />
<path
id="path1022"
d="m 282,247 c 0,0 -0,-2 2,-2"
style="fill:none;stroke-width:1px;" />
<path
id="path1024"
d="m 296,247 c 0,0 0,-2 -3,-3"
style="fill:none;stroke-width:1px;" />
<path
id="path976-2"
d="m 213,332 h 75"
style="fill:none;stroke-width:4;" />
<path
id="path1043"
d="m 240,338 0,-2 -7,3 6,3 -0,-2 4,-0 -0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-9"
d="m 240,169 0,-2 -7,3 6,3 -0,-2 4,-0 -0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-5"
d="m 65,228 0,-2 -7,3 6,3 -0,-2 4,-0 -0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-3"
d="m 65,250 0,-2 -7,3 6,3 -0,-2 4,-0 -0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-94"
d="m 253,288 -0,-2 7,3 -6,3 0,-2 -4,-0 0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-94-5"
d="m 236,103 -0,-2 7,3 -6,3 0,-2 -4,-0 0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-94-2"
d="m 65,342 -0,-2 7,3 -6,3 0,-2 -4,-0 0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-94-6"
d="m 65,126 -0,-2 7,3 -6,3 0,-2 -4,-0 0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1043-94-9"
d="m 65,180 -0,-2 7,3 -6,3 0,-2 -4,-0 0,-2 z"
style="fill:none;stroke-width:1px;" />
<path
id="path1123"
d="m 253,52 h 9 c 0,0 3,-0 5,1 1,1 1,5 1,5 l -0,3 c 0,0 2,0 3,1 1,1 1,3 1,3 h -11 c 0,0 -0,-2 1,-4 1,-1 2,-1 2,-1 v -3 c 0,0 0,-2 -1,-3 -1,-0 -2,-0 -2,-0 h -9"
style="fill:none;stroke-width:1px;" />
<path
id="path1125"
d="m 266,68 -0,2"
style="fill:none;stroke-width:1px;" />
<path
id="path1127"
d="m 266,73 v 1"
style="fill:none;stroke-width:1px;" />
<path
id="path1129"
d="m 268,68 v 2"
style="fill:none;stroke-width:1px;" />
<path
id="path1131"
d="m 268,73 v 1"
style="fill:none;stroke-width:1px;" />
<path
id="path1133"
d="m 271,69 v 2"
style="fill:none;stroke-width:1px;" />
<path
id="path1135"
d="M 271,74 V 75"
style="fill:none;stroke-width:1px;" />
<path
id="path1137"
d="m 285,314 h -4 v 12 h 4"
style="fill:none;stroke-width:1px;" />
<path
id="path1139"
d="m 280,318 h -4 v -5 h -4 v 5 h -4 c 0,0 -1,-0 -2,1 -0,1 -0,4 -0,4 0,0 -0,3 2,3 2,0 2,-2 2,-3 0,-1 2,-1 2,-1 l 9,0"
style="fill:none;stroke-width:1px;" />
<path
id="path1141"
d="m 278,312 h -9 c -1,0 -2,-1 0,-1 2,0 2,1 4,1 2,0 3,-1 5,-1 2,0 0,1 -0,1 z"
style="fill:none;stroke-width:1px;" />
</g>
<g style="font-size: 28px; font-weight: 600; font-family: sans-serif; text-anchor: middle; fill: rgb(200, 210, 210); letter-spacing: 1px;" >
<text id="PufferOtxt" dy="200" dx="320">
--.- °C
</text>
<text id="PufferMtxt" dy="400" dx="320">
--.- °C
</text>
<text id="PufferUtxt" dy="600" dx="320">
--.- °C
</text>
<text id="heaterRL" dy="505" dx="530" style="font-size: 22px;">
--.- °C
</text>
<text id="heaterVL" dy="260" dx="530" style="font-size: 22px;">
--.- °C
</text>
<text id="fbVL" dy="465" dx="70" style="font-size: 22px;">
--.- °C
</text>
<text id="fbRL" dy="605" dx="70" style="font-size: 22px;">
--.- °C
</text>
<text id="thermeRL" dy="385" dx="70" style="font-size: 22px;">
--.- °C
</text>
<text id="thermeVLfb" dy="290" dx="110" style="font-size: 22px;">
--.- °C
</text>
<text id="thermeVLww" dy="180" dx="70" style="font-size: 22px;">
--.- °C
</text>
<text id="triacLbl" dy="350" dx="515" style="font-size: 22px;">
Triac:
</text>
<text id="triac" dy="375" dx="515" style="font-size: 22px;">
--.- °C
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

+893
View File
@@ -0,0 +1,893 @@
<style>
/* Fluss-Animation als transform statt stroke-dashoffset.
stroke-dashoffset ist nicht kompositierbar - jede Stufe kostete einen
Neuzeichenvorgang auf dem Hauptthread. Ein transform kann der Compositor
dagegen selbst verschieben.
Die Punkte laufen eine Periode (111 Einheiten) weit, danach steht der
naechste an derselben Stelle - der Zyklus schliesst nahtlos.
--dx/--dy stehen pro Linie im style-Attribut, --w setzt setFlowWidth().
steps(40) = 20 Bewegungen pro Sekunde, wie in der urspruenglichen
Fassung. Das deckelt die Neuzeichnungen fuer den Fall, dass der Browser
das transform doch nicht kompositiert; auf dem Compositor kostet die
Stufung nichts. Fuer volle Fluessigkeit: linear statt steps(40). */
.stream, .stream-rev {
animation: flow 2s steps(40) infinite;
}
.stream-rev {
animation-direction: reverse;
}
@keyframes flow {
to { transform: translate(var(--dx), var(--dy)); }
}
.stream circle, .stream-rev circle {
r: calc(var(--w, 0px) / 2);
fill: gray;
}
.powerTextClass {
font-size: 27px;
font-family: sans-serif;
text-anchor: middle;
fill: rgb(200, 210, 210);
letter-spacing: 0px;
}
.infoTextClass {
font-size: 18px;
font-family: sans-serif;
text-anchor: middle;
fill: rgb(200, 210, 210);
}
.tooltipTextClass {
font-size: 18px;
font-family: sans-serif;
text-anchor: left;
fill: rgb(200, 210, 210);
}
.clickable {
cursor: pointer;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="-100 -100 1600 950" width="100%" height="100%" id="realtimeSvgAni">
<defs>
<filter
id="glow" filterUnits="userSpaceOnUse"
x="-20%" y="-20%" width="120%" height="120%">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur5" />
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur10" />
<feGaussianBlur in="SourceGraphic" stdDeviation="14" result="blur20" />
<feGaussianBlur in="SourceGraphic" stdDeviation="17" result="blur30" />
<feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blur50" />
<feMerge result="blur-merged">
<feMergeNode in="blur5" />
<feMergeNode in="blur10" />
<feMergeNode in="blur10" />
<feMergeNode in="blur20" />
</feMerge>
<feMerge>
<feMergeNode in="blur-merged" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<clipPath id="clip_consOGani"><rect x="-30" y="-30" width="404.4" height="60" transform="translate(770.0,380.0) rotate(-64.1790)"/></clipPath>
<clipPath id="clip_consAni"><rect x="-30" y="-30" width="400.0" height="60" transform="translate(430.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEGani"><rect x="-30" y="-30" width="386.5" height="60" transform="translate(770.0,380.0) rotate(62.6501)"/></clipPath>
<clipPath id="clip_consUGani"><rect x="-30" y="-30" width="396.2" height="60" transform="translate(770.0,380.0) rotate(120.3791)"/></clipPath>
<clipPath id="clip_consHeatAni"><rect x="-30" y="-30" width="410.0" height="60" transform="translate(770.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEVani"><rect x="-30" y="-30" width="360.0" height="60" transform="translate(920.0,670.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consEVOGani"><rect x="-30" y="-30" width="360.0" height="60" transform="translate(920.0,70.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_consGridani"><rect x="-30" y="-30" width="390.0" height="60" transform="translate(100.0,380.0) rotate(0.0000)"/></clipPath>
<clipPath id="clip_pvani"><rect x="-30" y="-30" width="413.6" height="60" transform="translate(180.0,130.0) rotate(45.0000)"/></clipPath>
<clipPath id="clip_battani"><rect x="-30" y="-30" width="413.6" height="60" transform="translate(180.0,630.0) rotate(-45.0000)"/></clipPath>
</defs>
<g opacity="0">
<animate begin="lastInitAni.end" attributetype="CSS" attributeName="opacity" from="0.0" to="1.0"
dur="0.5s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animate begin="pvInfo.click" attributetype="CSS" attributeName="opacity" from="1.0" to="0.0"
dur="1s"
repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="0.0"
to="1.0"
dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<!-- CONS pos: 330, 280
+340, 0 (center +100 in x und y)
Cons OG pos: 330,280
+500 -300 (center +90 in x und y)
-->
<line x1="770" y1="380" x2="920" y2="70" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consOGani)"><g id="consOGani" class="stream" style="--dx:48.347px;--dy:-99.918px"><circle cx="721.7" cy="479.9"/><circle cx="770.0" cy="380.0"/><circle cx="818.3" cy="280.1"/><circle cx="866.7" cy="180.2"/><circle cx="915.0" cy="80.2"/><circle cx="963.4" cy="-19.7"/></g></g>
<line x1="430" y1="380" x2="770" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consAni)"><g id="consAni" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="319.0" cy="380.0"/><circle cx="430.0" cy="380.0"/><circle cx="541.0" cy="380.0"/><circle cx="652.0" cy="380.0"/><circle cx="763.0" cy="380.0"/><circle cx="874.0" cy="380.0"/></g></g>
<line x1="770" y1="380" x2="920" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEGani)"><g id="consEGani" class="stream" style="--dx:50.996px;--dy:98.592px"><circle cx="719.0" cy="281.4"/><circle cx="770.0" cy="380.0"/><circle cx="821.0" cy="478.6"/><circle cx="872.0" cy="577.2"/><circle cx="923.0" cy="675.8"/></g></g>
<line x1="770" y1="380" x2="600" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consUGani)"><g id="consUGani" class="stream" style="--dx:-56.135px;--dy:95.759px"><circle cx="826.1" cy="284.2"/><circle cx="770.0" cy="380.0"/><circle cx="713.9" cy="475.8"/><circle cx="657.7" cy="571.5"/><circle cx="601.6" cy="667.3"/><circle cx="545.5" cy="763.0"/></g></g>
<line x1="770" y1="380" x2="1120" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consHeatAni)"><g id="consHeatAni" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="659.0" cy="380.0"/><circle cx="770.0" cy="380.0"/><circle cx="881.0" cy="380.0"/><circle cx="992.0" cy="380.0"/><circle cx="1103.0" cy="380.0"/><circle cx="1214.0" cy="380.0"/></g></g>
<line x1="920" y1="670" x2="1220" y2="670" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEVani)"><g id="consEVani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="809.0" cy="670.0"/><circle cx="920.0" cy="670.0"/><circle cx="1031.0" cy="670.0"/><circle cx="1142.0" cy="670.0"/><circle cx="1253.0" cy="670.0"/></g></g>
<line x1="920" y1="70" x2="1220" y2="70" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consEVOGani)"><g id="consEVOGani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="809.0" cy="70.0"/><circle cx="920.0" cy="70.0"/><circle cx="1031.0" cy="70.0"/><circle cx="1142.0" cy="70.0"/><circle cx="1253.0" cy="70.0"/></g></g>
<line x1="100" y1="380" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_consGridani)"><g id="consGridani" class="stream" style="--dx:111.000px;--dy:0.000px"><circle cx="-11.0" cy="380.0"/><circle cx="100.0" cy="380.0"/><circle cx="211.0" cy="380.0"/><circle cx="322.0" cy="380.0"/><circle cx="433.0" cy="380.0"/></g></g>
<line x1="180" y1="130" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_pvani)"><g id="pvani" class="stream" style="--dx:78.489px;--dy:78.489px"><circle cx="101.5" cy="51.5"/><circle cx="180.0" cy="130.0"/><circle cx="258.5" cy="208.5"/><circle cx="337.0" cy="287.0"/><circle cx="415.5" cy="365.5"/><circle cx="494.0" cy="444.0"/></g></g>
<line x1="180" y1="630" x2="430" y2="380" style="stroke:#777;stroke-width:2;stroke-dasharray:1,6;" />
<g clip-path="url(#clip_battani)"><g id="battani" class="stream" style="--dx:78.489px;--dy:-78.489px"><circle cx="101.5" cy="708.5"/><circle cx="180.0" cy="630.0"/><circle cx="258.5" cy="551.5"/><circle cx="337.0" cy="473.0"/><circle cx="415.5" cy="394.5"/><circle cx="494.0" cy="316.0"/></g></g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg"
style="position:absolute;top:0;"
viewBox="-100 -100 1600 950" width="100%" height="100%" id="realtimeSvg">
<defs>
<filter id="shadow" x="-0.3" y="-0.3" width="160%" height="160%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="0.7" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="0.5" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<filter id="shadowMiddle" x="-0.4" y="-0.4" width="180%" height="180%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="1" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<filter id="shadowBig" x="-0.45" y="-0.45" width="190%" height="190%">
<feMorphology in="SourceAlpha" result="offOut" operator="dilate" radius="3" />
<feGaussianBlur result="offOut" in="offOut" stdDeviation="2" />
<feBlend in="SourceGraphic" in2="offOut" mode="normal" />
</filter>
<g id="circGrid">
<line x2='27.7' y2='76.5' x1='-18.9' y1='61.4' style='stroke-width:4px' />
<line x2='16.5' y2='115.9' x1='-8.1' y1='120.6' style='stroke:#222;stroke-width:2;' />
<line x2='34.5' y2='154.2' x1='15.2' y1='170.1' style='stroke:#222;stroke-width:2;' />
<line x2='68.7' y2='179' x1='59.5' y1='202.3' style='stroke:#222;stroke-width:2;' />
<line x2='110.7' y2='184.3' x1='113.8' y1='209.1' style='stroke:#222;stroke-width:2;' />
<line x2='150' y2='168.8' x1='164.7' y1='189' style='stroke:#222;stroke-width:2;' />
<line x2='176.9' y2='136.2' x1='199.5' y1='146.8' style='stroke:#222;stroke-width:2;' />
<line x2='184.8' y2='94.7' x1='209.8' y1='93.1' style='stroke:#222;stroke-width:2;' />
<line x2='171.8' y2='54.5' x1='192.9' y1='41.1' style='stroke:#222;stroke-width:2;' />
<line x2='140.9' y2='25.5' x1='153' y1='3.6' style='stroke:#222;stroke-width:2;' />
<line x2='100' y2='24' x1='100' y1='-24' style='stroke-width:4px' />
</g>
<g id="circGrid6kW">
<line x2='27.7' y2='76.5' x1='-18.9' y1='61.4' style='stroke-width:4px' />
<line x2='26.4' y2='142.5' x1='4.7' y1='155' style='stroke:#222;stroke-width:2;' />
<line x2='82.3' y2='183.1' x1='77.1' y1='207.6' style='stroke:#555;stroke-width:4px;' />
<line x2='149.9' y2='168.8' x1='164.6' y1='189.0' style='stroke:#222;stroke-width:2;' />
<line x2='184.5' y2='108.9' x1='209.4' y1='111.5' style='stroke:#555;stroke-width:4px;' />
<line x2='163.2' y2='43.1' x1='181.7' y1='26.4' style='stroke:#222;stroke-width:2;' />
<line x2='100' y2='24' x1='100' y1='-24' style='stroke-width:4px' />
</g>
<linearGradient id="gradient1" x1="0" x2="0" y1="0" y2="1">
<stop id="tmpH" offset="0%" stop-color="#2389BA" stop-opacity="0.7" />
<stop id="tmpM" offset="50%" stop-color="#2389BA" stop-opacity="0.7" />
<stop id="tmpL" offset="100%" stop-color="#2389BA" stop-opacity="0.7" />
</linearGradient>
<filter id="fillpartialEV" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#1f5a66" />
<feComposite id="evChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialHeat" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#5E2B21" />
<feComposite id="heatChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialBat" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#25481C" />
<feComposite id="batChargeState" y="100%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<filter id="fillpartialWater" primitiveUnits="objectBoundingBox" x="-5%" y="-5%" width="110%"
height="110%">
<feFlood flood-color="#182C43" />
<feComposite id="waterState" y="50%" operator="in" in2="SourceGraphic" />
<feComposite operator="over" in2="SourceGraphic" />
</filter>
<g id="heater" transform="rotate(-90),scale(0.07,0.07)">
<path
d="M485.924,134.508c14.381,0,26.076-11.669,26.076-26.017v-17.39c0-14.348-11.695-26.017-26.076-26.017H468.61v-8.737
c0-23.89-19.466-43.331-43.39-43.331s-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331
c-23.924,0-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331
c-23.924,0-43.39,19.441-43.39,43.331v8.737h-26.034v-8.737c0-23.89-19.466-43.331-43.39-43.331S43.39,32.458,43.39,56.348v8.737
H17.356V39.051H0v34.712v52.068v34.712h17.356v-26.034H43.39v269.017H8.678c-4.797,0-8.678,3.881-8.678,8.678v34.712
c0,4.797,3.881,8.678,8.678,8.678c4.797,0,8.678-3.881,8.678-8.678v-26.034H43.39v34.78c0,23.89,19.466,43.322,43.39,43.322
s43.39-19.432,43.39-43.322v-0.068h26.034v0.068c0,23.89,19.466,43.322,43.39,43.322c23.924,0,43.39-19.432,43.39-43.322v-0.068
h26.034v0.068c0,23.89,19.466,43.322,43.39,43.322c23.924,0,43.39-19.432,43.39-43.322v-0.068h26.034v0.068
c0,23.89,19.466,43.322,43.39,43.322s43.39-19.432,43.39-43.322v-0.068h17.314c14.381,0,26.076-11.669,26.076-26.017v-17.39
c0-14.347-11.695-26.017-26.076-26.017H468.61V134.508H485.924z M468.61,82.441h17.314c4.805,0,8.72,3.881,8.72,8.661v17.39
c0,4.78-3.915,8.661-8.72,8.661H468.61V82.441z M43.39,117.153H17.356V82.441H43.39V117.153z M112.814,73.763v52.068v269.017
v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M156.203,438.237h-26.034v-34.712h26.034V438.237z
M156.203,386.169h-26.034V134.508h26.034V386.169z M156.203,117.153h-26.034V82.441h26.034V117.153z M225.627,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M269.017,438.237h-26.034v-34.712h26.034V438.237z
M269.017,386.169h-26.034V134.508h26.034V386.169z M269.017,117.153h-26.034V82.441h26.034V117.153z M338.441,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M381.831,438.237h-26.034v-34.712h26.034V438.237z
M381.831,386.169h-26.034V134.508h26.034V386.169z M381.831,117.153h-26.034V82.441h26.034V117.153z M451.254,73.763v52.068
v269.017v52.068v8.746c0,14.313-11.678,25.966-26.034,25.966s-26.034-11.653-26.034-25.966v-8.746v-52.068V125.831V73.763V56.348
c0-14.322,11.678-25.975,26.034-25.975s26.034,11.653,26.034,25.975V73.763z M485.924,403.525c4.805,0,8.72,3.881,8.72,8.661
v17.39c0,4.78-3.915,8.661-8.72,8.661H468.61v-34.712H485.924z" />
</g>
<g id="gas">
<path fill-opacity="1" stroke-width="0.2" stroke-linejoin="round"
d="M 42.75,19L 42.75,31.4977C 44.5834,31.378 48.9184,32.7881 48.2917,49.0833C 49.0833,53.8333 54.625,53.8333 55.8125,47.5C 57,42.75 53.8333,42.75 52.25,28.5C 52.25,26.9167 52.25,22.1667 45.9167,19L 45.9167,17.4167C 52.25,17.4167 57,25.3333 57,26.9167C 57,28.5 55.4167,28.5 55.4167,28.5C 55.4167,28.5 55.8125,38 57.3958,39.5833C 58.9792,41.1667 58.5833,45.9167 58.5833,49.0833C 58.5833,52.25 53.8333,55.4167 52.25,55.4167C 50.6667,55.4167 45.9167,55.4167 45.9167,50.6667C 45.9167,46.3403 45.2599,36.7598 42.75,35.0854L 42.75,55.4167C 43.6244,55.4167 44.3333,56.1255 44.3333,57L 44.3333,60.1667L 19,60.1667L 19,57C 19,56.1255 19.7089,55.4167 20.5833,55.4167L 20.5833,19C 20.5833,17.2511 22.0011,15.8333 23.75,15.8333L 39.5833,15.8333C 41.3322,15.8333 42.75,17.2511 42.75,19 Z M 25.7291,33.2501L 37.6041,33.25C 38.4786,33.25 39.5833,32.1453 39.5833,31.2709L 39.5833,20.9792C 39.5833,20.1048 38.4786,19 37.6041,19L 25.7292,19C 24.8547,19 23.75,20.1048 23.75,20.9792L 23.75,31.2709C 23.75,32.1453 24.8547,33.2501 25.7291,33.2501 Z " />
</g>
<g id="water">
<path
d="M283.897,92.846c-36.582-49.345-73.688-89.267-74.061-89.664C207.944,1.153,205.296,0,202.523,0
c-2.774,0-5.423,1.152-7.314,3.182c-0.371,0.397-37.478,40.319-74.06,89.664c-49.971,67.403-75.308,119.726-75.308,155.513
c0,86.396,70.287,156.688,156.682,156.688c86.396,0,156.683-70.29,156.683-156.688C359.206,212.572,333.868,160.25,283.897,92.846z
M218.171,354.342c-8.213,1.941-16.68,2.926-25.162,2.926c-60.294,0-109.347-49.055-109.347-109.35
c0-8.312,2.559-23.373,14.75-47.914c1.225-2.467,4.046-3.691,6.687-2.908c2.639,0.785,4.33,3.357,4.007,6.091
c-0.28,2.361-0.421,4.584-0.421,6.607c0,64.629,45.966,120.77,109.298,133.484c2.607,0.525,4.5,2.795,4.545,5.455
C222.575,351.396,220.761,353.729,218.171,354.342z" />
</g>
<g id="raisedBedIcon" filter="url(#shadow)">
<path d="M2 8h20v12H2V8Z" fill="none" stroke="currentColor" stroke-width="1.8" />
<path d="M4 12h16M5 16h14" fill="none" stroke="currentColor" stroke-width="1.4" />
<path d="M7 8V4m0 2L5 4m2 2 2-2M13 8V3m0 3-2-2m2 2 2-2M18 8V4m0 2-2-2m2 2 2-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</g>
<g id="flowerPotIcon" filter="url(#shadow)">
<path d="M3 8h18l-2 4H5L3 8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M6 12h12l-2 8H8l-2-8Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M12 8V3m0 3L9 4m3 2 3-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
</g>
<g id="gardenPlantIcon" filter="url(#shadow)">
<path d="M12 19V10m0 4-4-3m4 1 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 3C9 3 7 5 7 8c-3 0-5 2-5 5s2 5 5 5h10c3 0 5-2 5-5s-2-5-5-5c0-3-2-5-5-5Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
<path d="M5 22h14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</g>
<g id="voltage">
<path stroke="#212529" stroke-width="0.2px" d="M11.46,18V13.58H9.86L12.54,8v3.89h1.6Z" />
</g>
<path id="powerTextPath" d="M 100, 100 m -87, 0 a 87,87 0 1,1 174,0 a 87,87 0 1,1 -174,0"></path>
<path id="infoTextPath" d="M 100, 100 m -105, 0 a 105,105 0 1,0 210,0 a 105,105 0 1,0 -210,0"></path>
<g id="pvPlate" fill="none" stroke-width="4" stroke="#f7c000">
<rect width="70" height="40" x="0" y="0" rx="5" ry="5" />
</g>
<g id="ElementBG">
<path fill="#212529" stroke-width="1.5"
d="M 99.80277789465349 -12.999827891201505 A 113 113 0 1 0 100 -13" />
<path fill="none" stroke-width="4"
d="M 99.86386438745993 22.00011880076356 A 78 78 0 1 0 100 22" />
</g>
<g id="plugIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)" fill="#71afcd">
<path d="M15.21,6.73V2.63A.72.72,0,0,0,14.4,2h0a.71.71,0,0,0-.75.66V6.73Z" />
<path d="M10.39,6.73V2.62A.7.7,0,0,0,9.6,2h0a.73.73,0,0,0-.78.66V6.73Z" />
<path
d="M6.71,7.23C6.37,7.22,6,8,6,8.8V10c0,2.48.57,4.62,1.63,5.28S9,16,10.35,16.77V22h3.3V16.77c1.38-.8,1.66-.85,2.72-1.51S18,12.46,18,10V8.8c0-.78-.37-1.58-.71-1.57Z" />
</g>
<g id="ogIcon" filter="url(#shadow)" transform="translate(52,45),scale(4,4)">
<polygon fill="#70CCBB"
points="12.01 1.5 2 10.32 3.11 10.32 3.11 21.5 5.12 21.5 5.12 10.32 12 4.13 18.89 10.32 18.89 21.5 20.89 21.5 20.89 10.32 22 10.32 12.01 1.5" />
<path fill="#70CCBB"
d="M8.55,12.65v3.49a2.12,2.12,0,0,0,2.13,2.09h.67s0,3.23,0,3.27h1.49s0-3.24,0-3.28h.77a1.82,1.82,0,0,0,1.8-1.85l0-3.73" />
<path fill="#70CCBB" d="M10.76,11.41v-1.3c0-.45-.17-.67-.52-.68s-.54.22-.54.69v1.67h1.06Z" />
<path fill="#70CCBB"
d="M14.3,11.41c0-.44,0-.88,0-1.3s-.17-.67-.51-.68-.55.22-.55.69v1.67H14.3Z" />
<path fill="#212529"
d="M9.86,13.86h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,13.86Z" />
</g>
<g id="heatIcon" filter="url(#shadow)" transform="translate(52,70),scale(4,4)" fill="#C2614E">
<path clip-path="M-22 2.24h42V22h-42z"
d="M16.543 8.028c-.023 1.503-.523 3.538-2.867 4.327.734-1.746.846-3.417.326-4.979-.695-2.097-3.014-3.735-4.557-4.627-.527-.306-1.203.074-1.193.683.02 1.112-.318 2.737-1.959 4.378C4.107 9.994 3 12.251 3 14.517 3 17.362 5 21 9 21c-4.041-4.041-1-7.483-1-7.483C8.846 19.431 12.988 21 15 21c1.711 0 5-1.25 5-6.448 0-3.133-1.332-5.511-2.385-6.899-.347-.458-1.064-.198-1.072.375" />
</g>
<g id="egIcon" filter="url(#shadow)" transform="translate(52,50),scale(4,4)">
<rect stroke="#70CC9C" stroke-width="2" fill="none" x="3" y="3" width="18" height="18" rx="2" />
<path fill="#70CC9C"
d="M8.55,9.2v3.47a2.11,2.11,0,0,0,2.13,2.08h.67s0,3.22,0,3.26h1.49s0-3.22,0-3.26h.77a1.81,1.81,0,0,0,1.8-1.84l0-3.71" />
<path fill="#70CC9C" d="M10.76,8V6.67c0-.45-.17-.66-.52-.67s-.54.22-.54.68V8.34h1.06Z" />
<path fill="#70CC9C" d="M14.3,8c0-.44,0-.87,0-1.3S14.12,6,13.78,6s-.55.22-.55.68V8.34H14.3Z" />
<path fill="#212529"
d="M9.86,10.41h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,10.41Z" />
</g>
<g id="ugIcon" filter="url(#shadow)" transform="translate(52,50),scale(4,4)">
<path
style="fill:none;stroke:#83a55b;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
d="M 2.977125,13.982235 V 2.973455 h 17.952227 l 0.03597,16.261337"
id="path12" />
<path fill="#83A55B"
d="M8.55,9.2v3.47a2.11,2.11,0,0,0,2.13,2.08h.67s0,3.22,0,3.26h1.49s0-3.22,0-3.26h.77a1.81,1.81,0,0,0,1.8-1.84l0-3.71" />
<path fill="#83A55B" d="M10.76,8V6.67c0-.45-.17-.66-.52-.67s-.54.22-.54.68V8.34h1.06Z" />
<path fill="#83A55B" d="M14.3,8c0-.44,0-.87,0-1.3S14.12,6,13.78,6s-.55.22-.55.68V8.34H14.3Z" />
<path fill="#212529"
d="M9.86,10.41h4.28a0,0,0,0,1,0,0v2a1,1,0,0,1-1,1H10.86a1,1,0,0,1-1-1v-2A0,0,0,0,1,9.86,10.41Z" />
<path
style="fill:none;stroke:#83a55b;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"
d="M 23.96101,19.98372 C 11.24143,20.28899 11.139675,15.0994 -3.106255,14.081835" />
</g>
<g id="carIcon" filter="url(#shadow)" transform="translate(45,45),scale(4.8,4.8)"
fill="#212529" stroke-linecap="round">
<path d="M16.22,15.37l-9.3-.16L3,15,.88,14.47.75,10.39A2.78,2.78,0,0,1,1.31,9.2L2.55,7.63" />
<path
d="M.78,7.46,7,7l.87,0L11.37,7a5.57,5.57,0,0,1,1.06.23,9.43,9.43,0,0,1,2.16,1L17.23,9.9l4.22.7a1.73,1.73,0,0,1,1.21.93l.26.49a2.84,2.84,0,0,1,.32,1.6l0,.23A1.52,1.52,0,0,1,22.53,15h0a1.53,1.53,0,0,1-.56.21l-.89.15H15.59" />
<path fill="#888888" stroke="none"
d="M16.06,11c-1.85.11-3.7.14-5.55.16a13.36,13.36,0,0,1-2.82,0,2.62,2.62,0,0,1-.87-.4,1.28,1.28,0,0,1-.53-1.05,1.4,1.4,0,0,1,.09-.51,2.67,2.67,0,0,1,.22-.35,2.79,2.79,0,0,1,.5-.58s.09,0,.11,0a.06.06,0,0,1,0,.05h0c0,.25,0,.49,0,.71s0,.23,0,.31a.75.75,0,0,0,.08.18c.07.09.17.14.17.08a1.81,1.81,0,0,0,.43.05,14.41,14.41,0,0,1,2.68.33c1.82.26,3.64.53,5.46.88a.08.08,0,0,1,.06.09A.07.07,0,0,1,16.06,11Z" />
<circle cx="4.94" cy="15.08" r="1.98" />
<circle cx="19.19" cy="15.08" r="1.98" />
</g>
<g id="plugCordIcon" transform="scale(0.08,0.08),rotate(340),translate(-350,1200)">
<path
d="M489.8,280.92c0-13.9-11.3-25.2-25.2-25.2h-74.1c-62.1,0-112.4,50.2-112.7,112.2c-0.1,32.3-23,61-55,65
c-38.4,4.8-71.2-25.2-71.2-62.6v-56.4c57.6-11.7,101-62.6,101-123.7v-64c0-9.1-7.3-16.4-16.4-16.4h-31.3v-79.1
c0-12.4-9-23.4-21.3-24.6c-14.1-1.4-26,9.7-26,23.6v80.1H95.2v-79.1c0-12.4-9-23.4-21.4-24.6c-14.1-1.3-26,9.7-26,23.6v80.1H16.4
c-9.1,0-16.4,7.3-16.4,16.4v64c0,61.1,43.4,112,101,123.7v53.5c0,61.5,47.7,114,109.2,116.3c64.6,2.4,118-49.4,118-113.5v-0.9
c0-34.9,28.3-63.1,63.1-63.1h73.2C478.5,306.12,489.8,294.82,489.8,280.92L489.8,280.92z" />
</g>
<g id="lockIcon" transform="translate(80,130),scale(0.11,0.11)" style="">
<path
d="M65,330h200c8.284,0,15-6.716,15-15V145c0-8.284-6.716-15-15-15h-15V85c0-46.869-38.131-85-85-85
S80,38.131,80,85v45H65c-8.284,0-15,6.716-15,15v170C50,323.284,56.716,330,65,330z M207.481,219.356l-42.5,42.5
c-2.929,2.929-6.768,4.394-10.606,4.394s-7.678-1.465-10.606-4.394l-21.25-21.25c-5.858-5.858-5.858-15.354,0-21.213
c5.857-5.858,15.355-5.858,21.213,0l10.644,10.643l31.894-31.893c5.857-5.858,15.355-5.858,21.213,0
C213.34,204.002,213.34,213.498,207.481,219.356z M110,85c0-30.327,24.673-55,55-55s55,24.673,55,55v45H110V85z"
style="stroke-width:15px;stroke:rgb(30,30,40);fill:#778;" />
</g>
<g filter="url(#shadow)" id="clock" style="fill:#cc0;"
transform="translate(80,32),scale(0.07,0.07)">
<path
d="m256,51.8c-112.6,0-204.2,91.6-204.2,204.2s91.6,204.2 204.2,204.2 204.2-91.6 204.2-204.2-91.6-204.2-204.2-204.2v-1.42109e-14zm0,449.2c-135.1,0-245-109.9-245-245s109.9-245 245-245 245,109.9 245,245-109.9,245-245,245z" />
<path
d="m327.9,276.4h-71.9c-11.3,0-20.4-9.1-20.4-20.4v-132.2c0-11.3 9.1-20.4 20.4-20.4 11.3,0 20.4,9.1 20.4,20.4v111.8h51.5c11.3,0 20.4,9.1 20.4,20.4s-9.1,20.4-20.4,20.4z" />
</g>
<g id="sunIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)"
style="stroke:#f7c000;stroke-width:2;stroke-linecap:round;">
<path id="Pfad_7565" data-name="Pfad 7565" fill="#f7c002" stroke-width="0"
d="M12,8a4,4,0,1,0,4,4A4,4,0,0,0,12,8Z" />
<line x1="12" y1="3" x2="12" y2="5" />
<line x1="12" y1="19" x2="12" y2="21" />
<line x1="19" y1="12" x2="21" y2="12" />
<line x1="3" y1="12" x2="5" y2="12" />
<line x1="5.7" y1="18.5" x2="7" y2="17" />
<line x1="5.7" y1="5.7" x2="7" y2="7" />
<line x1="18.5" y1="18.5" x2="16.7" y2="17" />
<line x1="18.5" y1="5.7" x2="16.7" y2="7" />
</g>
<g id="battIcon" filter="url(#shadow)" transform="translate(30,25),scale(6,6)">
<g>
<rect fill="#6cbe58" x="9.25" y="3" width="5.5" height="4" rx="1" ry="1" />
<path fill="#6cbe58"
d="M16.28,5H7.72A1.21,1.21,0,0,0,6.5,6.2V19.73A1.28,1.28,0,0,0,7.72,21h8.56a1.28,1.28,0,0,0,1.22-1.27V6.2A1.21,1.21,0,0,0,16.28,5Z" />
<rect id="discharged" fill="#212529" rx="1" ry="1" x="8" y="6.5" width="8"
height="13" />
<use id="charge" fill="#cccc00" href="#voltage" />
</g>
</g>
<g id="gridIcon" filter="url(#shadow)" transform="translate(30,27),scale(6,6)">
<path fill="#808090"
d="M13.38,8.54V6.63H18.3V8.06h.95V5.7l-6.35-1L12,2l-1,2.8-6.25.9V8.07H5.6V6.63h4.84V8.54l-7.59,1.9v2.84H3.8V11.76H9.87L6.93,22H9.11l.57-2.46h4.59L14.8,22h2.27L14.13,11.76H20.2v1.52h.95V10.44Zm-3.32,8.81L12,11.11l1.91,6.24Z" />
</g>
<g id="inverterIcon" filter="url(#shadowBig)" fill="#b2b2b2" stroke-width="2px" stroke="#666"
transform="translate(43,43),scale(1.6,1.6)">
<rect stroke-width="2px" x="5.65" y="3" width="58.7" height="64" rx="6.8" />
<circle cx="35.03" cy="29.61" r="11" />
<circle cx="15" cy="40" r="4" />
<path
d="M40.19,52H36.73a2,2,0,0,0-3.46,0H29.81a1.08,1.08,0,1,0,0,2.15h3.46a2,2,0,0,0,3.46,0h3.46a1.08,1.08,0,1,0,0-2.15Z" />
<path fill="#e2001a"
d="M34.89,61.87c-3.24,0-5.87-.73-5.87-1.62s2.63-1.64,5.87-1.64,5.87.74,5.87,1.64S38.13,61.87,34.89,61.87Z" />
<line x1="5.07" y1="53.08" x2="28.73" y2="53.08" />
<line x1="41.27" y1="53.08" x2="64.93" y2="53.08" />
</g>
<g id="northIcon" fill="#808090">
<path
d="M42.066 0v20h3.752V6.957L53.881 20h4.053V0h-3.752v13.355L45.996 0zm5.57 26.688l-25.77 69.949c-.823 2.238 1.658 4.248 3.677 2.978L50 84.195l24.455 15.42c2.02 1.273 4.504-.738 3.68-2.978l-25.79-70c-.472-1.096-1.283-1.632-2.384-1.635c-1.1-.003-2.017.856-2.324 1.686zm-.136 14.83V79.86L29.1 91.463z"></path>
</g>
<g filter="url(#shadow)" id="ecoIcon" transform="translate(60,12)">
<path style="stroke:#6cbe58;fill:rgb(30,30,40);" stroke-width="3" stroke-linejoin="round"
d="M 57,22.1667C 49.0833,28.5 52.25,31.6667 50.6667,36.4167C 49.0833,41.1667 50.6667,42.75 44.3333,47.5C 36.2227,53.583 29.2917,49.875 29.2917,49.875C 27.7083,51.4583 21.7708,54.2292 21.7708,54.2292L 20.0143,50.7285C 26.3476,49.1452 27.7083,47.1042 27.7083,47.1042C 26.125,43.9375 23.75,38 28.5,30.0833C 33.25,22.1667 57,22.1667 57,22.1667 Z M 30.0833,44.3333L 31.6667,45.9167C 31.6667,45.9167 34.8333,44.3333 36.4167,39.5833C 38,34.8333 45.9167,26.9167 45.9167,26.9167C 42.75,28.5 36.4167,34.8333 34.8333,39.5833C 33.25,44.3333 30.0833,44.3333 30.0833,44.3333 Z " />
</g>
<g filter="url(#shadow)" id="alertIcon" fill="#aa0" transform="translate(34,28),scale(2.2,2.2)">
<path
d="M214.413,746a4.455,4.455,0,0,1-3.84-2.166,4.249,4.249,0,0,1,0-4.334l25.572-43.331a4.483,4.483,0,0,1,7.679,0L269.4,739.5a4.249,4.249,0,0,1,0,4.334,4.452,4.452,0,0,1-3.84,2.166H214.413ZM240,706a4,4,0,0,0-4,4v16a4,4,0,0,0,8,0V710A4,4,0,0,0,240,706Zm0,36a4,4,0,1,0-4-4A4,4,0,0,0,240,742Z"
id="attention" transform="translate(-209.969 -694)" />
</g>
</defs>
<g id="pvInfo" class="clickable" transform="translate(330,280)">
<animateMotion begin="0.5s" dur="2s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-250 -250" fill="freeze">
</animateMotion>
<animate id="openDetails" begin="click" attributetype="CSS" attributeName="opacity" from="1"
to="1"
dur="0.1s" repeatCount="1" calcMode="spline"
fill="freeze">
</animate>
<!--<g filter="url(#glow)">-->
<use href="#ElementBG" stroke="#574400" />
<use href="#sunIcon" />
<path id="pvArc" fill="none" stroke="#f7c000" stroke-width="30" />
<path id="pv2Arc" fill="none" stroke="#f78d00" stroke-width="30" />
<path id="pv3Arc" fill="none" stroke="#f72300" stroke-width="30" />
<use href="#circGrid" style="stroke:#574400;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="pvText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="pv3txt" href="#infoTextPath" startOffset="12%">--.-- kW</textPath>
<textPath id="pv2txt" href="#infoTextPath" startOffset="35%">--.-- kW</textPath>
<textPath id="pv1txt" href="#infoTextPath" startOffset="55%">--W</textPath>
</text>
</g>
<circle id="closeDetails" fill="transparent" transform="translate(250,124)">
<animate begin="click" attributetype="CSS" attributeName="r" from="120" to="0" dur="0.1s"
repeatCount="1"
fill="freeze" />
<animate begin="pvInfo.click" attributeName="r" from="0" to="120" dur="0.1s" repeatCount="1"
fill="freeze" />
</circle>
<!--NON-PV-->
<g id="nonPVGroup">
<animate begin="pvInfo.click" attributetype="CSS" attributeName="opacity" from="1.0" to="0.0"
dur="1s"
repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="0.0"
to="1.0"
dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<g transform="translate(330,280), scale(0.90,0.90)" id="heatInfo" class="clickable" onclick="openModal('heater')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze"></animateMotion>
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L700 0" fill="freeze"></animateMotion>
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#5E2B21" />
<path fill="url(#gradient1)" stroke="#5E2B21" stroke-width="4"
d="M 99.86386438745993 22.00011880076356 A 78 78 0 1 0 100 22" />
<use href="#heatIcon" />
<path id="heatArc" fill="none" stroke="#C2614E" stroke-width="30" />
<use href="#circGrid6kW" style="stroke:#5E2B21;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="heatText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="heatUp" href="#infoTextPath" startOffset="9%">--°C</textPath>
<textPath id="heatDown" href="#infoTextPath" startOffset="36%">--°C</textPath>
</text>
<use id="heatEco" href="#ecoIcon" style="display:none;" />
<use id="heatDefault" href="#alertIcon" style="display:none;" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" class="clickable" id="CarInfoOG" onclick="openModal('CarOG')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion id="lastInitAni" begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline"
keyTimes="0;1" keySplines=".6,0,.27,1" path="M340 0 L800 -300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#182C43" />
<!--<path
filter="url(#fillpartialEV)" fill="#212529" stroke="#642445" stroke-width="4" d="M 99.863863
22.00012 A 78 78 0 1 0 100 22" />-->
<use href="#carIcon" stroke="#4b71a6" />
<use id="evChargeOG" href="#voltage" transform="translate(167,45),rotate(90),scale(5,5)"
style="fill:#cccc00; display:none;" />
<path id="evArcOG" fill="none" stroke="#4b71a6" stroke-width="30" />
<use href="#circGrid" style="stroke:#182C43;" />
<!--</g>-->
<use id="evPlugOG" href="#plugCordIcon" style="fill:#4b71a6;display:none;" />
<!--<use
href="#voltage" style="fill:#888;" transform="scale(3, 3),translate(7,32), rotate(50)" />-->
<!--<use
href="#gas" style="fill:#888;" transform="scale(0.55, 0.55),translate(210,319), rotate(337)"
/>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="evTextOG" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<!--<textPath
id="evFuelOG" href="#infoTextPath" startOffset="40%">-%</textPath>-->
<!--<textPath
id="evSOCOG" href="#infoTextPath" startOffset="15%">-%</textPath>-->
</text>
<!--<use
id="evLockOG" href="#lockIcon" style="display:none;" />-->
<use id="evEcoOG" href="#ecoIcon" style="display:none;" />
<use id="evDefaultOG" href="#plugCordIcon" transform="translate(105,-65),rotate(20)"
style="fill:#a53;display:none;" />
<use id="evNextTripOG" href="#clock" style="display:none;" />
<text dy="160" dx="100" id="evRemText" class="powerTextClass" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" class="clickable" id="CarInfo" onclick="openModal('CarEG')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion id="lastInitAni" begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline"
keyTimes="0;1" keySplines=".6,0,.27,1" path="M340 0 L800 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#1f5a66" />
<path filter="url(#fillpartialEV)" fill="#212529" stroke="#1f5a66" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#carIcon" stroke="#60afbf" />
<use id="evCharge" href="#voltage" transform="translate(167,45),rotate(90),scale(5,5)"
style="fill:#cccc00; display:none;" />
<path id="evArc" fill="none" stroke="#60afbf" stroke-width="30" />
<use href="#circGrid" style="stroke:#1f5a66;" />
<use id="evPlug" href="#plugCordIcon" style="fill:#60afbf;display:none;" />
<!--</g>-->
<use href="#voltage" style="fill:#888;" transform="scale(3, 3),translate(7,32), rotate(50)" />
<use href="#gas" style="fill:#888;"
transform="scale(0.55, 0.55),translate(210,319), rotate(337)" />
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="evText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="evFuel" href="#infoTextPath" startOffset="40%">--%</textPath>
<textPath id="evSOC" href="#infoTextPath" startOffset="15%">--%</textPath>
</text>
<use id="evLock" href="#lockIcon" style="display:none;" />
<use id="evEco" href="#ecoIcon" style="display:none;" />
<use id="evDefault" href="#plugCordIcon" transform="translate(105,-65),rotate(20)"
style="fill:#a53;display:none;" />
<use id="evNextTrip" href="#clock" style="display:none;" />
<text dy="160" dx="100" id="evRemText" class="powerTextClass" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="UGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L180 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#314619" />
<use href="#ugIcon" />
<path id="ugArc" fill="none" stroke="#83A55B" stroke-width="30" />
<use href="#circGrid" style="stroke:#314619;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="ugText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="EGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L500 300" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#18423B" />
<use href="#egIcon" />
<path id="egArc" fill="none" stroke="#70CC9C" stroke-width="30" />
<use href="#circGrid" style="stroke:#18423B;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="egText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
<use id="heatEG" transform="translate(120,170)"
style='display:none;mix-blend-mode: difference;fill:#70CCBB;' href="#heater" />
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="waterInfo" class="clickable" onclick="openModal('watering')">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L120 -300" fill="freeze" />
<use href="#ElementBG" stroke="#182C43" />
<path filter="url(#fillpartialWater)" fill="#212529" stroke="#182C43" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#water" transform="translate(60,55), scale(0.20,0.20)" fill="#3A6292" />
<!-- Wasserstand und Temperatur werden von der Bewässerungssteuerung nicht unterstützt.
<text id="waterText" filter="url(#shadowMiddle)" class="powerTextClass" text-anchor="middle" x="100" y="55">-.- cm</text>
<text id="waterTemp" filter="url(#shadowMiddle)" class="powerTextClass" text-anchor="middle" x="100" y="88">-.- °C</text>
-->
<use id="wateringFrontIcon" href="#gardenPlantIcon" transform="translate(152,162),rotate(310),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Vorn: aus</title>
</use>
<use id="wateringTroughIcon" href="#flowerPotIcon" transform="translate(18,100),rotate(75),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Tröge: aus</title>
</use>
<use id="wateringBedsIcon" href="#raisedBedIcon" transform="translate(66,174),rotate(15),scale(1.3)" fill="#6c757d" color="#6c757d">
<title>Beete: aus</title>
</use>
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="wateringFrontStatus" href="#infoTextPath" startOffset="46%">Aus </textPath>
<textPath id="wateringTroughStatus" href="#infoTextPath" startOffset="10%">Aus </textPath>
<textPath id="wateringBedsStatus" href="#infoTextPath" startOffset="28%">Aus </textPath>
</text>
</g>
<g transform="translate(330,280), scale(0.90,0.90)" id="OGInfo">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<animateMotion begin="1.2s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".6,0,.27,1" path="M340 0 L500 -300" fill="freeze" />
<use href="#ElementBG" stroke="#18423B" />
<use href="#ogIcon" />
<path id="ogArc" fill="none" stroke="#70CCBB" stroke-width="30" />
<use href="#circGrid" style="stroke:#18423B;" />
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="ogText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
</text>
<use id="heatOG" transform="translate(120,165)"
style='display:none;mix-blend-mode: difference;fill:#70CCBB;' href="#heater" />
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur=".7s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L340 0" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#1D4154" />
<use href="#plugIcon" />
<path id="consumerArc" fill="none" stroke="#71afcd" stroke-width="30" />
<use href="#circGrid" style="stroke:#1D4154;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="consumerText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="consumerTextAllg" href="#infoTextPath" startOffset="25%">--.-- kW</textPath>
<!--<textPath id="consumerTextEG" href="#infoTextPath" startOffset="35%">.- kW</textPath>
<textPath id="consumerTextUG" href="#infoTextPath" startOffset="55%">-.- kW</textPath> -->
</text>
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur="1.5s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-250 250" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#25481C" />
<path id="batArc" fill="none" stroke="#6cbe58" stroke-width="30" />
<path filter="url(#fillpartialBat)" fill="#212529" stroke="#25481C" stroke-width="4"
d="M 99.863863 22.00012 A 78 78 0 1 0 100 22" />
<use href="#battIcon" />
<use href="#circGrid" style="stroke:#25481C;" />
<use id="batLim" href="#clock" style="" transform="translate(4,40)" />
<!--</g>-->
<text id="batLimText" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(103,132)"></text>
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="batText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="batSOC" href="#infoTextPath" startOffset="26%">--.- %</textPath>
</text>
</g>
<g transform="translate(330,280)">
<animateMotion begin="0.5s" dur="1s" repeatCount="1" calcMode="spline" keyTimes="0;1"
keySplines=".1,0,.27,1" path="M0 0 L-330 0" fill="freeze" />
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#2D2D34" />
<path id="gridArc" fill="none" stroke="#808090" stroke-width="30" />
<use href="#circGrid" style="stroke:#2D2D34;" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="gridText" href="#powerTextPath" startOffset="15%">--.-- kW</textPath>
<textPath id="il1txt" href="#infoTextPath" startOffset="10%">--.- A</textPath>
<textPath id="il2txt" href="#infoTextPath" startOffset="25%">--.- A</textPath>
<textPath id="il3txt" href="#infoTextPath" startOffset="40%">--.- A</textPath>
</text>
<use href="#gridIcon" />
</g>
<g transform="translate(330,280), scale(1.1,1.1)" class="tooltip-trigger"
data-tooltip-text="The amazing pink box">
<!--<g
filter="url(#glow)">-->
<use href="#ElementBG" stroke="#2D2D34" />
<!--</g>-->
<text filter="url(#shadowMiddle)" class="powerTextClass">
<textPath id="genText" href="#powerTextPath" startOffset="22%">Autarkie: </textPath>
<textPath id="genInfoText" href="#infoTextPath" startOffset="25%"></textPath>
</text>
<use href="#inverterIcon" />
</g>
</g>
<g id="pvDetail" opacity="0" transform="scale(0.1,0.1)">
<animate begin="openDetails.begin" attributetype="CSS" attributeName="opacity" from="0" to="1.0"
dur="0.7s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animateTransform attributeName="transform"
type="scale"
from="0.1,0.1"
to="1,1"
begin="openDetails.begin"
dur="0.3s" repeatCount="1" fill="freeze" />
<animate begin="closeDetails.click" attributetype="CSS" attributeName="opacity" from="1.0"
to="0"
dur="0.4s" repeatCount="1" calcMode="spline" keyTimes="0;1" keySplines=".1,0,.27,1"
fill="freeze" />
<animateTransform attributeName="transform"
type="scale"
from="1,1"
to="0.1,0.1"
begin="closeDetails.click"
dur="0.7s" repeatCount="1" fill="freeze" />
<use href="#northIcon" transform="translate(1200,10),rotate(10)" />
<!--<polygon points="370 0, 1300 0, 1300 700, 0 700, 0 250, 370 250" fill="rgb(24, 27, 31)" />-->
<polygon points="560,80 720,80 720,300 1160,300 1160,370 560,370" stroke-linejoin="round"
fill="#f7c002" stroke-width="30" stroke="#f7c002" opacity="0.1" />
<use href="#pvPlate" transform="translate(600,80), rotate(90)" />
<use href="#pvPlate" transform="translate(600,150), rotate(90)" />
<use href="#pvPlate" transform="translate(600,220), rotate(90)" />
<use href="#pvPlate" transform="translate(600,290), rotate(90)" />
<use href="#pvPlate" transform="translate(640,80), rotate(90)" />
<use href="#pvPlate" transform="translate(640,150), rotate(90)" />
<use href="#pvPlate" transform="translate(640,220), rotate(90)" />
<use href="#pvPlate" transform="translate(640,290), rotate(90)" />
<use href="#pvPlate" transform="translate(680,80), rotate(90)" />
<use href="#pvPlate" transform="translate(680,150), rotate(90)" />
<use href="#pvPlate" transform="translate(680,220), rotate(90)" />
<use href="#pvPlate" transform="translate(680,290), rotate(90)" />
<use href="#pvPlate" transform="translate(720,80), rotate(90)" />
<use href="#pvPlate" transform="translate(720,150), rotate(90)" />
<use href="#pvPlate" transform="translate(720,220), rotate(90)" />
<use href="#pvPlate" transform="translate(790,290)" />
<use href="#pvPlate" transform="translate(720,330)" />
<use href="#pvPlate" transform="translate(790,330)" />
<use href="#pvPlate" transform="translate(870,290)" />
<use href="#pvPlate" transform="translate(870,330)" />
<use href="#pvPlate" transform="translate(940,330)" />
<use href="#pvPlate" transform="translate(1160,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1120,300), rotate(90)" />
<use href="#pvPlate" transform="translate(1080,300), rotate(90)" />
<text id="det_pv0P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(560, 150), scale(1.5,1.5)">-.-- kW</text>
<polygon points="980,60 1160,60 1160,265 980,265" stroke-linejoin="round" fill="#F76D00"
stroke-width="30" stroke="#F76D00" opacity="0.1" />
<use href="#pvPlate" transform="translate(1160,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,200), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1025,130), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1160,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1115,60), rotate(90), scale(1,1.125)" />
<use href="#pvPlate" transform="translate(1070,60), rotate(90), scale(1,1.125)" />
<text id="det_pv1P" filter="url(#shadowBig)" class="infoTextClass" dx="55" dy="30"
transform="translate(990, 140), scale(1.5,1.5)">-.-- kW</text>
<use href="#pvPlate" x="200" y="680" />
<text id="det_pv33P" filter="url(#shadowMiddle)" class="infoTextClass" x="200" y="680" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="550" />
<text id="det_pv32P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="550" dx="36"
dy="26">#19</text>
<use href="#pvPlate" x="380" y="590" />
<text id="det_pv31P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="590" dx="36"
dy="26">#18</text>
<use href="#pvPlate" x="380" y="630" />
<text id="det_pv30P" filter="url(#shadowMiddle)" class="infoTextClass" x="380" y="630" dx="36"
dy="26">#17</text>
<use href="#pvPlate" x="310" y="510" />
<text id="det_pv29P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="510" dx="36"
dy="26">#16</text>
<use href="#pvPlate" x="310" y="550" />
<text id="det_pv28P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="550" dx="36"
dy="26">#15</text>
<use href="#pvPlate" x="310" y="590" />
<text id="det_pv27P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="590" dx="36"
dy="26">#14</text>
<use href="#pvPlate" x="310" y="630" />
<text id="det_pv26P" filter="url(#shadowMiddle)" class="infoTextClass" x="310" y="630" dx="36"
dy="26">#13</text>
<use href="#pvPlate" x="240" y="510" />
<text id="det_pv25P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="510" dx="36"
dy="26">#12</text>
<use href="#pvPlate" x="240" y="550" />
<text id="det_pv24P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="550" dx="36"
dy="26">#11</text>
<use href="#pvPlate" x="240" y="590" />
<text id="det_pv23P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="590" dx="36"
dy="26">#10</text>
<use href="#pvPlate" x="240" y="630" />
<text id="det_pv22P" filter="url(#shadowMiddle)" class="infoTextClass" x="240" y="630" dx="36"
dy="26">#9</text>
<use href="#pvPlate" x="170" y="510" />
<text id="det_pv20P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="510" dx="36"
dy="26">#8</text>
<use href="#pvPlate" x="170" y="550" />
<text id="det_pv21P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="550" dx="36"
dy="26">#7</text>
<use href="#pvPlate" x="170" y="590" />
<text id="det_pv19P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="590" dx="36"
dy="26">#6</text>
<use href="#pvPlate" x="170" y="630" />
<text id="det_pv18P" filter="url(#shadowMiddle)" class="infoTextClass" x="170" y="630" dx="36"
dy="26">#5</text>
<use href="#pvPlate" x="100" y="510" />
<text id="det_pv14P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="510" dx="36"
dy="26">#1</text>
<use href="#pvPlate" x="100" y="550" />
<text id="det_pv15P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="550" dx="36"
dy="26">#2</text>
<use href="#pvPlate" x="100" y="590" />
<text id="det_pv16P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="590" dx="36"
dy="26">#3</text>
<use href="#pvPlate" x="100" y="630" />
<text id="det_pv17P" filter="url(#shadowMiddle)" class="infoTextClass" x="100" y="630" dx="36"
dy="26">#4</text>
<use href="#pvPlate" transform="translate(600,500)" />
<text id="det_pv13P" filter="url(#shadowMiddle)" class="infoTextClass" x="600" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(500,500)" />
<text id="det_pv12P" filter="url(#shadowMiddle)" class="infoTextClass" x="500" y="500" dx="36"
dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,120), rotate(-90)" />
<text id="det_pv5P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,190), rotate(-90)" />
<text id="det_pv4P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,270), rotate(-90)" />
<text id="det_pv9P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,340), rotate(-90)" />
<text id="det_pv8P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,340), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,420), rotate(-90)" />
<text id="det_pv11P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,420), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(460,490), rotate(-90)" />
<text id="det_pv10P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(460,490), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,120), rotate(-90)" />
<text id="det_pv7P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,120), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,190), rotate(-90)" />
<text id="det_pv6P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,190), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,270), rotate(-90)" />
<text id="det_pv3P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,270), rotate(-90)" dx="36" dy="26">- W</text>
<use href="#pvPlate" transform="translate(400,340), rotate(-90)" />
<text id="det_pv2P" filter="url(#shadowMiddle)" class="infoTextClass"
transform="translate(400,340), rotate(-90)" dx="36" dy="26">- W</text>
<text id="invList" filter="url(#shadowMiddle)" class="tooltipTextClass"
transform="translate(80,255)"></text>
<rect width="305" height="210" x="70" y="250" rx="5" ry="5" fill="none" stroke-width="4px"
stroke="#f7c000" />
</g>
<g id="tooltip" visibility="hidden">
<rect x="2" y="2" width="200" height="60" fill="black" opacity="0.4" rx="2" ry="2" />
<rect width="200" height="60" fill="white" rx="2" ry="2" />
<text x="4" y="6">Tooltip</text>
</g>
</svg>
+350
View File
@@ -0,0 +1,350 @@
<?php
/*
* Copyright (C) 2022 Lukas Buchs
* license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
*
* Server test script for WebAuthn library. Saves new registrations in session.
*
* JAVASCRIPT | SERVER
* ------------------------------------------------------------
*
* REGISTRATION
*
* window.fetch -----------------> getCreateArgs
* |
* navigator.credentials.create <-------------'
* |
* '-------------------------> processCreate
* |
* alert ok or fail <----------------'
*
* ------------------------------------------------------------
*
* VALIDATION
*
* window.fetch ------------------> getGetArgs
* |
* navigator.credentials.get <----------------'
* |
* '-------------------------> processGet
* |
* alert ok or fail <----------------'
*
* ------------------------------------------------------------
*/
require_once './restricted/WebAuthn/src/WebAuthn.php';
require_once("./restricted/mysql.php");
try {
session_start();
// read get argument and post body
$fn = filter_input(INPUT_GET, 'fn');
$requireResidentKey = false;
$userVerification = false;
$formats = [];
$msg="";
$formats[] = 'android-key';
$formats[] = 'android-safetynet';
$formats[] = 'apple';
$formats[] = 'fido-u2f';
$formats[] = 'none';
$formats[] = 'packed';
$formats[] = 'tpm';
$userId = "E071229F004A4CDE";
$userName = "SmartHomeWagner";
$userDisplayName = "2025Bym0";
$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post, null, 512, JSON_THROW_ON_ERROR);
}
$rpId = "nas.el-wa.org";
if ($rpId === false) {
throw new Exception('invalid relying party ID');
}
// cross-platform: true, if type internal is not allowed
// false, if only internal is allowed
// null, if internal and cross-platform is allowed
$crossPlatformAttachment = null;
// new Instance of the server library.
// make sure that $rpId is the domain name.
$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats);
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/isrg-root-x2.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solo.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_f1.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/solokey_r1.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/apple.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/yubico.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/hypersecu.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/globalSign.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/googleHardware.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/microsoftTpmCollection.pem');
$WebAuthn->addRootCertificates('./restricted/WebAuthn/rootCertificates/mds');
// ------------------------------------
// request for create arguments
// ------------------------------------
if ($fn === 'getCreateArgs') {
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
header('Content-Type: application/json');
print(json_encode($createArgs));
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
// ------------------------------------
// request for get arguments
// ------------------------------------
} else if ($fn === 'getGetArgs') {
$ids = [];
$mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db);
$result = mysqli_query($mysql,"SELECT credentialId FROM users WHERE userId = '".base64_encode($userId)."';");
if(!$result){
$msg = "Error:<br>".mysqli_error($mysql)."<br />";
}
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$ids[] = base64_decode($row["credentialId"]);
}
}
/*
if ($requireResidentKey) {
if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) {
throw new Exception('we do not have any registrations in session to check the registration');
}
} else {
// load registrations from session stored there by processCreate.
// normaly you have to load the credential Id's for a username
// from the database.
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
foreach ($_SESSION['registrations'] as $reg) {
if ($reg->userId === $userId) {
$ids[] = $reg->credentialId;
}
}
}
}
*/
if (count($ids) === 0) {
throw new Exception('no registrations in session for userId ' . $userId);
}
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, true, true, true, true, true, $userVerification);
header('Content-Type: application/json');
print(json_encode($getArgs));
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
// ------------------------------------
// process create
// ------------------------------------
} else if ($fn === 'processCreate') {
$mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db);
$clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null;
$attestationObject = !empty($post->attestationObject) ? base64_decode($post->attestationObject) : null;
$challenge = $_SESSION['challenge'] ?? null;
// processCreate returns data to be stored for future logins.
// in this example we store it in the php session.
// Normally you have to store the data in a database connected
// with the username.
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false);
// add user infos
$data->userId = $userId;
$data->userName = $userName;
$data->userDisplayName = $userDisplayName;
//set Null to 0
$data->signatureCounter ??= 0;
/*
if (!isset($_SESSION['registrations']) || !array_key_exists('registrations', $_SESSION) || !is_array($_SESSION['registrations'])) {
$_SESSION['registrations'] = [];
}*/
if(!mysqli_query($mysql,"INSERT INTO users SET userId = '".base64_encode($data->userId)."', credentialId = '".base64_encode($data->credentialId)."', credentialPublicKey = '".base64_encode($data->credentialPublicKey)."', signatureCounter = '".base64_encode($data->signatureCounter)."', name = '".mysqli_real_escape_string($mysql,filter_input(INPUT_GET, 'name'))."';")){
$msg = "Error:<br>".mysqli_error($mysql)."<br />";
}
else{
if ($data->rootValid === false) {
$msg = 'registration ok, but certificate does not match any of the selected root ca.';
}
// $msg = "Data: ".json_last_error();//json_encode($data);//'registration success.';
}
/*
$_SESSION['registrations'][] = $data;
*/
$return = new stdClass();
$return->success = true;
$return->msg = $msg;
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// proccess get
// ------------------------------------
} else if ($fn === 'processGet') {
$clientDataJSON = !empty($post->clientDataJSON) ? base64_decode($post->clientDataJSON) : null;
$authenticatorData = !empty($post->authenticatorData) ? base64_decode($post->authenticatorData) : null;
$signature = !empty($post->signature) ? base64_decode($post->signature) : null;
$userHandle = !empty($post->userHandle) ? base64_decode($post->userHandle) : null;
$id = !empty($post->id) ? base64_decode($post->id) : null;
$challenge = $_SESSION['challenge'] ?? '';
$credentialPublicKey = null;
// looking up correspondending public key of the credential id
// you should also validate that only ids of the given user name
// are taken for the login.
$mysql = new mysqli($mysql_server,$mysql_user,$mysql_pass,$mysql_db);
$result = mysqli_query($mysql,"SELECT credentialPublicKey, userId, name FROM users WHERE credentialId = '".base64_encode($id)."';");
if(!$result){
$msg = "Error:<br>".mysqli_error($mysql)."<br />";
}
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$credentialPublicKey = base64_decode($row["credentialPublicKey"]);
$reg = (object) ['userId' => base64_decode($row["userId"])];
$username = $row["name"];
} /*else {
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
foreach ($_SESSION['registrations'] as $reg) {
if ($reg->credentialId === $id) {
$credentialPublicKey = $reg->credentialPublicKey;
break;
}
}
}
}*/
if ($credentialPublicKey === null) {
throw new Exception('Public Key for credential ID not found!');
}
// if we have resident key, we have to verify that the userHandle is the provided userId at registration
if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) {
throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')');
}
// process the get request. throws WebAuthnException if it fails
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required');
$return = new stdClass();
$return->success = true;
$authKey = strval(random_int(0,99999999));
$result = mysqli_query($mysql,"UPDATE users SET authKey=".$authKey.", lastAuth=NOW() WHERE credentialId = '".base64_encode($id)."';");
$_SESSION["Logged"] = true;
$_SESSION["user"] = $username;
$_SESSION["authKey"] = $authKey;
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// proccess clear registrations
// ------------------------------------
} else if ($fn === 'clearRegistrations') {
$_SESSION['registrations'] = null;
$_SESSION['challenge'] = null;
$return = new stdClass();
$return->success = true;
$return->msg = 'all registrations deleted';
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// display stored data as HTML
// ------------------------------------
} /*else if ($fn === 'getStoredDataHtml') {
$html = '<!DOCTYPE html>' . "\n";
$html .= '<html><head><style>tr:nth-child(even){background-color: #f2f2f2;}</style></head>';
$html .= '<body style="font-family:sans-serif">';
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
$html .= '<p>There are ' . count($_SESSION['registrations']) . ' registrations in this session:</p>';
foreach ($_SESSION['registrations'] as $reg) {
$html .= '<table style="border:1px solid black;margin:10px 0;">';
foreach ($reg as $key => $value) {
if (is_bool($value)) {
$value = $value ? 'yes' : 'no';
} else if (is_null($value)) {
$value = 'null';
} else if (is_object($value)) {
$value = chunk_split(strval($value), 64);
} else if (is_string($value) && strlen($value) > 0 && htmlspecialchars($value, ENT_QUOTES) === '') {
$value = chunk_split(bin2hex($value), 64);
}
$html .= '<tr><td>' . htmlspecialchars($key) . '</td><td style="font-family:monospace;">' . nl2br(htmlspecialchars($value)) . '</td>';
}
$html .= '</table>';
}
} else {
$html .= '<p>There are no registrations.</p>';
}
$html .= '</body></html>';
header('Content-Type: text/html');
print $html;
// ------------------------------------
// get root certs from FIDO Alliance Metadata Service
// ------------------------------------
} */else if ($fn === 'queryFidoMetaDataService') {
$mdsFolder = './restricted/WebAuthn/rootCertificates/mds';
$success = false;
$msg = null;
// fetch only 1x / 24h
$lastFetch = \is_file($mdsFolder . '/lastMdsFetch.txt') ? \strtotime(\file_get_contents($mdsFolder . '/lastMdsFetch.txt')) : 0;
if ($lastFetch + (3600*48) < \time()) {
$cnt = $WebAuthn->queryFidoMetaDataService($mdsFolder);
$success = true;
\file_put_contents($mdsFolder . '/lastMdsFetch.txt', date('r'));
$msg = 'successfully queried FIDO Alliance Metadata Service - ' . $cnt . ' certificates downloaded.';
} else {
$msg = 'Fail: last fetch was at ' . date('r', $lastFetch) . ' - fetch only 1x every 48h';
}
$return = new stdClass();
$return->success = $success;
$return->msg = $msg;
header('Content-Type: application/json');
print(json_encode($return));
}
} catch (Throwable $ex) {
$return = new stdClass();
$return->success = false;
$return->msg = $ex->getMessage();
header('Content-Type: application/json');
print(json_encode($return));
}
+7
View File
File diff suppressed because one or more lines are too long
+5
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+21
View File
@@ -0,0 +1,21 @@
.winddir {
position:absolute;
left:60%;
width:25%;
height:40%;
padding: 0;
background-image: url('../assets/img/arrow.svg');
background-size:contain;
background-repeat: no-repeat;
background-position: center;
}
/* Schieberegler in den Modals (Autoladung, Heizstab, Thermostat).
Der gefuellte Anteil kommt aus --background-size, gesetzt von
bindModalSlider() in js/solar/common.js. */
input[type='range']::-webkit-slider-runnable-track {
background: linear-gradient(to right, #00788F, #00788F), #D7D7D7;
background-size: var(--background-size, 0%) 100%;
background-repeat: no-repeat;
}
-181
View File
@@ -1,181 +0,0 @@
-- ============================================================================
-- Somfy Tahoma Datenbank Schema
-- Normalisierte Struktur für Aktoren, Sensoren und ihre Parameter
-- ============================================================================
-- Datenbank erstellen (falls noch nicht vorhanden)
-- CREATE DATABASE IF NOT EXISTS EnergyFlow CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
-- USE EnergyFlow;
-- ============================================================================
-- HAUPTTABELLEN
-- ============================================================================
-- Tabelle: actors
-- Speichert alle Aktoren (Geräte mit Steuerungsfunktion)
CREATE TABLE IF NOT EXISTS `actors` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(50) NOT NULL COMMENT 'Gerätetyp z.B. RollerShutter',
`name` varchar(70) NOT NULL COMMENT 'Name des Geräts',
`parameters` text DEFAULT NULL COMMENT 'Zusätzliche Meta-Informationen als JSON',
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL',
PRIMARY KEY (`id`),
UNIQUE KEY `url` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- Tabelle: sensors
-- Speichert alle Sensoren (Geräte die Werte melden)
CREATE TABLE IF NOT EXISTS `sensors` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(50) NOT NULL COMMENT 'Sensortyp z.B. TemperatureSensor',
`name` varchar(70) NOT NULL COMMENT 'Name des Sensors',
`parameters` text DEFAULT NULL COMMENT 'Zusätzliche Meta-Informationen als JSON',
`url` varchar(100) NOT NULL COMMENT 'Tahoma Device URL',
PRIMARY KEY (`id`),
UNIQUE KEY `url` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ============================================================================
-- AKTOR-BEZOGENE TABELLEN
-- ============================================================================
-- Tabelle: actor_commands
-- Speichert alle verfügbaren Commands für jeden Aktor
CREATE TABLE IF NOT EXISTS `actor_commands` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`actor_id` int(11) NOT NULL COMMENT 'Referenz zum Aktor',
`command_name` varchar(100) NOT NULL COMMENT 'Name des Commands z.B. setPosition, open, close',
PRIMARY KEY (`id`),
KEY `actor_id` (`actor_id`),
CONSTRAINT `fk_actor_commands_actor`
FOREIGN KEY (`actor_id`) REFERENCES `actors`(`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- Tabelle: command_parameters
-- Speichert die Parameter für jeden Command
CREATE TABLE IF NOT EXISTS `command_parameters` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`command_id` int(11) NOT NULL COMMENT 'Referenz zum Command',
`parameter_name` varchar(100) NOT NULL COMMENT 'Name des Parameters z.B. position',
`parameter_type` varchar(50) DEFAULT NULL COMMENT 'Datentyp z.B. integer, string',
`min_value` decimal(10,2) DEFAULT NULL COMMENT 'Minimaler Wert (falls numerisch)',
`max_value` decimal(10,2) DEFAULT NULL COMMENT 'Maximaler Wert (falls numerisch)',
`possible_values` text DEFAULT NULL COMMENT 'JSON Array mit möglichen Werten (für Enums)',
PRIMARY KEY (`id`),
KEY `command_id` (`command_id`),
CONSTRAINT `fk_command_parameters_command`
FOREIGN KEY (`command_id`) REFERENCES `actor_commands`(`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ============================================================================
-- SENSOR-BEZOGENE TABELLEN
-- ============================================================================
-- Tabelle: sensor_states
-- Speichert alle verfügbaren States für jeden Sensor
CREATE TABLE IF NOT EXISTS `sensor_states` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sensor_id` int(11) NOT NULL COMMENT 'Referenz zum Sensor',
`state_name` varchar(100) NOT NULL COMMENT 'Name des State z.B. core:TemperatureState',
`state_type` int(11) DEFAULT NULL COMMENT 'State-Typ Code aus Tahoma API',
`current_value` varchar(255) DEFAULT NULL COMMENT 'Aktueller Wert des State',
`unit` varchar(20) DEFAULT NULL COMMENT 'Einheit z.B. °C, %, lux',
`last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `sensor_id` (`sensor_id`),
CONSTRAINT `fk_sensor_states_sensor`
FOREIGN KEY (`sensor_id`) REFERENCES `sensors`(`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ============================================================================
-- GEMEINSAME TABELLE FÜR ACTOR STATES (optional)
-- ============================================================================
-- Tabelle: actor_states
-- Speichert die aktuellen States von Aktoren (z.B. aktuelle Position)
CREATE TABLE IF NOT EXISTS `actor_states` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`actor_id` int(11) NOT NULL COMMENT 'Referenz zum Aktor',
`state_name` varchar(100) NOT NULL COMMENT 'Name des State z.B. core:ClosureState',
`state_type` int(11) DEFAULT NULL COMMENT 'State-Typ Code aus Tahoma API',
`current_value` varchar(255) DEFAULT NULL COMMENT 'Aktueller Wert des State',
`unit` varchar(20) DEFAULT NULL COMMENT 'Einheit falls vorhanden',
`last_updated` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `actor_id` (`actor_id`),
CONSTRAINT `fk_actor_states_actor`
FOREIGN KEY (`actor_id`) REFERENCES `actors`(`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- ============================================================================
-- INDIZES FÜR PERFORMANCE
-- ============================================================================
-- Zusätzliche Indizes für häufige Queries
CREATE INDEX idx_actors_type ON actors(type);
CREATE INDEX idx_sensors_type ON sensors(type);
CREATE INDEX idx_actor_commands_name ON actor_commands(command_name);
CREATE INDEX idx_sensor_states_name ON sensor_states(state_name);
CREATE INDEX idx_actor_states_name ON actor_states(state_name);
-- ============================================================================
-- VIEWS (optional - für einfachere Queries)
-- ============================================================================
-- View: Alle Aktoren mit ihren Commands
CREATE OR REPLACE VIEW view_actors_with_commands AS
SELECT
a.id as actor_id,
a.name as actor_name,
a.type as actor_type,
a.url as actor_url,
ac.id as command_id,
ac.command_name,
cp.parameter_name,
cp.parameter_type,
cp.min_value,
cp.max_value,
cp.possible_values
FROM actors a
LEFT JOIN actor_commands ac ON a.id = ac.actor_id
LEFT JOIN command_parameters cp ON ac.id = cp.command_id
ORDER BY a.id, ac.id, cp.id;
-- View: Alle Sensoren mit ihren States
CREATE OR REPLACE VIEW view_sensors_with_states AS
SELECT
s.id as sensor_id,
s.name as sensor_name,
s.type as sensor_type,
s.url as sensor_url,
ss.state_name,
ss.state_type,
ss.current_value,
ss.unit,
ss.last_updated
FROM sensors s
LEFT JOIN sensor_states ss ON s.id = ss.sensor_id
ORDER BY s.id, ss.id;
-- View: Übersicht aller Geräte
CREATE OR REPLACE VIEW view_all_devices AS
SELECT
'actor' as device_category,
id,
type,
name,
url
FROM actors
UNION ALL
SELECT
'sensor' as device_category,
id,
type,
name,
url
FROM sensors
ORDER BY device_category, name;
+67
View File
@@ -0,0 +1,67 @@
<?php
session_start();
require_once("restricted/mysql.php");
$_SESSION["local"] =false;
function isLocal(){
$ipv6_prefix = explode(":",$_SERVER['SERVER_ADDR'],5);
$ipv6 = explode(":",$_SERVER['REMOTE_ADDR'],5);
if(($ipv6_prefix[0] == $ipv6[0] && $ipv6_prefix[1] == $ipv6[1] && $ipv6_prefix[2] == $ipv6[2] && $ipv6_prefix[3] == $ipv6[3]) || str_starts_with($_SERVER['REMOTE_ADDR'],"192.168.179.")){
$_SESSION["local"] =true;
return true;
}else{
return false;
}
}
/**
* Kleine HTTP-GET-Anfrage im Heimnetz, Antwort als JSON-Array.
* Ersetzt die frueher in carOG.php, heater.php und AutoAction.php je einzeln
* definierte Funktion getSSLPage().
*/
function httpGetJson($url){
$curlSession = curl_init();
curl_setopt($curlSession, CURLOPT_URL, $url);
curl_setopt($curlSession, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlSession, CURLOPT_TIMEOUT, 5);
$response = curl_exec($curlSession);
curl_close($curlSession);
return json_decode($response, true);
}
function checkLogin(){
$mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]);
if(isLocal()){
$_SESSION["local"] =true;
return true;
}
if(isset($_SESSION["authKey"])){
$res = mysqli_query($mysql,"SELECT id FROM users WHERE lastAuth > DATE_SUB(NOW(), INTERVAL 2 DAY) AND authKey = '".mysqli_real_escape_string($mysql,$_SESSION["authKey"])."' AND name = '".mysqli_real_escape_string($mysql,$_SESSION["user"])."';");
if(!$res){
echo mysqli_error($mysql);
return false;
}
if(mysqli_num_rows($res) == 1){
return isset($_SESSION["Logged"]);
}
}else{
return false;
}
}
function checkAdduser(){
$mysql = new mysqli($GLOBALS["mysql_server"],$GLOBALS["mysql_user"],$GLOBALS["mysql_pass"],$GLOBALS["mysql_db"]);
if(!mysqli_query($mysql,"DELETE FROM addUser WHERE datetime < DATE_SUB(NOW(), INTERVAL 1 MINUTE);")){
echo mysqli_error($mysql);
}
$result = mysqli_query($mysql,"SELECT * FROM addUser WHERE accesskey='".mysqli_real_escape_string($mysql,$_GET["addUser"])."';");
if(!$result){
return false;
}
if ($result->num_rows > 0) {
return true;
}
return false;
}
?>
+132
View File
@@ -0,0 +1,132 @@
<?php
require_once("helper.php");
if (isset($_GET["addUser"])) {
if (checkAdduser($_GET["addUser"])) {
echo <<<ENDE
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Smarthome control</title>
<link rel="icon" type="image/png" href="assets/img/favicon.png">
<!--end::Accessibility Meta Tags-->
<!--begin::Primary Meta Tags-->
<meta name="title" content="Smarthome control" />
<meta name="author" content="ColorlibHQ" />
<meta name="description" content="Smarthome control panel by m0." />
<meta name="keywords" content="smarthome dashboard, admin panel" />
<!--end::Primary Meta Tags-->
<!--begin::Fonts-->
<link rel="stylesheet" href="assets/fonts/font_poppins.css" media="print" onload="this.media='all'" />
<!--end::Fonts-->
<!--begin::Third Party Plugin(Bootstrap Icons)-->
<link rel="stylesheet" href="css/bootstrap-icons.min.css" />
<!--end::Third Party Plugin(Bootstrap Icons)-->
<!--begin::Required Plugin(AdminLTE)-->
<link rel="stylesheet" href="./css/adminlte.min.css?v=2" />
<!--end::Required Plugin(AdminLTE)-->
</head>
<body onload="createRegistration()">
</body>
<script src="js/auth.js"></script>
<script>
async function createRegistration() {
try {
// check browser support
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error('Browser not supported.');
}
let keyName = encodeURIComponent(prompt('Bitte Hier den Namen des Schlüssels eingeben:'));
// get create args
let rep = await window.fetch('authServer.php?fn=getCreateArgs', {method:'GET', cache:'no-cache'});
//alert(await rep.text());
const createArgs = await rep.json();
// error handling
if (createArgs.success === false) {
throw new Error(createArgs.msg || 'unknown error occured');
}
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(createArgs);
// create credentials
const cred = await navigator.credentials.create(createArgs);
// create object
const authenticatorAttestationResponse = {
transports: cred.response.getTransports ? cred.response.getTransports() : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
};
// check auth on server side
rep = await window.fetch('authServer.php?fn=processCreate&name='+keyName, {
method : 'POST',
body : JSON.stringify(authenticatorAttestationResponse),
cache : 'no-cache'
});
authenticatorAttestationServerResponse = await rep.json();
// prompt server response
if (authenticatorAttestationServerResponse.success) {
window.alert(authenticatorAttestationServerResponse.msg || 'registration success');
if(!authenticatorAttestationServerResponse.msg){
window.location.href = "https://nas.el-wa.org/smart";
}
} else {
throw new Error(authenticatorAttestationServerResponse.msg);
}
} catch (err) {
window.alert(err.message || 'unknown error occured');
}
}
</script>
</html>
ENDE;
}
} else if (checkLogin()) {
/*
* Seitentabelle: Vorlage, Seitenskripte und welche Bibliotheken die Seite
* ueberhaupt braucht. Frueher wurden Chart.js, MQTT & Co. auf jeder Seite
* geladen - die Home-Seite etwa zeichnet gar keine Diagramme.
*/
$pages = [
"solar" => ["template" => "solar.php", "mqtt" => true, "charts" => true, "meteogram" => true,
"scripts" => ["js/solar/solarMQTT.js"]],
"home" => ["template" => "home.php", "mqtt" => true, "charts" => false, "meteogram" => false,
"scripts" => ["js/solar/autoActionFuncs.js", "js/solar/homeMQTT.js"]],
"heat" => ["template" => "heat.php", "mqtt" => true, "charts" => true, "meteogram" => false,
"scripts" => ["js/solar/heatMQTT.js"]],
"history" => ["template" => "history.php", "mqtt" => false, "charts" => true, "meteogram" => false,
"scripts" => ["js/solar/historyMQTT.js"]],
"weather" => ["template" => "weather.php", "mqtt" => true, "charts" => false, "meteogram" => true,
"scripts" => ["js/solar/weatherMQTT.js"]],
];
// Unbekannte oder fehlende Aktion faellt auf die Solar-Seite zurueck.
if (!isset($_GET["action"]) || !isset($pages[$_GET["action"]])) {
$_GET["action"] = "solar";
}
$page = $pages[$_GET["action"]];
include "restricted/header.php";
include "restricted/" . $page["template"];
include "restricted/footer.php";
} else {
echo <<<ENDE
<html>
<head><link rel="icon" type="image/png" href="assets/img/favicon.png"></head>
<body onload="checkRegistration()">
<script src="js/auth.js"></script>
</body>
</html>
ENDE;
}
+7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+149
View File
@@ -0,0 +1,149 @@
async function checkRegistration() {
try {
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error('Browser not supported.');
}
// get check args
let rep = await window.fetch('authServer.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'});
const getArgs = await rep.json();
// error handling
if (getArgs.success === false) {
throw new Error(getArgs.msg);
}
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(getArgs);
// check credentials with hardware
const cred = await navigator.credentials.get(getArgs);
// create object for transmission to server
const authenticatorAttestationResponse = {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
};
// send to server
rep = await window.fetch('authServer.php?fn=processGet' + getGetParams(), {
method:'POST',
body: JSON.stringify(authenticatorAttestationResponse),
cache:'no-cache'
});
const authenticatorAttestationServerResponse = await rep.json();
// check server response
if (authenticatorAttestationServerResponse.success) {
window.location.reload();
/// window.alert(authenticatorAttestationServerResponse.msg || 'login success');
} else {
throw new Error(authenticatorAttestationServerResponse.msg);
}
} catch (err) {
//reloadServerPreview();
window.alert(err.message || 'unknown error occured');
}
}
function queryFidoMetaDataService() {
window.fetch('authServer.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(json) {
if (json.success) {
window.alert(json.msg);
} else {
throw new Error(json.msg);
}
}).catch(function(err) {
window.alert(err.message || 'unknown error occured');
});
}
/**
* convert RFC 1342-like base64 strings to array buffer
* @param {mixed} obj
* @returns {undefined}
*/
function recursiveBase64StrToArrayBuffer(obj) {
let prefix = '=?BINARY?B?';
let suffix = '?=';
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'string') {
let str = obj[key];
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
str = str.substring(prefix.length, str.length - suffix.length);
let binary_string = window.atob(str);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
obj[key] = bytes.buffer;
}
} else {
recursiveBase64StrToArrayBuffer(obj[key]);
}
}
}
}
/**
* Convert a ArrayBuffer to Base64
* @param {ArrayBuffer} buffer
* @returns {String}
*/
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa(binary);
}
/**
* Get URL parameter
* @returns {String}
*/
function getGetParams() {
let url = '';
url += '&apple=1';
url += '&yubico=1';
url += '&solo=1';
url += '&hypersecu=1';
url += '&google=1';
url += '&microsoft=1';
url += '&mds=1';
url += '&requireResidentKey=0';
url += '&type_usb=1';
url += '&type_nfc=1';
url += '&type_ble=1';
url += '&type_int=1';
url += '&type_hybrid=1';
url += '&fmt_android-key=1';
url += '&fmt_android-safetynet=1';
url += '&fmt_apple=1';
url += '&fmt_fido-u2f=1';
url += '&fmt_none=0' ;
url += '&fmt_packed=1';
url += '&fmt_tpm=1';
url += '&userVerification=discouraged';
return url;
}
+7
View File
File diff suppressed because one or more lines are too long
+14
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
const meteogramURL = "https://www.meteoblue.com/de/wetter/widget/meteogram/untermaiselstein_deutschland_2819110?geoloc=fixed&temperature_units=CELSIUS&windspeed_units=KILOMETER_PER_HOUR&precipitation_units=MILLIMETER&forecast_days=5&layout=dark&autowidth=auto&user_key=cb2300d59c850d41&embed_key=f7f2b30b36c02cf0&sig=757b13cab0ac3c327a180babd58ef181fb6c83474b58dd6e5e5c2570b0884884";
+2
View File
@@ -0,0 +1,2 @@
/*! https://github.com/FranBar1966/bootstrap-5-modal-dynamic - License in the terms described in the LICENSE file */
function startDynamicModal(){document.body.removeEventListener("click",dynamicModalHandler),document.body.addEventListener("click",dynamicModalHandler)}function dynamicModalHandler(e){const t=e.target.closest(".modal-dynamic");if(!t)return;e.target.closest("a")&&e.preventDefault();const a=e.target.getAttribute("href"),d=e.target.dataset.template||"#modalTemplate";let o=document.querySelector(a);if(!o){const e=document.querySelector(d);e&&(o=e.cloneNode(!0),o.id=a.substring(1),document.body.appendChild(o))}if(!o)return;const n=e.target.dataset.class,r=e.target.dataset.title,s=e.target.dataset.header,l=e.target.dataset.noheader,c=e.target.dataset.url,i=e.target.dataset.footer,u=e.target.dataset.nofooter,m=e.target.dataset.width||"",y=e.target.dataset.backdrop||"false",h=!e.target.dataset.keyboard||"true"===e.target.dataset.keyboard;if(n&&o.classList.add(...n.split(" ")),s){const e=o.querySelector(".modal-header"),t=document.querySelector(s);e&&t&&(e.innerHTML=t.innerHTML)}if(l){const e=o.querySelector(".modal-header");e&&e.classList.add("hidden","d-none")}if(r){const e=o.querySelector(".modal-title");e&&(e.innerHTML=r)}if(i){const e=o.querySelector(".modal-footer"),t=document.querySelector(i);e&&t&&(e.innerHTML=t.innerHTML)}if(u){const e=o.querySelector(".modal-footer");e&&e.classList.add("hidden","d-none")}if(m){const e=o.querySelector(".modal-dialog");if(e){const t=isNaN(m)||""===m?m:m+"px";e.style.maxWidth=t,e.style.width="auto"}}let f=bootstrap.Modal.getInstance(o);f||(f=new bootstrap.Modal(o,{keyboard:h,backdrop:y})),o.addEventListener("hidden.bs.modal",e=>{o.remove()}),o.addEventListener("shown.bs.modal",e=>{o.focus()}),f.show();const g=o.querySelector(".modal-body");if(c.startsWith("#")){const e=document.querySelector(c);g.innerHTML=e?e.innerHTML:"ERROR: Content not found"}else fetch(c,{method:"GET",headers:{"X-Requested-From-Modal":a.substring(1),"Requested-With-Ajax":"ajax"}}).then(e=>e.text()).then(e=>{g.innerHTML=e,window.dispatchEvent(new CustomEvent("neutralFetchCompleted",{detail:{element:o,url:c}}))}).catch(e=>{g.innerHTML=e.message,window.dispatchEvent(new CustomEvent("neutralFetchError",{detail:{element:o,url:c}}))})}startDynamicModal(),window.addEventListener("neutralFetchCompleted",()=>{startDynamicModal()});
+19
View File
File diff suppressed because one or more lines are too long
+6
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
function siConvert(r){var t={Y:24,Z:21,E:18,P:15,T:12,G:9,M:6,k:3,"":0,m:-3,"µ":-6,n:-9,p:-12,f:-15,a:-18,z:-21,y:-24};if("string"==typeof r)return t[r];if("number"==typeof r)for(var n in t)if(t[n]===r)return n}function scale(r,t){t=t!==!1,"string"==typeof r&&(r=expand(r)),r=Number(r);var n=0;if(r!=0){n=Math.floor(Math.log10(Math.abs(r)));}n=3*Math.floor(n/3),n>24?n=24:n<-24&&(n=-24),r/=Math.pow(10,n);r=Math.round(r*10)/10;var e=siConvert(n);return 1==t?(String(r)+" "+e).trim():0==t?[r,e]:void 0}function expand(r){var t=0;if("string"==typeof r&&/[a-zA-Z]/.test(r.slice(-1))){var n=r.slice(-1);t=siConvert(n),r=r.slice(0,-1).trim()}return r=Number(r),r*=Math.pow(10,t)}
+390
View File
@@ -0,0 +1,390 @@
function addOptions(dropdownID, opts){
opts.forEach((opt, index) => {
var option = document.createElement("option");
if(opt.name){ //parameter could be command in case of actor.
option.text = opt.name;
option.value = opt.url;
if(opt.operators)
option.setAttribute("data-tglstates",JSON.stringify(opt.operators));
if(opt.type)
option.setAttribute("data-type",opt.type);
if(opt.options)
option.setAttribute("data-parameters",JSON.stringify([opt]));
if(opt.parameters){ //we have an actor with more than one parameters, then the parameters are actually nested inside the command
option.setAttribute("data-parameters",JSON.stringify(opt.parameters));
}
}else if(typeof opt === 'object'){
option.value = Object.keys(opt)[0];
option.text = opt[option.value];
}else {
option.text = opt;
option.value = opt;
}
if(typeof dropdownID === 'object'){
dropdownID.appendChild(option);
}else{
document.getElementById(dropdownID).appendChild(option);
}
});
}
function clearOptions(dropdownID){
if(typeof dropdownID === 'object'){
dropdownID.innerHTML = "";
}else{
document.getElementById(dropdownID).innerHTML = "";
}
}
function changeValueType(event){
dropdown = event.currentTarget;
type = dropdown.options[dropdown.selectedIndex].dataset.type;
id = dropdown.id.match(/\d+$/);
if(dropdown.id.search("act") > -1){
opBtnID = document.getElementById("btnActOperator"+id);
valBlockID = document.getElementById("act1ValBlock"+id);
valElemID = document.getElementById("act1Value"+id);
valOptID = document.getElementById("act1ValueOpt"+id);
val2BlockID = document.getElementById("act2ValBlock"+id);
val3BlockID = document.getElementById("act3ValBlock"+id);
val4BlockID = document.getElementById("act4ValBlock"+id);
}else{
opBtnID = document.getElementById("btnOperator"+id);
valBlockID =document.getElementById("valBlock"+id);
valElemID = document.getElementById("threshold"+id);
valOptID = document.getElementById("thresholdOpt"+id);
val2BlockID = null;
}
if(type =="hidden"){
opBtnID.hidden = true;
valBlockID.hidden = true;
if(val2BlockID){ //if this is an actor hide all additional inputs, as only one dropdown is supported
val2BlockID.hidden = true;
val3BlockID.hidden = true;
val4BlockID.hidden = true;
}
}else{
opBtnID.setAttribute("data-tglstates",dropdown.options[dropdown.selectedIndex].dataset.tglstates);
opBtnID.innerHTML = JSON.parse(dropdown.options[dropdown.selectedIndex].dataset.tglstates)[0];
opBtnID.hidden = false;
valBlockID.hidden = false;
if(type == "dropdown"){
valElemID.type = "text";
valOptID.hidden = false;
valElemID.hidden = true;
if(val2BlockID){ //if this is an actor hide all additional inputs, as only one dropdown is supported
val2BlockID.hidden = true;
val3BlockID.hidden = true;
val4BlockID.hidden = true;
}
clearOptions(valOptID);
addOptions(valOptID,JSON.parse(dropdown.options[dropdown.selectedIndex].dataset.parameters)[0].options);
}else if(type == "button" || type == "bool"){
valElemID.type = "text";
valOptID.hidden = true;
valBlockID.hidden = true;
if(val2BlockID){ //if this is an actor hide all additional inputs, as only one dropdown is supported
val2BlockID.hidden = true;
val3BlockID.hidden = true;
val4BlockID.hidden = true;
}
}else{
valElemID.type = type;
if(type == "time")
valElemID.value = "00:00";
else if(type == "date")
valElemID.value = new Date().toISOString().split('T')[0]
else if(type == "text")
valElemID.value = "";
else
valElemID.value = "0";
valOptID.hidden = true;
valElemID.hidden = false;
if(val2BlockID){
parameters = JSON.parse(dropdown.options[dropdown.selectedIndex].dataset.parameters);
val2BlockID.hidden = true;
val3BlockID.hidden = true;
val4BlockID.hidden = true;
theAssociatedLabel = valElemID.parentNode.querySelector("label[for='" + valElemID.id + "']");
theAssociatedLabel.innerHTML = parameters[0].name;
switch(parameters.length){
case 4:
theAssociatedLabel = val4BlockID.querySelector("label[for='" + val4BlockID.id.replace("Block","ue") + "']");
theAssociatedLabel.innerHTML = parameters[3].name;
val4BlockID.hidden = false;
case 3:
theAssociatedLabel = val3BlockID.querySelector("label[for='" + val3BlockID.id.replace("Block","ue") + "']");
theAssociatedLabel.innerHTML = parameters[2].name;
val3BlockID.hidden = false;
case 2:
theAssociatedLabel = val2BlockID.querySelector("label[for='" + val2BlockID.id.replace("Block","ue") + "']");
theAssociatedLabel.innerHTML = parameters[1].name;
val2BlockID.hidden = false;
}
}
}
}
}
function fillSensorDD(ID){
var elem = document.getElementById("sensorSelect"+ID);
fetch("./ajax/fillSensorDD.php", {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
elem.innerHTML = html;
elem.addEventListener("change", arrangeSensorInputs);
document.getElementById("paramSelect"+ID).addEventListener("change", changeValueType);
elem.dispatchEvent(new Event('change'));
})
.catch(error => {
elem.innerHTML = "";
});
}
function fillActorDD(ID){
var elem = document.getElementById("actorSelect"+ID);
fetch("./ajax/fillActorDD.php", {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
elem.innerHTML = html;
elem.addEventListener("change", arrangeActorInputs);
document.getElementById("actParamSelect"+ID).addEventListener("change", changeValueType);
document.getElementById("actorSelect"+ID).dispatchEvent(new Event('change'));
})
.catch(error => {
elem.innerHTML = "";
});
}
function changeAllIDs (parentNode, newID) {
for (var i = 0; i < parentNode.childNodes.length; i++) {
var child = parentNode.childNodes[i];
changeAllIDs(child, newID);
}
if(parentNode.id){
parentNode.id = parentNode.id.replace(/\d+$/, newID);
}
if(parentNode.dataset !== undefined){
if(parentNode.dataset.delid)
parentNode.dataset.delid = parentNode.dataset.delid.replace(/\d+$/, newID);
}
}
function delAutoEntry(event){
ID = event.currentTarget.dataset.delid;
document.getElementById(ID).remove();
ID = ID.replace(/\d+$/, function(n){ return ++n });
while(sensor= document.getElementById(ID)){
changeAllIDs(sensor, ID.match(/\d+$/)-1);
ID = ID.replace(/\d+$/, function(n){ return ++n });
}
}
function addSensor(event){
var t = document.getElementById('sensorsList').children;
//get the second last element (ignore "add" button)
nextID = Number(t[t.length-1].id.replace("sensorSettings","")) + 1;
var div = document.createElement('div');
div.className = "input-group mt-3";
div.id = "sensorSettings"+String(nextID);
div.innerHTML = `<button class='btn btn-outline-secondary' type='button' id='btnLogic${nextID}' name='btnLogic${nextID}' data-tglstates='["und","oder"]' >und</button>
<div class="form-floating">
<select class="form-select" id="sensorSelect${nextID}" name="sensorSelect${nextID}" aria-label="Default select example">
</select>
<label>Sensor</label>
</div>
<div class="form-floating" id="paramBlock${nextID}">
<select class="form-select" id="paramSelect${nextID}" name="paramSelect${nextID}" aria-label="Default select example">
</select>
<label>Messwert</label>
</div>
<button class="btn btn-outline-secondary" style="min-width: 40px;" type="button" id="btnOperator${nextID}" name="btnOperator${nextID}">&gt;</button>
<div class="form-floating" id="valBlock${nextID}">
<input type="number" class="form-control" id="threshold${nextID}" name="threshold${nextID}" placeholder="0" value="0"></input>
<select class="form-select" id="thresholdOpt${nextID}" name="thresholdOpt${nextID}" placeholder="0" value="0"></select>
<label>Wert/Schwelle</label>
</div>
<button class='btn btn-outline-danger' type='button' id='btnDel${nextID}' data-delid="sensorSettings${nextID}" onclick="delAutoEntry(event)"><i class="bi bi-trash3"></i></button>
</div>`;
document.getElementById("sensorsList").appendChild(div);
fillSensorDD(nextID);
}
function addActor(event){
var t = document.getElementById('actorsList').children;
//get the second last element (ignore "add" button)
nextID = Number(t[t.length-1].id.replace("actorSettings","")) + 1;
var div = document.createElement('div');
div.className = "input-group mt-3";
div.id = "actorSettings"+String(nextID);
div.innerHTML = `<div class="form-floating">
<select class="form-select" id="actorSelect${nextID}" name="actorSelect${nextID}" aria-label="Default select example">
</select>
<label>Aktor</label>
</div>
<div class="form-floating" id="actParamBlock${nextID}">
<select class="form-select" id="actParamSelect${nextID}" name="actParamSelect${nextID}" aria-label="Default select example">
</select>
<label>Eigenschaft</label>
</div>
<button class="btn btn-outline-secondary" style="min-width: 40px;" type="button" id="btnActOperator${nextID}" name="btnActOperator${nextID}">&gt;</button>
<div class="form-floating" id="act1ValBlock${nextID}">
<input type="number" class="form-control" id="act1Value${nextID}" name="act1Value${nextID}" placeholder="0" value="0"></input>
<select class="form-select" id="act1ValueOpt${nextID}" name="act1ValueOpt${nextID}"></select>
<label for="act1Value${nextID}">Wert 1</label>
</div>
<div class="form-floating" id="act2ValBlock{nextID}">
<input type="number" class="form-control" id="act2Value${nextID}" name="act2Value${nextID}" value="0"></input>
<label for="act2Value${nextID}">Wert 2</label>
</div>
<div class="form-floating" id="act3ValBlock{nextID}">
<input type="number" class="form-control" id="act3Value${nextID}" name="act3Value${nextID}" value="0"></input>
<label for="act3Value${nextID}">Wert 3</label>
</div>
<div class="form-floating" id="act4ValBlock{nextID}">
<input type="number" class="form-control" id="act4Value${nextID}" name="act4Value${nextID}" value="0"></input>
<label for="act4Value${nextID}">Wert 4</label>
</div>
<button class='btn btn-outline-danger' type='button' id='btnActDel${nextID}' data-delid="actorSettings${nextID}" onclick="delAutoEntry(event)"><i class="bi bi-trash3"></i></button>`;
document.getElementById("actorsList").appendChild(div);
/*"afterend",`<div class='input-group mb-3' id='actorSettings${nextID}'>
<div class="form-floating">
<select class="form-select" id="actorSelect${nextID}" aria-label="Default select example">
</select>
<label>Aktor</label>
</div>
<div class="form-floating" id="actParamBlock${nextID}">
<select class="form-select" id="actParamSelect${nextID}" aria-label="Default select example">
</select>
<label>Eigenschaft</label>
</div>
<button class="btn btn-outline-secondary " type="button" id="btnActOperator${nextID}">&gt;</button>
<div class="form-floating" id="actValBlock${nextID}">
<input type="number" class="form-control" id="actValue${nextID}" placeholder="0" value="0"></input>
<label>Sollwert</label>
</div>
<button class='btn btn-outline-danger' type='button' id='btnActDel${nextID}' data-delid="actorSettings${nextID}" onclick="delAutoEntry(event)"><i class="bi bi-trash3"></i></button>
</div>`);*/
fillActorDD(nextID);
}
function arrangeSensorInputs(event){
ID = Number(event.currentTarget.id.replace("sensorSelect",""));
sensorID = document.getElementById("sensorSelect"+ID).value;
fetch("./ajax/sensorDetails.php?sensorID="+sensorID, {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
params = JSON.parse(html);
if(params.parameters.length == 1){
document.getElementById("paramBlock"+ID).hidden = true;
}else{
document.getElementById("paramBlock"+ID).hidden = false;
}
document.getElementById("btnOperator"+ID).addEventListener("click", tglOperators);
clearOptions("paramSelect"+ID);
addOptions("paramSelect"+ID,params.parameters);
if(document.getElementById("btnLogic"+ID))
document.getElementById("btnLogic"+ID).addEventListener("click", tglOperators);
document.getElementById("paramSelect"+ID).dispatchEvent(new Event('change'));
})
.catch(error => {
alert(error);
document.getElementById("paramSelect"+ID).innerHTML = "";
});
}
function arrangeActorInputs(event){
ID = Number(event.currentTarget.id.replace("actorSelect",""));
actorID = document.getElementById("actorSelect"+ID).value;
fetch("./ajax/actorDetails.php?actorID="+actorID, {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
params = JSON.parse(html);
document.getElementById("actParamSelect"+ID).innerHTML = "";
if(params.commands.length == 1){
document.getElementById("actParamBlock"+ID).hidden = true;
}else{
document.getElementById("actParamBlock"+ID).hidden = false;
}
clearOptions("actParamSelect"+ID);
addOptions("actParamSelect"+ID,params.commands);
document.getElementById("actParamSelect"+ID).dispatchEvent(new Event('change'));
})
.catch(error => {
document.getElementById("actParamSelect"+ID).innerHTML = "";
});
}
function tglOperators(event){
btn = event.currentTarget;
operators = JSON.parse(btn.dataset.tglstates);
current = btn.innerHTML;
btn.innerHTML = operators[(operators.indexOf(current)+1) % operators.length];
}
function loadAutomatic(params){
if(params.search("action=new")>0){
fillSensorDD("1");
fillActorDD("1");
}
}
function openAutoActionModal(params) {
let contentURL = "./ajax/AutoAction.php"+params;
if(params.search("action=new")>0)
document.getElementById("modal-title").innerHTML = "Neue Automatik anlegen";
else
document.getElementById("modal-title").innerHTML = "Automatik Einstellungen";
modalBodyElement = document.getElementById('modal-body');
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
document.getElementById("modalSaveBtn").contentURL = contentURL;
modalEV.show();
fetch(contentURL, {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
modalBodyElement.innerHTML = html;
loadAutomatic(params);
document.getElementById("btnAddSensor").addEventListener("click", addSensor);
document.getElementById("btnAddActor").addEventListener("click", addActor);
})
.catch(error => {
modalBodyElement.innerHTML += error.message;
});
}
+182
View File
@@ -0,0 +1,182 @@
/*
* Gemeinsame Bausteine der Dashboard-Seiten.
* Wird vor solarMQTT.js / homeMQTT.js / heatMQTT.js / historyMQTT.js geladen.
*/
/** Sekunden als "HH:MM". */
String.prototype.toHHMM = function () {
var sec_num = parseInt(this, 10);
var hours = Math.floor(sec_num / 3600);
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
if (hours < 10) { hours = "0" + hours; }
if (minutes < 10) { minutes = "0" + minutes; }
return hours + ':' + minutes;
};
function powerToString(power) {
if (Math.abs(power) > 999) {
power = power / 1000;
return power.toPrecision(3) + " kW";
} else {
return Math.round(power) + " W";
}
}
function loadingHTML(msg) {
return "<div class='m-4'><div class='spinner-border' role='status'><span class='visually-hidden'>Loading...</span></div>&nbsp;&nbsp;&nbsp;" + msg + "</div>";
}
/**
* Diagrammdaten holen und eintragen.
* params Funktion, die {FROM, TO} liefert (Zeitraum aendert sich zur Laufzeit)
* sunrise true -> Sonnenstands-Markierungen fuer denselben Zeitraum
* Objekt {FROM, TO} -> fester Zeitraum
* autoUpdate alle 5 Minuten neu laden
* updateMode an chart.update() durchgereicht ("none" = ohne Animation)
*/
async function getData(chart, url, options = {}) {
const {
params = null,
sunrise = false,
autoUpdate = true,
updateMode = undefined,
refreshMs = 5 * 60 * 1000
} = options;
const range = params ? params() : null;
const query = range ? "?FROM=" + range.FROM + "&TO=" + range.TO : "";
try {
chart.options.events = []; // waehrend des Ladens keine Tooltips
const response = await fetch(url + query);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
chart.data = await response.json();
if (sunrise) {
const sunRange = sunrise === true ? range : sunrise;
const response2 = await fetch("ajax/getSunrise.php?FROM=" + sunRange.FROM + "&TO=" + sunRange.TO);
if (!response2.ok) {
throw new Error(`Response status: ${response2.status}`);
}
chart.options.plugins.annotation.annotations = await response2.json();
}
chart.options.events = ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'];
chart.update(updateMode);
} catch (error) {
console.log("getData " + url + ": " + error.message);
}
if (autoUpdate) {
setTimeout(function () { getData(chart, url, options); }, refreshMs);
}
}
/** Vorgerendertes HTML-Schnipsel in ein Element laden. */
async function getStats(elem_id, url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
document.getElementById(elem_id).innerHTML = await response.text();
} catch (error) {
console.log("getStats " + url + ": " + error.message);
}
}
/**
* Formular des Modals per POST abschicken.
* Der Start/Stop-Knopf der Ladesteuerung schickt nur sich selbst.
*/
function submitFormAjax(event) {
let xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
setTimeout(function () { modalEV.hide(); }, 700);
if (xmlhttp.responseText) {
alert(xmlhttp.responseText);
}
}
};
let post = "";
if (event.currentTarget.id == "evStart/Stop") {
post = "evStart/Stop=" + event.currentTarget.innerHTML;
} else {
const form = document.getElementById('modalEV').querySelector('form');
for (const element of Array.from(form.elements)) {
if (!element.name) continue;
if (element.type == "radio" || element.type == "checkbox") {
if (element.checked) {
post += element.name + "=" + encodeURIComponent(element.value) + "&";
}
} else if (element.type == "button") {
if (!element.classList.contains("accordion-button")) {
post += element.name + "=" + encodeURIComponent(element.innerHTML) + "&";
}
} else {
post += element.name + "=" + encodeURIComponent(element.value) + "&";
}
}
}
xmlhttp.open("POST", event.currentTarget.contentURL, true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
document.getElementById('modalEV').querySelector('.modal-body').innerHTML = loadingHTML("Änderungen werden übernommen...");
xmlhttp.send(post);
return false;
}
/**
* Schieberegler im Modal mit seiner Beschriftung verbinden.
* label(wert) liefert den anzuzeigenden Text.
*/
function bindModalSlider(slider, span, label) {
slider.oninput = function () {
const min = Number(slider.min);
const max = Number(slider.max);
const percent = (Number(slider.value) - min) * 100 / (max - min);
slider.style.setProperty("--background-size", `${percent}%`);
// Beschriftung mitlaufen lassen, an den Raendern etwas gebremst
let margin = percent;
if (margin < 10) {
margin = margin - 0.4 * margin;
} else if (margin < 85) {
margin = margin - 4;
} else {
margin = margin - 4 - 0.5 * (margin - 85);
}
span.setAttribute('style', 'margin-left:' + margin + '%;');
span.innerHTML = label(slider.value);
};
}
/** Meteoblue-Meteogramm einhängen, sofern die Seite einen Platz dafür hat. */
function mountMeteogram() {
const target = document.getElementById("meteogram");
if (!target) return;
// loading="lazy": das Meteogramm ist ein vollstaendiges Fremd-Dokument und
// liegt unterhalb der Falz - es wird erst geladen, wenn man hinscrollt.
target.innerHTML = "<iframe loading='lazy' src='" + meteogramURL + "' frameborder='0' scrolling=NO' allowtransparency='true' sandbox='allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox' style='width: 100%;height: 500px;border: 0;overflow: hidden;'></iframe><!-- DO NOT REMOVE THIS LINK --><a href='https://www.meteoblue.com/de/wetter/woche/index' target='_blank' rel='noopener'></a>";
}
/**
* Wetterkacheln aktualisieren (restricted/weather_cards.html).
* value(name) liefert den Wert zu weatherStation/<name> - die Seiten legen
* die MQTT-Werte unterschiedlich ab, deshalb der Zugriff über eine Funktion.
*/
function updateWeatherCards(value) {
const windDir = document.getElementById("windDir");
if (!windDir) return; // Seite ohne Wetterkacheln
windDir.style.transform = "rotate(" + (90 + Number(value("windDeg"))) + "deg)";
document.getElementById("windSpd").innerHTML = value("avgWindspeed");
document.getElementById("humidity").innerHTML = value("hum");
document.getElementById("temp").innerHTML = value("tempAmb");
document.getElementById("windDirGust").style.transform = "rotate(" + (90 + Number(value("gustDeg"))) + "deg)";
document.getElementById("windGust").innerHTML = value("maxgust");
document.getElementById("ambPress").innerHTML = Math.round(value("qff") * 10) / 10;
}
+193
View File
@@ -0,0 +1,193 @@
var mqttData = {};
const solarMQTT = {
getMQTT: function () {
const id = Math.random().toString(36).substring(7);
const topic = "#";
const connection = "wss://mqtt.nas.el-wa.org:443"
mqttsolarTreeDone = false;
// const connection = "ws://username:password@37.97.203.138:8083" // Works
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
const client = mqtt.connect(connection, {
rejectUnauthorized: false,
});
client.on("message", messageReceived);
client.on("connect", function () {
client.subscribe("solarManager/#");
client.subscribe("wattpilot/properties/lmo/state");
client.subscribe("wattpilot/properties/ftt/state");
client.subscribe("wattpilot/properties/fte/state");
client.subscribe("wattpilot/properties/amp/state");
client.subscribe("wattpilot/properties/car/state");
client.subscribe("go-eCharger/270003/amp");
client.subscribe("go-eCharger/270003/ate");
client.subscribe("go-eCharger/270003/lmo");
client.subscribe("go-eCharger/270003/att");
client.subscribe("go-eCharger/270003/car");
client.subscribe("weatherStation/#");
});
client.on("error", function (error) {
//alert("MQTT Error: " + error);
});
client.on('end', function () {
setTimeout(getMQTT, 5000);
alert("MQTT Disconnected, try to reconnect in 5 secs.");
})
function getNestedProp(obj, path) {
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
}
function setNestedProp(obj, path, value) {
var schema = obj; // a moving reference to internal objects within obj
var pList = path.split('/');
var len = pList.length;
for (var i = 0; i < len - 1; i++) {
var elem = pList[i];
if (!schema[elem]) schema[elem] = {}
schema = schema[elem];
}
schema[pList[len - 1]] = value;
}
function messageReceived(topic, message) {
setNestedProp(mqttData, topic, message);
if (topic == "solarManager/P_Load") {
setTimeout(function () { solarSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
} else if (topic == "weatherStation/windDeg") {
setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation"]?.[name]) }, 200);
}
}
}
}
const solarSVG = {
updateCnt: 99,
updateValuesMQTT: function (mqttData) {
if (this.updateCnt > 10) {
this.updateCnt = 0;
var obj = document.querySelector("object");
var htmlNode = obj.contentDocument;
htmlNode.getElementById("PufferOtxt").innerHTML = mqttData["solarManager"]["t_buffT"] + " °C";
htmlNode.getElementById("PufferMtxt").innerHTML = mqttData["solarManager"]["t_buffM"] + " °C";
htmlNode.getElementById("PufferUtxt").innerHTML = mqttData["solarManager"]["t_buffB"] + " °C";
htmlNode.getElementById("heaterVL").innerHTML = mqttData["solarManager"]["t_heatVL"] + " °C";
htmlNode.getElementById("heaterRL").innerHTML = mqttData["solarManager"]["t_heatRL"] + " °C";
htmlNode.getElementById("thermeVLfb").innerHTML = mqttData["solarManager"]["t_gasVLu"] + " °C";
htmlNode.getElementById("thermeVLww").innerHTML = mqttData["solarManager"]["t_gasVLo"] + " °C";
htmlNode.getElementById("thermeRL").innerHTML = mqttData["solarManager"]["t_gasRL"] + " °C";
htmlNode.getElementById("fbVL").innerHTML = mqttData["solarManager"]["t_fbVL"] + " °C";
htmlNode.getElementById("fbRL").innerHTML = mqttData["solarManager"]["t_fbRL"] + " °C";
htmlNode.getElementById("triac").innerHTML = mqttData["solarManager"]["t_triac"] + " °C";
}
this.updateCnt++;
}
}
var chartSettings = {
type: 'line',
options: {
animation: true,
plugins: {
annotation: {
common: { type: 'box', drawTime: 'beforeDatasetsDraw', yScaleID: 'y-axis-0', backgroundColor: 'rgba(255, 255, 255, 0.05)', init: true },
annotations: []
},
tooltip: {
position: 'nearest',
pointStyle: "circle",
boxWidth: 4,
usePointStyle: true,
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.dataset.yAxisID == "y1") {
label += Math.round(context.parsed.y * 10) / 10 + " " + "L/min";
} else {
if (context.parsed.y !== null) {
ret = scale(Math.round(context.parsed.y), false);
label += ret[0] + " " + ret[1] + "°C";
}
}
return label;
},
},
},
legend: {
position: "bottom",
labels: {
pointStyleWidth: 10,
usePointStyle: true,
pointStyle: "line",
}
},
},
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
scales: {
x: {
adapters: {
date: {
locale: "DE-de"
}
},
ticks: {
},
type: 'timestack',
},
y: {
stacked: false,
display: true,
position: 'left',
ticks: {
callback: value => `${value} °C`,
},
title: {
display: true,
text: "Temperatur"
}
},
y1: {
stacked: false,
display: true,
position: 'right',
ticks: {
callback: value => `${value} L/min`,
},
title: {
display: true,
text: "Wasserverbrauch"
},
data:{}
}
}
}
};
var chartData = {};
const heatChart = new Chart(
document.querySelector('#heat-chart'),
Object.assign({}, chartSettings)
);
const waterChart = new Chart(
document.querySelector('#water-chart'),
Object.assign({}, chartSettings)
);
document.addEventListener('readystatechange', function () {
if (event.target.readyState === "complete") {
solarMQTT.getMQTT();
getData(heatChart, 'ajax/getHeaterData.php', { sunrise: { FROM: -24, TO: 0 } });
getData(waterChart, 'ajax/getWaterData.php', { sunrise: { FROM: -24, TO: 0 } });
}
});
+152
View File
@@ -0,0 +1,152 @@
tooltipLabel = function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.dataset.yAxisID == "y1") {
label += Math.round(context.parsed.y * 10) / 10 + " " + "%";
} else {
if (context.parsed.y !== null) {
ret = scale(Math.round(context.parsed.y), false);
label += ret[0] + " " + ret[1] + "Wh";
}
}
return label;
};
tooltipFooter = function (tooltipItems){
let sum = 0;
tooltipItems.forEach(function(tooltipItem) {
if (tooltipItem.dataset.yAxisID != "y1") {
sum += tooltipItem.parsed.y;
}
});
ret = scale(Math.round(sum), false);
sum = ret[0] + " " + ret[1] + "Wh";
return 'Summe: ' + sum;
}
legendLabels = function(chart){
const datasets = chart.data.datasets;
const {
labels: {
usePointStyle,
pointStyle,
textAlign,
color
}
} = chart.legend.options;
return chart._getSortedDatasetMetas().map((meta) => {
const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);
const borderWidth = Chart.helpers.toPadding(style.borderWidth);
ret = scale(Math.round(arraySum(datasets[meta.index].data)), false);
ret2 = scale(Math.round(datasets[meta.index].data[datasets[meta.index].data.length-1]), false);
return {
text: datasets[meta.index].label + " Σ " + ret[0]+" "+ret[1]+"Wh",//+ " Last: " + ret2[0]+" "+ret2[1]+"W",
fillStyle: style.backgroundColor,
fontColor: color,
hidden: !meta.visible,
lineCap: style.borderCapStyle,
lineDash: style.borderDash,
lineDashOffset: style.borderDashOffset,
lineJoin: style.borderJoinStyle,
lineWidth: (borderWidth.width + borderWidth.height) / 4,
strokeStyle: style.borderColor,
pointStyle: pointStyle || style.pointStyle,
rotation: style.rotation,
textAlign: textAlign || style.textAlign,
borderRadius: 0, // TODO: v4, default to style.borderRadius
datasetIndex: meta.index
};
}, this);
}
Chart.defaults.plugins.tooltip.callbacks.footer = tooltipFooter;
Chart.defaults.plugins.tooltip.callbacks.label = tooltipLabel;
Chart.defaults.plugins.legend.labels.generateLabels = legendLabels;
var forecastChartSettings = {
type: 'bar',
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
scales: {
y: {
stacked: true,
display: true,
min: 0,
suggestedMax: 1000,
ticks: {
callback: value => `${value / 1000} kWh`,
}
},
}
}
};
var decadeChartSettings = {
type: 'bar',
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
scales: {
y: {
stacked: true,
display: true,
min: 0,
suggestedMax: 1000000,
ticks: {
callback: value => `${value / 1000000} MWh`,
}
},
}
}
};
var chartData = {};
const consChart = new Chart(
document.querySelector('#consumption-chart'),
Object.assign({}, forecastChartSettings)
);
const prodChart = new Chart(
document.querySelector('#production-chart'),
Object.assign({}, forecastChartSettings)
);
const consChartYear = new Chart(
document.querySelector('#consumption-chart-year'),
Object.assign({}, decadeChartSettings)
);
const prodChartYear = new Chart(
document.querySelector('#production-chart-year'),
Object.assign({}, decadeChartSettings)
);
const consChartDecade = new Chart(
document.querySelector('#consumption-chart-decade'),
Object.assign({}, decadeChartSettings)
);
const prodChartDecade = new Chart(
document.querySelector('#production-chart-decade'),
Object.assign({}, decadeChartSettings)
);
document.addEventListener('readystatechange', function () {
if (event.target.readyState === "complete") {
getData(prodChart, 'ajax/energyHistory.php?series=prod&range=month');
getData(consChart, 'ajax/energyHistory.php?series=cons&range=month');
getData(prodChartYear, 'ajax/energyHistory.php?series=prod&range=year');
getData(consChartYear, 'ajax/energyHistory.php?series=cons&range=year');
getData(prodChartDecade, 'ajax/energyHistory.php?series=prod&range=decade');
getData(consChartDecade, 'ajax/energyHistory.php?series=cons&range=decade');
getStats("Stats-Year","ajax/getStats.php?type=ThisYear");
getStats("Stats-Lastyear","ajax/getStats.php?type=LastYear");
getStats("Stats-Prelastyear","ajax/getStats.php?type=PreLastYear");
}
});
+197
View File
@@ -0,0 +1,197 @@
const homeMQTT = {
getMQTT: function () {
const id = Math.random().toString(36).substring(7);
const topic = "#";
const connection = "wss://mqtt.nas.el-wa.org:443"
mqttsolarTreeDone = false;
// const connection = "ws://username:password@37.97.203.138:8083" // Works
// const connection = "wss://public:public@public.cloud.shiftr.io" // Works
const client = mqtt.connect(connection, {
rejectUnauthorized: false,
});
client.on("message", messageReceived);
client.on("connect", function () {
client.subscribe("Raumtemp/#");
});
client.on("error", function (error) {
//alert("MQTT Error: " + error);
});
client.on('end', function () {
setTimeout(getMQTT, 5000);
alert("MQTT Disconnected, try to reconnect in 5 secs.");
})
function getNestedProp(obj, path) {
return path.split('/').reduce((acc, key) => acc && acc[key], obj);
}
function setNestedProp(obj, path, value) {
var schema = obj; // a moving reference to internal objects within obj
var pList = path.split('/');
var len = pList.length;
for (var i = 0; i < len - 1; i++) {
var elem = pList[i];
if (!schema[elem]) schema[elem] = {}
schema = schema[elem];
}
schema[pList[len - 1]] = value;
}
function messageReceived(topic, message) {
setNestedProp(mqttData, topic, message);
setTimeout(function () { homeSVG.updateValuesMQTT(mqttData) }, 200); //give the object tree some time to build up and receive all values
}
}
}
const homeSVG = {
updateCnt: 99,
fillElementArray: function () {
},
updateValuesMQTT: function (mqttData) {
// homeRooms liefert restricted/rooms.php (in home.php eingebettet).
// Jeder Raum bekommt die gleiche Behandlung - Raeume kommen dort dazu,
// nicht hier.
homeRooms.forEach(function (room) {
const values = mqttData["Raumtemp"]?.[room.floor]?.[room.mqtt];
if (!values) return; // Topic noch nicht empfangen
const el = (part) => document.getElementById(room.floor + "_" + part);
if (typeof (values["Heating"]) != "undefined")
el(room.id + "_heater").setAttribute("display", values["Heating"] == "false" ? "none" : "");
if (typeof (values["mode"]) != "undefined")
el(room.id + "_buffer").setAttribute("display", values["mode"] == "Overheating" ? "" : "none");
if (typeof (values["Set Temp[degC]"]) != "undefined")
el("pres_" + room.id).innerHTML = Math.floor(values["Set Temp[degC]"]) + "\u2009\u00b0C";
if (typeof (values["Temp[degC]"]) != "undefined")
el("tmp_" + room.id).innerHTML = values["Temp[degC]"] + "\u2009\u00b0C";
if (typeof (values["rHum[%]"]) != "undefined")
el("hum_" + room.id).innerHTML = values["rHum[%]"] + "\u2009%rF";
});
}
}
var currentFloor = "OG";
function addClass(el, classNameToAdd){
el.className += ' ' + classNameToAdd;
}
function removeClass(el, classNameToRemove){
var elClass = ' ' + el.className + ' ';
while(elClass.indexOf(' ' + classNameToRemove + ' ') !== -1){
elClass = elClass.replace(' ' + classNameToRemove + ' ', '');
}
el.className = elClass;
}
function switchTab(newtab){
newContent = document.getElementById(newtab);
newTabBtn = document.getElementById(newtab+"-tab");
removeClass(document.getElementById("actions-OG-tab"),"active");
removeClass(document.getElementById("actions-EG-tab"),"active");
removeClass(document.getElementById("actions-UG-tab"),"active");
removeClass(document.getElementById("actions-OG"),"active show");
removeClass(document.getElementById("actions-EG"),"active show");
removeClass(document.getElementById("actions-UG"),"active show");
addClass(newTabBtn,"active");
addClass(newContent,"active show");
}
function switchFloor(floor){
if(currentFloor == floor)
return;
const targetIn = document.getElementById(floor+'_Info');
const targetOut = document.getElementById(currentFloor+'_Info');
var blendIn = new KeyframeEffect(
targetIn, [{opacity: '0'},{opacity: '100'}],
{
duration: 500,
easing: "ease-in-out",
fill: "forwards",
iterations: 1,
}
);
var blendOut = new KeyframeEffect(
targetOut, [{opacity: '100'},{opacity: '0'}],
{
duration: 500,
easing: "ease-in-out",
fill: "forwards",
iterations: 1,
}
);
var inAnim = new Animation(blendIn,document.timeline);
var outAnim = new Animation(blendOut,document.timeline);
targetIn.setAttribute("display","");
outAnim.onfinish= (event) => {
targetOut.setAttribute("display","none");
};
inAnim.play();
outAnim.play();
currentFloor = floor;
}
var modalEV = new bootstrap.Modal(document.getElementById('modalEV'), {
keyboard: false
});
var offcanvas = new bootstrap.Offcanvas(document.getElementById('offcanvas'), {
keyboard: false
});
var mqttData = {};
function openHeaterSettings(heater){
openModal(heater)
}
document.addEventListener('readystatechange', function () {
if (event.target.readyState === "complete") {
homeMQTT.getMQTT();
mountMeteogram();
}
});
function openModal(heater) {
var contentURL = "./ajax/roomtemp.php?heater="+heater;
let heaterMQTT = heater.toString().split("_");
document.getElementById("modal-title").innerHTML = "Thermostat "+heater.replace("_"," ").replace("ue","ü").replace("ae","ä").replace("oe","ö");
modalBodyElement = document.getElementById("modal-body");
modalBodyElement.innerHTML = loadingHTML("Wird geladen...");
document.getElementById("modalSaveBtn").addEventListener("click", submitFormAjax);
document.getElementById("modalSaveBtn").contentURL = contentURL;
modalEV.show();
fetch(contentURL, {
method: 'GET',
headers: {
'X-Requested-From-Modal': 'a',
'Requested-With-Ajax': 'ajax'
}
})
.then(response => response.text())
.then(html => {
modalBodyElement.innerHTML = html;
var slider = document.getElementById("modal-slider");
var span = document.getElementById("modal-slider-label");
bindModalSlider(slider, span, value => value + " °C");
if(typeof(mqttData["Raumtemp"][heaterMQTT[0]][heaterMQTT[1]]["Set Temp[degC]"])!= "undefined")
slider.setAttribute("value",mqttData["Raumtemp"][heaterMQTT[0]][heaterMQTT[1]]["Set Temp[degC]"]);
else
slider.setAttribute("value",10);
slider.dispatchEvent(new Event('input'));
})
.catch(error => {
modalBodyElement.innerHTML += error.message;
});
}
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
/*
* Wetterseite: Messwerte der eigenen Wetterstation und das Meteogramm.
* Die Anzeige selbst steckt in updateWeatherCards() / mountMeteogram()
* in common.js, hier bleibt nur die MQTT-Anbindung.
*/
var mqttData = {};
const weatherMQTT = {
getMQTT: function () {
const client = mqtt.connect("wss://mqtt.nas.el-wa.org:443", { rejectUnauthorized: false });
client.on("connect", function () {
client.subscribe("weatherStation/#");
});
client.on("message", function (topic, message) {
mqttData[topic] = message;
if (topic == "weatherStation/windDeg") {
// etwas warten, bis die uebrigen Werte eingetroffen sind
setTimeout(function () { updateWeatherCards(name => mqttData["weatherStation/" + name]) }, 200);
}
});
client.on("error", function (error) {
console.log("MQTT: " + error.message);
});
}
};
document.addEventListener('readystatechange', function () {
if (event.target.readyState === "complete") {
weatherMQTT.getMQTT();
mountMeteogram();
}
});
+1
View File
@@ -0,0 +1 @@
deny from all
+8
View File
@@ -0,0 +1,8 @@
# Netbeans project
nbproject/
/index.php
# .pem files from FIDO Alliance Metadata Service (MDS)
_test/rootCertificates/mds/*.pem
_test/rootCertificates/mds/lastMdsFetch.txt
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright © 2022 Lukas Buchs
Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part)
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.
+144
View File
@@ -0,0 +1,144 @@
[![Licensed under the MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/lbuchs/WebAuthn/blob/master/LICENSE)
[![Requires PHP 7.1.0](https://img.shields.io/badge/PHP-7.1.0-green.svg)](https://php.net)
[![Last Commit](https://img.shields.io/github/last-commit/lbuchs/WebAuthn.svg)](https://github.com/lbuchs/WebAuthn/commits/master)
# WebAuthn
*A simple PHP WebAuthn (FIDO2) server library*
Goal of this project is to provide a small, lightweight, understandable library to protect logins with passkeys, security keys like Yubico or Solo, fingerprint on Android or Windows Hello.
## Manual
See /_test for a simple usage of this library. Check [webauthn.lubu.ch](https://webauthn.lubu.ch) for a working example.
### Supported attestation statement formats
* android-key &#x2705;
* android-safetynet &#x2705;
* apple &#x2705;
* fido-u2f &#x2705;
* none &#x2705;
* packed &#x2705;
* tpm &#x2705;
> [!NOTE]
> This library supports authenticators which are signed with a X.509 certificate or which are self attested. ECDAA is not supported.
## Workflow
JAVASCRIPT | SERVER
------------------------------------------------------------
REGISTRATION
window.fetch -----------------> getCreateArgs
|
navigator.credentials.create <-------------'
|
'-------------------------> processCreate
|
alert ok or fail <----------------'
------------------------------------------------------------
VALIDATION
window.fetch ------------------> getGetArgs
|
navigator.credentials.get <----------------'
|
'-------------------------> processGet
|
alert ok or fail <----------------'
## Attestation
Typically, when someone logs in, you only need to confirm that they are using the same device they used during
registration. In this scenario, you do not require any form of attestation.
However, if you need additional security, such as when your company mandates the use of a Solokey for login,
you can verify its authenticity through direct attestation. Companies may also purchase authenticators that
are signed with their own root certificate, enabling them to validate that an authenticator is affiliated with
their organization.
### no attestation
just verify that the device is the same device used on registration.
You can use 'none' attestation with this library if you only check 'none' as format.
> [!TIP]
> this is propably what you want to use if you want secure login for a public website.
### indirect attestation
the browser may replace the AAGUID and attestation statement with a more privacy-friendly and/or more easily
verifiable version of the same data (for example, by employing an anonymization CA).
You can not validate against any root ca, if the browser uses a anonymization certificate.
this library sets attestation to indirect, if you select multiple formats but don't provide any root ca.
> [!TIP]
> hybrid soultion, clients may be discouraged by browser warnings but then you know what device they're using (statistics rulez!)
### direct attestation
the browser proviedes data about the identificator device, the device can be identified uniquely. User could be tracked over multiple sites, because of that the browser may show a warning message about providing this data when register.
this library sets attestation to direct, if you select multiple formats and provide root ca's.
> [!TIP]
> this is probably what you want if you know what devices your clients are using and make sure that only this devices are used.
## Passkeys / Client-side discoverable Credentials
A Client-side discoverable Credential Source is a public key credential source whose credential private key is stored in the authenticator,
client or client device. Such client-side storage requires a resident credential capable authenticator.
This is only supported by FIDO2 hardware, not by older U2F hardware.
>[!NOTE]
>Passkeys is a technique that allows sharing credentials stored on the device with other devices. So from a technical standpoint of the server, there is no difference to client-side discoverable credentials. The difference is only that the phone or computer system is automatically syncing the credentials between the user’s devices via a cloud service. The cross-device sync of passkeys is managed transparently by the OS.
### How does it work?
In a typical server-side key management process, a user initiates a request by entering their username and, in some cases, their password.
The server validates the user's credentials and, upon successful authentication, retrieves a list of all public key identifiers associated with that user account.
This list is then returned to the authenticator, which selects the first credential identifier it issued and responds with a signature that can be verified using the public key registered during the registration process.
In a client-side key process, the user does not need to provide a username or password.
Instead, the authenticator searches its own memory to see if it has saved a key for the relying party (domain).
If a key is found, the authentication process proceeds in the same way as it would if the server had sent a list
of identifiers. There is no difference in the verification process.
### How can I use it with this library?
#### on registration
When calling `WebAuthn\WebAuthn->getCreateArgs`, set `$requireResidentKey` to true,
to notify the authenticator that he should save the registration in its memory.
#### on login
When calling `WebAuthn\WebAuthn->getGetArgs`, don't provide any `$credentialIds` (the authenticator will look up the ids in its own memory and returns the user ID as userHandle).
Set the type of authenticator to `hybrid` (Passkey scanned via QR Code) and `internal` (Passkey stored on the device itself).
#### disadvantage
The RP ID (= domain) is saved on the authenticator. So If an authenticator is lost, its theoretically possible to find the services, which the authenticator is used and login there.
### device support
Availability of built-in passkeys that automatically synchronize to all of a user’s devices: (see also [passkeys.dev/device-support](https://passkeys.dev/device-support/))
* Apple iOS 16+ / iPadOS 16+ / macOS Ventura+
* Android 9+
* Microsoft Windows 11 23H2+
## Requirements
* PHP >= 8.0 with [OpenSSL](http://php.net/manual/en/book.openssl.php) and [Multibyte String](https://www.php.net/manual/en/book.mbstring.php)
* Browser with [WebAuthn support](https://caniuse.com/webauthn) (Firefox 60+, Chrome 67+, Edge 18+, Safari 13+)
* PHP [Sodium](https://www.php.net/manual/en/book.sodium.php) (or [Sodium Compat](https://github.com/paragonie/sodium_compat) ) for [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) support
## Infos about WebAuthn
* [Wikipedia](https://en.wikipedia.org/wiki/WebAuthn)
* [W3C](https://www.w3.org/TR/webauthn/)
* [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
* [dev.yubico](https://developers.yubico.com/FIDO2/)
* [FIDO Alliance](https://fidoalliance.org)
* [passkeys](https://passkeys.dev/)
## FIDO2 Hardware
* [Yubico](https://www.yubico.com)
* [Solo](https://solokeys.com) Open Source!
* [Nitrokey](https://www.nitrokey.com/)
* [Feitan](https://fido.ftsafe.com/)
* [TrustKey](https://www.trustkeysolutions.com)
* [Google Titan](https://cloud.google.com/titan-security-key)
* [Egis](https://www.egistec.com/u2f-solution/)
* [OneSpan](https://www.vasco.com/products/two-factor-authenticators/hardware/one-button/digipass-secureclick.html)
* [Hypersecu](https://hypersecu.com/tmp/products/hyperfido)
* [Kensington VeriMark™](https://www.kensington.com/)
* [Token2](https://www.token2.com/shop/category/fido2-keys)
+547
View File
@@ -0,0 +1,547 @@
<!DOCTYPE html>
<!--
Copyright (C) 2022 Lukas Buchs
license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
-->
<html>
<head>
<title>lbuchs/WebAuthn Test</title>
<meta charset="UTF-8">
<script>
/**
* creates a new FIDO2 registration
* @returns {undefined}
*/
async function createRegistration() {
try {
// check browser support
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error('Browser not supported.');
}
// get create args
let rep = await window.fetch('server.php?fn=getCreateArgs' + getGetParams(), {method:'GET', cache:'no-cache'});
const createArgs = await rep.json();
// error handling
if (createArgs.success === false) {
throw new Error(createArgs.msg || 'unknown error occured');
}
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(createArgs);
// create credentials
const cred = await navigator.credentials.create(createArgs);
// create object
const authenticatorAttestationResponse = {
transports: cred.response.getTransports ? cred.response.getTransports() : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
};
// check auth on server side
rep = await window.fetch('server.php?fn=processCreate' + getGetParams(), {
method : 'POST',
body : JSON.stringify(authenticatorAttestationResponse),
cache : 'no-cache'
});
const authenticatorAttestationServerResponse = await rep.json();
// prompt server response
if (authenticatorAttestationServerResponse.success) {
reloadServerPreview();
window.alert(authenticatorAttestationServerResponse.msg || 'registration success');
} else {
throw new Error(authenticatorAttestationServerResponse.msg);
}
} catch (err) {
reloadServerPreview();
window.alert(err.message || 'unknown error occured');
}
}
/**
* checks a FIDO2 registration
* @returns {undefined}
*/
async function checkRegistration() {
try {
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error('Browser not supported.');
}
// get check args
let rep = await window.fetch('server.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'});
const getArgs = await rep.json();
// error handling
if (getArgs.success === false) {
throw new Error(getArgs.msg);
}
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(getArgs);
// check credentials with hardware
const cred = await navigator.credentials.get(getArgs);
// create object for transmission to server
const authenticatorAttestationResponse = {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
};
// send to server
rep = await window.fetch('server.php?fn=processGet' + getGetParams(), {
method:'POST',
body: JSON.stringify(authenticatorAttestationResponse),
cache:'no-cache'
});
const authenticatorAttestationServerResponse = await rep.json();
// check server response
if (authenticatorAttestationServerResponse.success) {
reloadServerPreview();
window.alert(authenticatorAttestationServerResponse.msg || 'login success');
} else {
throw new Error(authenticatorAttestationServerResponse.msg);
}
} catch (err) {
reloadServerPreview();
window.alert(err.message || 'unknown error occured');
}
}
function clearRegistration() {
window.fetch('server.php?fn=clearRegistrations' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(json) {
if (json.success) {
reloadServerPreview();
window.alert(json.msg);
} else {
throw new Error(json.msg);
}
}).catch(function(err) {
reloadServerPreview();
window.alert(err.message || 'unknown error occured');
});
}
function queryFidoMetaDataService() {
window.fetch('server.php?fn=queryFidoMetaDataService' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(json) {
if (json.success) {
window.alert(json.msg);
} else {
throw new Error(json.msg);
}
}).catch(function(err) {
window.alert(err.message || 'unknown error occured');
});
}
/**
* convert RFC 1342-like base64 strings to array buffer
* @param {mixed} obj
* @returns {undefined}
*/
function recursiveBase64StrToArrayBuffer(obj) {
let prefix = '=?BINARY?B?';
let suffix = '?=';
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'string') {
let str = obj[key];
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
str = str.substring(prefix.length, str.length - suffix.length);
let binary_string = window.atob(str);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
obj[key] = bytes.buffer;
}
} else {
recursiveBase64StrToArrayBuffer(obj[key]);
}
}
}
}
/**
* Convert a ArrayBuffer to Base64
* @param {ArrayBuffer} buffer
* @returns {String}
*/
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa(binary);
}
/**
* Get URL parameter
* @returns {String}
*/
function getGetParams() {
let url = '';
url += '&apple=' + (document.getElementById('cert_apple').checked ? '1' : '0');
url += '&yubico=' + (document.getElementById('cert_yubico').checked ? '1' : '0');
url += '&solo=' + (document.getElementById('cert_solo').checked ? '1' : '0');
url += '&hypersecu=' + (document.getElementById('cert_hypersecu').checked ? '1' : '0');
url += '&google=' + (document.getElementById('cert_google').checked ? '1' : '0');
url += '&microsoft=' + (document.getElementById('cert_microsoft').checked ? '1' : '0');
url += '&mds=' + (document.getElementById('cert_mds').checked ? '1' : '0');
url += '&requireResidentKey=' + (document.getElementById('requireResidentKey').checked ? '1' : '0');
url += '&type_usb=' + (document.getElementById('type_usb').checked ? '1' : '0');
url += '&type_nfc=' + (document.getElementById('type_nfc').checked ? '1' : '0');
url += '&type_ble=' + (document.getElementById('type_ble').checked ? '1' : '0');
url += '&type_int=' + (document.getElementById('type_int').checked ? '1' : '0');
url += '&type_hybrid=' + (document.getElementById('type_hybrid').checked ? '1' : '0');
url += '&fmt_android-key=' + (document.getElementById('fmt_android-key').checked ? '1' : '0');
url += '&fmt_android-safetynet=' + (document.getElementById('fmt_android-safetynet').checked ? '1' : '0');
url += '&fmt_apple=' + (document.getElementById('fmt_apple').checked ? '1' : '0');
url += '&fmt_fido-u2f=' + (document.getElementById('fmt_fido-u2f').checked ? '1' : '0');
url += '&fmt_none=' + (document.getElementById('fmt_none').checked ? '1' : '0');
url += '&fmt_packed=' + (document.getElementById('fmt_packed').checked ? '1' : '0');
url += '&fmt_tpm=' + (document.getElementById('fmt_tpm').checked ? '1' : '0');
url += '&rpId=' + encodeURIComponent(document.getElementById('rpId').value);
url += '&userId=' + encodeURIComponent(document.getElementById('userId').value);
url += '&userName=' + encodeURIComponent(document.getElementById('userName').value);
url += '&userDisplayName=' + encodeURIComponent(document.getElementById('userDisplayName').value);
if (document.getElementById('userVerification_required').checked) {
url += '&userVerification=required';
} else if (document.getElementById('userVerification_preferred').checked) {
url += '&userVerification=preferred';
} else if (document.getElementById('userVerification_discouraged').checked) {
url += '&userVerification=discouraged';
}
return url;
}
function reloadServerPreview() {
let iframe = document.getElementById('serverPreview');
iframe.src = iframe.src;
}
function setAttestation(attestation) {
let inputEls = document.getElementsByTagName('input');
for (const inputEl of inputEls) {
if (inputEl.id && inputEl.id.match(/^(fmt|cert)\_/)) {
inputEl.disabled = !attestation;
}
if (inputEl.id && inputEl.id.match(/^fmt\_/)) {
inputEl.checked = attestation ? inputEl.id !== 'fmt_none' : inputEl.id === 'fmt_none';
}
if (inputEl.id && inputEl.id.match(/^cert\_/)) {
inputEl.checked = attestation ? inputEl.id === 'cert_mds' : false;
}
}
}
/**
* force https on load
* @returns {undefined}
*/
window.onload = function() {
if (!window.isSecureContext && location.protocol !== 'https:') {
location.href = location.href.replace('http://', 'https://');
}
if (!document.getElementById('rpId').value) {
document.getElementById('rpId').value = location.hostname;
}
if (!document.getElementById('attestation_yes').checked) {
setAttestation(false);
}
}
</script>
<style>
body {
font-family:sans-serif;
margin: 0 20px;
padding: 0;
}
.splitter {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0;
padding: 0;
}
.splitter > .form {
flex: 1;
min-width: 600px;
}
.splitter > .serverPreview {
width: 740px;
min-height: 700px;
margin: 0;
padding: 0;
border: 1px solid grey;
display: flex;
flex-direction: column;
}
.splitter > .serverPreview iframe {
width: 700px;
flex: 1;
border: 0;
}
</style>
</head>
<body>
<h1 style="margin: 40px 10px 2px 0;">lbuchs/WebAuthn</h1>
<div style="font-style: italic;">A simple PHP WebAuthn (FIDO2) server library.</div>
<div class="splitter">
<div class="form">
<div>&nbsp;</div>
<div>&nbsp;</div>
<div>Simple working demo for the <a href="https://github.com/lbuchs/WebAuthn">lbuchs/WebAuthn</a> library.</div>
<div>
<div>&nbsp;</div>
<table>
<tbody><tr>
<td>
<button type="button" onclick="createRegistration()">&#10133; new registration</button>
</td>
<td>
<button type="button" onclick="checkRegistration()">&#10068; check registration</button>
</td>
<td>
<button type="button" onclick="clearRegistration()">&#9249; clear all registrations</button>
</td>
</tr>
</tbody>
</table>
<div>&nbsp;</div>
<div>
<input type="checkbox" id="requireResidentKey" name="requireResidentKey">
<label for="requireResidentKey">Use Client-side discoverable Credentials / Passkeys
<i style="font-size: 0.8em;">Warning: most client-side modules and authenticators don't allow to delete single client side credentials.
You have to reset the full device to remove the credentials again.</i>
</label>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">Relying Party</div>
<p style="margin:0 0 5px 0;font-size:0.9em;font-style: italic;">A valid domain string that identifies the
WebAuthn Relying Party<br/>on whose behalf a given registration or authentication ceremony is being performed.</p>
<div>
<label for="rpId">RP ID:</label>
<input type="text" id="rpId" name="rpId" value="">
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">User</div>
<div style="margin-bottom:12px">
<label for="userId">User ID (Hex):</label>
<input type="text" id="userId" name="userId" value="64656d6f64656d6f" required pattern="[0-9a-fA-F]{2,}">
<i style="font-size: 0.8em;">You get the user ID back when checking registration (as userHandle), if you're using client-side discoverable credentials.
You can identify with this ID the user who wants to login.
A user handle is an opaque byte sequence with a maximum size of 64 bytes, and is not meant to be displayed to the user.
The user handle MUST NOT contain personally identifying information about the user, such as a username or e-mail address.</i>
</div>
<div style="margin-bottom:12px">
<label for="userName">User Name:</label>
<input type="text" id="userName" name="userName" value="demo" required pattern="[0-9a-zA-Z]{2,}">
<i style="font-size: 0.8em;">only for display, i.e., aiding the user in determining the difference between user accounts with similar display names.</i>
</div>
<div style="margin-bottom:6px">
<label for="userDisplayName">User Display Name:</label>
<input type="text" id="userDisplayName" name="userDisplayName" value="Demo Demolin" required>
<i style="font-size: 0.8em;">A human-palatable name for the user account, intended only for display.</i>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">user verification</div>
<div>
<input type="radio" id="userVerification_required" name="userVerification">
<label for="userVerification_required">required <i style="font-size: 0.8em;">User verification is required (e.g. by pin), the operation will fail if the response does not have the UV flag.</i></label>
</div>
<div>
<input type="radio" id="userVerification_preferred" name="userVerification">
<label for="userVerification_preferred">preferred <i style="font-size: 0.8em;">user verification is prefered, the operation will not fail if the response does not have the UV flag.</i></label>
</div>
<div>
<input type="radio" id="userVerification_discouraged" name="userVerification" checked>
<label for="userVerification_discouraged">discouraged <i style="font-size: 0.8em;">user verification should not be employed as to minimize the user interaction during the process.</i></label>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">type of authenticator</div>
<div>
<input type="checkbox" id="type_usb" name="type_usb" checked>
<label for="type_usb">USB</label>
</div>
<div>
<input type="checkbox" id="type_nfc" name="type_nfc" checked>
<label for="type_nfc">NFC</label>
</div>
<div>
<input type="checkbox" id="type_ble" name="type_ble" checked>
<label for="type_ble">BLE</label>
</div>
<div>
<input type="checkbox" id="type_hybrid" name="type_hybrid" checked>
<label for="type_hybrid">hybrid <i style="font-size: 0.8em;">Passkeys via mobile device, ...</i></label>
</div>
<div>
<input type="checkbox" id="type_int" name="type_int" checked>
<label for="type_int">internal <i style="font-size: 0.8em;">Passkeys on the device, Windows Hello, Android SafetyNet, Apple, ...</i></label>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">attestation</div>
<div>
<input type="radio" id="attestation_yes" value="yes" name="attestation" onchange="setAttestation(this.value === 'yes')" checked>
<label for="attestation_yes">yes</label>
</div>
<div>
<input type="radio" id="attestation_no" value="no" name="attestation" onchange="setAttestation(this.value === 'yes')">
<label for="attestation_no">no</label>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">attestation statement format</div>
<div>
<input type="checkbox" id="fmt_android-key" name="fmt_android-key" checked>
<label for="fmt_android-key">android-key</label>
</div>
<div>
<input type="checkbox" id="fmt_android-safetynet" name="fmt_android-safetynet" checked>
<label for="fmt_android-safetynet">android-safetynet</label>
</div>
<div>
<input type="checkbox" id="fmt_apple" name="fmt_apple" checked>
<label for="fmt_apple">apple</label>
</div>
<div>
<input type="checkbox" id="fmt_fido-u2f" name="fmt_fido-u2f" checked>
<label for="fmt_fido-u2f">fido-u2f</label>
</div>
<div>
<input type="checkbox" id="fmt_none" name="fmt_none">
<label for="fmt_none">none</label>
</div>
<div>
<input type="checkbox" id="fmt_packed" name="fmt_packed" checked>
<label for="fmt_packed">packed</label>
</div>
<div>
<input type="checkbox" id="fmt_tpm" name="fmt_tpm" checked>
<label for="fmt_tpm">tpm</label>
</div>
<div>&nbsp;</div>
<div style="font-weight: bold">attestation root certificates</div>
<div>
<input type="checkbox" id="cert_mds" name="mds" checked>
<label for="cert_mds">Accept keys signed by <a href="https://fidoalliance.org/metadata/" target="_blank">FIDO Alliance Metadata Service</a></label>
</div>
<div>
<input type="checkbox" id="cert_apple" name="apple">
<label for="cert_apple">Accept keys signed by apple root ca</label>
</div>
<div>
<input type="checkbox" id="cert_yubico" name="yubico">
<label for="cert_yubico">Accept keys signed by yubico root ca</label>
</div>
<div>
<input type="checkbox" id="cert_solo" name="solo">
<label for="cert_solo">Accept keys signed by solokeys root ca</label>
</div>
<div>
<input type="checkbox" id="cert_hypersecu" name="hypersecu">
<label for="cert_hypersecu">Accept keys signed by hypersecu root ca</label>
</div>
<div>
<input type="checkbox" id="cert_google" name="google">
<label for="cert_google">Accept keys signed by google root ca</label>
</div>
<div>
<input type="checkbox" id="cert_microsoft" name="microsoft">
<label for="cert_microsoft">Accept keys signed by Microsofts collection of trusted TPM root ca</label>
</div>
<div style="font-size: 0.7em">(Nothing checked = accept all)</div>
<div>&nbsp;</div>
<div>
<button type="button" onclick="queryFidoMetaDataService()">&#128472; update root certificates from FIDO Alliance Metadata Service (MDS)</button>
</div>
<div>&nbsp;</div>
<div>If you select a root ca, direct attestation is required to validate your client with the root.<br>
The browser may warn you that he will provide informations about your device.<br>
When not checking against any root ca (deselect all certificates),
the client may change the assertion from the authenticator (for instance, using an anonymization CA),<br>
the browser may not warn about providing informations about your device.
</div>
<div style="margin-top:20px;font-size: 0.7em;font-style: italic">
Copyright &copy; 2023 Lukas Buchs - <a href="https://raw.githubusercontent.com/lbuchs/WebAuthn/master/LICENSE">license therms</a>
</div>
</div>
</div>
<div class="serverPreview">
<p style="margin-left:10px;font-weight: bold;">Here you can see what's saved on the server:</p>
<iframe src="server.php?fn=getStoredDataHtml" id="serverPreview"></iframe>
</div>
<div>
</body>
</html>
@@ -0,0 +1,48 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
68:1d:01:6c:7a:3c:e3:02:25:a5:01:94:28:47:57:71
Signature Algorithm: ecdsa-with-SHA384
Issuer:
stateOrProvinceName = California
organizationName = Apple Inc.
commonName = Apple WebAuthn Root CA
Validity
Not Before: Mar 18 18:21:32 2020 GMT
Not After : Mar 15 00:00:00 2045 GMT
Subject:
stateOrProvinceName = California
organizationName = Apple Inc.
commonName = Apple WebAuthn Root CA
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
ASN1 OID: secp384r1
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
26:D7:64:D9:C5:78:C2:5A:67:D1:A7:DE:6B:12:D0:1B:63:F1:C6:D7
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
-----BEGIN CERTIFICATE-----
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
1bWeT0vT
-----END CERTIFICATE-----
@@ -0,0 +1,37 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
04:00:00:00:00:01:0f:86:26:e6:0d
Signature Algorithm: sha1WithRSAEncryption
Issuer: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
Validity
Not Before: Dec 15 08:00:00 2006 GMT
Not After : Dec 15 08:00:00 2021 GMT
Subject: OU=GlobalSign Root CA - R2, O=GlobalSign, CN=GlobalSign
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
-----END CERTIFICATE-----
@@ -0,0 +1,130 @@
Google Hardware Attestation Root certificate
----------------------------------------------
https://developer.android.com/training/articles/security-key-attestation.html
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
e8:fa:19:63:14:d2:fa:18
Signature Algorithm: sha256WithRSAEncryption
Issuer: serialNumber = f92009e853b6b045
Validity
Not Before: May 26 16:28:52 2016 GMT
Not After : May 24 16:28:52 2026 GMT
Subject: serialNumber = f92009e853b6b045
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
X509v3 Authority Key Identifier:
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Digital Signature, Certificate Sign, CRL Sign
X509v3 CRL Distribution Points:
Full Name:
URI:https://android.googleapis.com/attestation/crl/
Signature Algorithm: sha256WithRSAEncryption
-----BEGIN CERTIFICATE-----
MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
-----END CERTIFICATE-----
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 15352756130135856819 (0xd50ff25ba3f2d6b3)
Signature Algorithm: sha256WithRSAEncryption
Issuer:
serialNumber = f92009e853b6b045
Validity
Not Before: Nov 22 20:37:58 2019 GMT
Not After : Nov 18 20:37:58 2034 GMT
Subject:
serialNumber = f92009e853b6b045
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
X509v3 Authority Key Identifier:
keyid:36:61:E1:00:7C:88:05:09:51:8B:44:6C:47:FF:1A:4C:C9:EA:4F:12
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Key Usage: critical
Certificate Sign
Signature Algorithm: sha256WithRSAEncryption
-----BEGIN CERTIFICATE-----
MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
ex0SdDrx+tWUDqG8At2JHA==
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
HyperFIDO U2F Security Key Attestation CA
https://hypersecu.com/support/downloads/attestation
Last Update: 2017-01-01
HyperFIDO U2F Security Key devices which contain attestation certificates signed by a set of CAs.
This file contains the CA certificates that Relying Parties (RP) need to configure their software
with to be able to verify U2F device certificates.
The file will be updated as needed when we publish more CA certificates.
Issuer: CN=FT FIDO 0100
-----BEGIN CERTIFICATE-----
MIIBjTCCATOgAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxGVCBGSURP
IDAxMDAwHhcNMTQwNzAxMTUzNjI2WhcNNDQwNzAzMTUzNjI2WjAXMRUwEwYDVQQD
EwxGVCBGSURPIDAxMDAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxdLxJx8ol
S3DS5cIHzunPF0gg69d+o8ZVCMJtpRtlfBzGuVL4YhaXk2SC2gptPTgmpZCV2vbN
fAPi5gOF0vbZo3AwbjAdBgNVHQ4EFgQUXt4jWlYDgwhaPU+EqLmeM9LoPRMwPwYD
VR0jBDgwNoAUXt4jWlYDgwhaPU+EqLmeM9LoPROhG6QZMBcxFTATBgNVBAMTDEZU
IEZJRE8gMDEwMIIBATAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQC2
D9o9cconKTo8+4GZPyZBJ3amc8F0/kzyidX9dhrAIAIgM9ocs5BW/JfmshVP9Mb+
Joa/kgX4dWbZxrk0ioTfJZg=
-----END CERTIFICATE-----
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4107 (0x100b)
Signature Algorithm: ecdsa-with-SHA256
Issuer:
commonName = HYPERFIDO 0200
organizationName = HYPERSECU
countryName = CA
Validity
Not Before: Jan 1 00:00:00 2018 GMT
Not After : Dec 31 23:59:59 2047 GMT
Subject:
commonName = HYPERFIDO 0200
organizationName = HYPERSECU
countryName = CA
-----BEGIN CERTIFICATE-----
MIIBxzCCAWygAwIBAgICEAswCgYIKoZIzj0EAwIwOjELMAkGA1UEBhMCQ0ExEjAQ
BgNVBAoMCUhZUEVSU0VDVTEXMBUGA1UEAwwOSFlQRVJGSURPIDAyMDAwIBcNMTgw
MTAxMDAwMDAwWhgPMjA0NzEyMzEyMzU5NTlaMDoxCzAJBgNVBAYTAkNBMRIwEAYD
VQQKDAlIWVBFUlNFQ1UxFzAVBgNVBAMMDkhZUEVSRklETyAwMjAwMFkwEwYHKoZI
zj0CAQYIKoZIzj0DAQcDQgAErKUI1G0S7a6IOLlmHipLlBuxTYjsEESQvzQh3dB7
dvxxWWm7kWL91rq6S7ayZG0gZPR+zYqdFzwAYDcG4+aX66NgMF4wHQYDVR0OBBYE
FLZYcfMMwkQAGbt3ryzZFPFypmsIMB8GA1UdIwQYMBaAFLZYcfMMwkQAGbt3ryzZ
FPFypmsIMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMC
A0kAMEYCIQCG2/ppMGt7pkcRie5YIohS3uDPIrmiRcTjqDclKVWg0gIhANcPNDZH
E2/zZ+uB5ThG9OZus+xSb4knkrbAyXKX2zm/
-----END CERTIFICATE-----
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
Solokeys FIDO2/U2F Device Attestation CA
========================================
Data:
Version: 1 (0x0)
Serial Number: 14143382635911888524 (0xc44763928ff4be8c)
Signature Algorithm: ecdsa-with-SHA256
Issuer:
emailAddress = hello@solokeys.com
commonName = solokeys.com
organizationalUnitName = Root CA
organizationName = Solo Keys
stateOrProvinceName = Maryland
countryName = US
Validity
Not Before: Nov 11 12:51:42 2018 GMT
Not After : Oct 29 12:51:42 2068 GMT
Subject:
emailAddress = hello@solokeys.com
commonName = solokeys.com
organizationalUnitName = Root CA
organizationName = Solo Keys
stateOrProvinceName = Maryland
countryName = US
-----BEGIN CERTIFICATE-----
MIIB9DCCAZoCCQDER2OSj/S+jDAKBggqhkjOPQQDAjCBgDELMAkGA1UEBhMCVVMx
ETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQKDAlTb2xvIEtleXMxEDAOBgNVBAsM
B1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlzLmNvbTEhMB8GCSqGSIb3DQEJARYS
aGVsbG9Ac29sb2tleXMuY29tMCAXDTE4MTExMTEyNTE0MloYDzIwNjgxMDI5MTI1
MTQyWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1hcnlsYW5kMRIwEAYDVQQK
DAlTb2xvIEtleXMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDHNvbG9rZXlz
LmNvbTEhMB8GCSqGSIb3DQEJARYSaGVsbG9Ac29sb2tleXMuY29tMFkwEwYHKoZI
zj0CAQYIKoZIzj0DAQcDQgAEWHAN0CCJVZdMs0oktZ5m93uxmB1iyq8ELRLtqVFL
SOiHQEab56qRTB/QzrpGAY++Y2mw+vRuQMNhBiU0KzwjBjAKBggqhkjOPQQDAgNI
ADBFAiEAz9SlrAXIlEu87vra54rICPs+4b0qhp3PdzcTg7rvnP0CIGjxzlteQQx+
jQGd7rwSZuE5RWUPVygYhUstQO9zNUOs
-----END CERTIFICATE-----
@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID6TCCAdGgAwIBAgIUUAjLh3ownidqbj5fboDmufVXiecwDQYJKoZIhvcNAQEL
BQAwMjERMA8GA1UECgwIU29sb0tleXMxCzAJBgNVBAYTAkNIMRAwDgYDVQQDDAdS
b290IFIxMCAXDTIxMDUyODA4MjQxMloYDzIwNzEwNTE2MDgyNDEyWjAtMREwDwYD
VQQKDAhTb2xvS2V5czELMAkGA1UEBhMCQ0gxCzAJBgNVBAMMAkYxMFkwEwYHKoZI
zj0CAQYIKoZIzj0DAQcDQgAEPCcSladE9kbZ0XI7jmtceNSHVrC1Rx0d3U8aMvKS
CJimYSe7c0Jy7CZpw7TU6N6chNx6Q1jaZ/B3ZjPLGZBOMqOBxDCBwTAdBgNVHQ4E
FgQUQWu2S++iGQ3kYl/9KQSWuYIptPgwHwYDVR0jBBgwFoAUVOPVaecSkRBulbOE
QMfZOr8x1dcwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwMgYI
KwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZodHRwOi8vaS5zMnBraS5uZXQvcjEv
MCcGA1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly9jLnMycGtpLm5ldC9yMS8wDQYJKoZI
hvcNAQELBQADggIBACVIymBmQwWwIUKnffptCJPeAn4m+Vy7ntr6KeS6aM7NI3cb
xzRq5tHOugtjFJsBSGynbF0/Kc+VnGR2lCFuVKeuusFsAhk4F13jaOTPSTWTXK6k
2TdoqZ6wIqmQ7bAZVYqcE21ZkM/Bo5Ej+PZacGjlGaEHwjL5CU2scnZeqS8d1ago
MCIfvRlYd2vkbPjqQx0t5jzEKZ7hF4y77kh0JArYpgpp0Sq4P96pPDwIZCvVGmGi
jhNOie0UnF6trfTD1AAXtlPqYPK9gNpXlN2IhsIpNMf7YA9R1zjVvnfYnFS6Tr73
0UzBct6jC9JompqvAo9NIe6cu/Qkc/KUL4JDt9iJWB8RN2aAnVCYQ+xT4evVFQCV
F/1pbeSvFfPqCfazkSPIiff7n9Tmk1Wwe3VmkuU7HUmAkYaLazs7DLY/Cp0/1V1K
pURMyawlUtv2J4PQqvOnMGUYupxp2l3DjdzHMx8RE+caCM2PxzPsUucLVHdOkBW2
h6U5PIWJuZtbiabwFmQXahqSNkRO6kXRvedowqaHNZiIFi4VKqPHrJrEhNhncfJ/
jwAAThoo5yyEgh3a+THoZKOFfZhzFJA9MVMwqQB00iSsF5HKC+MFUkHpKaV2DVjS
mPXOYy4biL8XkOqQeJUuB5sQ/2LYxaaXj1dBrnR3cklHp2KKacWYdnLdEe8I
-----END CERTIFICATE-----
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFVzCCAz+gAwIBAgIUR9xt5vBCCwFTaFGS3VFH0v49lmUwDQYJKoZIhvcNAQEL
BQAwMjERMA8GA1UECgwIU29sb0tleXMxCzAJBgNVBAYTAkNIMRAwDgYDVQQDDAdS
b290IFIxMCAXDTIxMDUyMTIyMTA1M1oYDzIwNzEwNTA5MjIxMDUzWjAyMREwDwYD
VQQKDAhTb2xvS2V5czELMAkGA1UEBhMCQ0gxEDAOBgNVBAMMB1Jvb3QgUjEwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCBybLOEMbhPyBEaHeqXJQUfRgd
xZHFtum2oXknCineXh8NXBL8PJLJEYi4Clv/QQGj+VPGPOcF1nwMX5BsmTYsLPMo
Rt7npI/xaPYdW/ByIvQQNqH3NTQjt8j4Wz0XWHL/VuqNFGZAdI4GN00NifKH1FCy
wOgwyKHl74XmMJLzzigFoo8m5ZPD+uw7ZRt1Q28jFTJiB2qyMX1rn2wHZLnHHhgs
wp0uApnQw5CGmhK3pkW8U9Gr3JH6K0q6NwZ4Pp1DPJFbl4J9cPFP2o25sOsf+jjX
4Ko2J97qndoRxHFWjh6HLvbq6/RyySbVGBsBeep92QXidf/3Vgc+6xkgTmlWL+5W
6mj7F8zhPAz8l8m3HEv/pVdsJikwrR2FAllw4T2nBYXhdJ4lPdH9HYZrbmyGVOcD
1M/hQ2d/7X0H1K40KV1li1nYJHxkfcDKHeI5RcAaLE+n6ctLS5KqyJ0MXmmga6I7
rd5cTfQqjYQm/YbfShK6ZsmsGC/KJGmFbx9kqsMbcg7a2qF1RT0WPFJd2F1glaon
tsqPwFDi5mR7M46tjLAWJx2Wu5mbACedRoxKPMe8l+b1CQ7k+temvxNK0IvM38St
z9UiVpl0VwACNpJ2MobM+XJNQNCbMbBt0n6wPmftqI5taoFxPTNM6t1F2utsjbdn
M1/WUKOh5lhheBMDWQIDAQABo2MwYTAdBgNVHQ4EFgQUVOPVaecSkRBulbOEQMfZ
Or8x1dcwHwYDVR0jBBgwFoAUVOPVaecSkRBulbOEQMfZOr8x1dcwDwYDVR0TAQH/
BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAEh5VMjC
YnWzJK8Rolmsbmpa7LQiS+WKk2ihYZRLUYlzET5+MDmue+n4sBuuuXPLaQsdnTSO
xqmVts8bneacEckr0y8j5/2o5/5UPjAchXXK6PwHqnajk2kzCIH2tWTyhpKcxF1i
fQo9RGA6igOjes8vMmKCFQg3A88wiAtrafgf/I3fVSHJOSeU29nqmVNHGU4tQFu0
Frx/d3y5elo5rZpld71DwRA83qLUgJ3liancoSb/icR8igGyLxCcxmKU5fFItiG9
4x+egnblyYfuhLbXU0k8LXJVMdDImZHEyI80EHOSpV3wZR5LyfiXu3fygxtOm6tL
fi1khkJIfIPMyV1Y4B7y6zOlHbneF1F8P4pTjHTYFHKhNm1wWffrqgLDYdtLAXSV
tyDM0zjtoRSHRYWsuwbeWdwoWHQeLkPzMtmFkHrPi0i3iv4GaianTnE1k1lX8Xf4
HbWBHXoVJCoAwycJJqX2SPZwFlWGp8IMGLUNzjFtLP+D7pdUSBgkDqz6FumZQGx/
KLHugnolEb8ZiOZKbaPOjc9EmXFjA42y4vXAip6ZZ2FfuWDqkPaU1WfM95cE0hf5
BSbfxSSQ2V3z4sywzkfQxr7q2mYzJ9C2NhLYXMrc98L9uEd0O7dO0eEF+44gc5O7
Bc7NuTA9//IG8nJb0MvD76TxHPlZuVnMdMb6
-----END CERTIFICATE-----
@@ -0,0 +1,42 @@
Yubico U2F Device Attestation CA
================================
Last Update: 2014-09-01
Yubico manufacturer U2F devices that contains device attestation
certificates signed by a set of Yubico CAs. This file contains the CA
certificates that Relying Parties (RP) need to configure their
software with to be able to verify U2F device certificates.
This file has been signed with OpenPGP and you should verify the
signature and the authenticity of the public key before trusting the
content. The signature is located next to the file:
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt
https://developers.yubico.com/u2f/yubico-u2f-ca-certs.txt.sig
We will update this file from time to time when we publish more CA
certificates.
Name: Yubico U2F Root CA Serial 457200631
Issued: 2014-08-01
-----BEGIN CERTIFICATE-----
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
-----END CERTIFICATE-----
+370
View File
@@ -0,0 +1,370 @@
<?php
/*
* Copyright (C) 2022 Lukas Buchs
* license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
*
* Server test script for WebAuthn library. Saves new registrations in session.
*
* JAVASCRIPT | SERVER
* ------------------------------------------------------------
*
* REGISTRATION
*
* window.fetch -----------------> getCreateArgs
* |
* navigator.credentials.create <-------------'
* |
* '-------------------------> processCreate
* |
* alert ok or fail <----------------'
*
* ------------------------------------------------------------
*
* VALIDATION
*
* window.fetch ------------------> getGetArgs
* |
* navigator.credentials.get <----------------'
* |
* '-------------------------> processGet
* |
* alert ok or fail <----------------'
*
* ------------------------------------------------------------
*/
require_once '../src/WebAuthn.php';
try {
session_start();
// read get argument and post body
$fn = filter_input(INPUT_GET, 'fn');
$requireResidentKey = !!filter_input(INPUT_GET, 'requireResidentKey');
$userVerification = filter_input(INPUT_GET, 'userVerification', FILTER_SANITIZE_SPECIAL_CHARS);
$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
$userId = $userId ? preg_replace('/[^0-9a-f]/i', '', $userId): "";
$userName = $userName ? preg_replace('/[^0-9a-z]/i', '', $userName): "";
$userDisplayName = $userDisplayName ? preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName): "";
$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post, null, 512, JSON_THROW_ON_ERROR);
}
if ($fn !== 'getStoredDataHtml') {
// Formats
$formats = [];
if (filter_input(INPUT_GET, 'fmt_android-key')) {
$formats[] = 'android-key';
}
if (filter_input(INPUT_GET, 'fmt_android-safetynet')) {
$formats[] = 'android-safetynet';
}
if (filter_input(INPUT_GET, 'fmt_apple')) {
$formats[] = 'apple';
}
if (filter_input(INPUT_GET, 'fmt_fido-u2f')) {
$formats[] = 'fido-u2f';
}
if (filter_input(INPUT_GET, 'fmt_none')) {
$formats[] = 'none';
}
if (filter_input(INPUT_GET, 'fmt_packed')) {
$formats[] = 'packed';
}
if (filter_input(INPUT_GET, 'fmt_tpm')) {
$formats[] = 'tpm';
}
$rpId = 'localhost';
if (filter_input(INPUT_GET, 'rpId')) {
$rpId = filter_input(INPUT_GET, 'rpId', FILTER_VALIDATE_DOMAIN);
if ($rpId === false) {
throw new Exception('invalid relying party ID');
}
}
// types selected on front end
$typeUsb = !!filter_input(INPUT_GET, 'type_usb');
$typeNfc = !!filter_input(INPUT_GET, 'type_nfc');
$typeBle = !!filter_input(INPUT_GET, 'type_ble');
$typeInt = !!filter_input(INPUT_GET, 'type_int');
$typeHyb = !!filter_input(INPUT_GET, 'type_hybrid');
// cross-platform: true, if type internal is not allowed
// false, if only internal is allowed
// null, if internal and cross-platform is allowed
$crossPlatformAttachment = null;
if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) {
$crossPlatformAttachment = true;
} else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) {
$crossPlatformAttachment = false;
}
// new Instance of the server library.
// make sure that $rpId is the domain name.
$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $rpId, $formats);
// add root certificates to validate new registrations
if (filter_input(INPUT_GET, 'solo')) {
$WebAuthn->addRootCertificates('rootCertificates/solo.pem');
$WebAuthn->addRootCertificates('rootCertificates/solokey_f1.pem');
$WebAuthn->addRootCertificates('rootCertificates/solokey_r1.pem');
}
if (filter_input(INPUT_GET, 'apple')) {
$WebAuthn->addRootCertificates('rootCertificates/apple.pem');
}
if (filter_input(INPUT_GET, 'yubico')) {
$WebAuthn->addRootCertificates('rootCertificates/yubico.pem');
}
if (filter_input(INPUT_GET, 'hypersecu')) {
$WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem');
}
if (filter_input(INPUT_GET, 'google')) {
$WebAuthn->addRootCertificates('rootCertificates/globalSign.pem');
$WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem');
}
if (filter_input(INPUT_GET, 'microsoft')) {
$WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem');
}
if (filter_input(INPUT_GET, 'mds')) {
$WebAuthn->addRootCertificates('rootCertificates/mds');
}
}
// ------------------------------------
// request for create arguments
// ------------------------------------
if ($fn === 'getCreateArgs') {
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
header('Content-Type: application/json');
print(json_encode($createArgs));
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
// ------------------------------------
// request for get arguments
// ------------------------------------
} else if ($fn === 'getGetArgs') {
$ids = [];
if ($requireResidentKey) {
if (!isset($_SESSION['registrations']) || !is_array($_SESSION['registrations']) || count($_SESSION['registrations']) === 0) {
throw new Exception('we do not have any registrations in session to check the registration');
}
} else {
// load registrations from session stored there by processCreate.
// normaly you have to load the credential Id's for a username
// from the database.
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
foreach ($_SESSION['registrations'] as $reg) {
if ($reg->userId === $userId) {
$ids[] = $reg->credentialId;
}
}
}
if (count($ids) === 0) {
throw new Exception('no registrations in session for userId ' . $userId);
}
}
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
header('Content-Type: application/json');
print(json_encode($getArgs));
// save challange to session. you have to deliver it to processGet later.
$_SESSION['challenge'] = $WebAuthn->getChallenge();
// ------------------------------------
// process create
// ------------------------------------
} else if ($fn === 'processCreate') {
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
$challenge = $_SESSION['challenge'];
// processCreate returns data to be stored for future logins.
// in this example we store it in the php session.
// Normaly you have to store the data in a database connected
// with the user name.
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false);
// add user infos
$data->userId = $userId;
$data->userName = $userName;
$data->userDisplayName = $userDisplayName;
if (!isset($_SESSION['registrations']) || !array_key_exists('registrations', $_SESSION) || !is_array($_SESSION['registrations'])) {
$_SESSION['registrations'] = [];
}
$_SESSION['registrations'][] = $data;
$msg = 'registration success.';
if ($data->rootValid === false) {
$msg = 'registration ok, but certificate does not match any of the selected root ca.';
}
$return = new stdClass();
$return->success = true;
$return->msg = $msg;
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// proccess get
// ------------------------------------
} else if ($fn === 'processGet') {
$clientDataJSON = base64_decode($post->clientDataJSON);
$authenticatorData = base64_decode($post->authenticatorData);
$signature = base64_decode($post->signature);
$userHandle = base64_decode($post->userHandle);
$id = base64_decode($post->id);
$challenge = $_SESSION['challenge'] ?? '';
$credentialPublicKey = null;
// looking up correspondending public key of the credential id
// you should also validate that only ids of the given user name
// are taken for the login.
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
foreach ($_SESSION['registrations'] as $reg) {
if ($reg->credentialId === $id) {
$credentialPublicKey = $reg->credentialPublicKey;
break;
}
}
}
if ($credentialPublicKey === null) {
throw new Exception('Public Key for credential ID not found!');
}
// if we have resident key, we have to verify that the userHandle is the provided userId at registration
if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) {
throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')');
}
// process the get request. throws WebAuthnException if it fails
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required');
$return = new stdClass();
$return->success = true;
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// proccess clear registrations
// ------------------------------------
} else if ($fn === 'clearRegistrations') {
$_SESSION['registrations'] = null;
$_SESSION['challenge'] = null;
$return = new stdClass();
$return->success = true;
$return->msg = 'all registrations deleted';
header('Content-Type: application/json');
print(json_encode($return));
// ------------------------------------
// display stored data as HTML
// ------------------------------------
} else if ($fn === 'getStoredDataHtml') {
$html = '<!DOCTYPE html>' . "\n";
$html .= '<html><head><style>tr:nth-child(even){background-color: #f2f2f2;}</style></head>';
$html .= '<body style="font-family:sans-serif">';
if (isset($_SESSION['registrations']) && is_array($_SESSION['registrations'])) {
$html .= '<p>There are ' . count($_SESSION['registrations']) . ' registrations in this session:</p>';
foreach ($_SESSION['registrations'] as $reg) {
$html .= '<table style="border:1px solid black;margin:10px 0;">';
foreach ($reg as $key => $value) {
if (is_bool($value)) {
$value = $value ? 'yes' : 'no';
} else if (is_null($value)) {
$value = 'null';
} else if (is_object($value)) {
$value = chunk_split(strval($value), 64);
} else if (is_string($value) && strlen($value) > 0 && htmlspecialchars($value, ENT_QUOTES) === '') {
$value = chunk_split(bin2hex($value), 64);
}
$html .= '<tr><td>' . htmlspecialchars($key) . '</td><td style="font-family:monospace;">' . nl2br(htmlspecialchars($value)) . '</td>';
}
$html .= '</table>';
}
} else {
$html .= '<p>There are no registrations in this session.</p>';
}
$html .= '</body></html>';
header('Content-Type: text/html');
print $html;
// ------------------------------------
// get root certs from FIDO Alliance Metadata Service
// ------------------------------------
} else if ($fn === 'queryFidoMetaDataService') {
$mdsFolder = 'rootCertificates/mds';
$success = false;
$msg = null;
// fetch only 1x / 24h
$lastFetch = \is_file($mdsFolder . '/lastMdsFetch.txt') ? \strtotime(\file_get_contents($mdsFolder . '/lastMdsFetch.txt')) : 0;
if ($lastFetch + (3600*48) < \time()) {
$cnt = $WebAuthn->queryFidoMetaDataService($mdsFolder);
$success = true;
\file_put_contents($mdsFolder . '/lastMdsFetch.txt', date('r'));
$msg = 'successfully queried FIDO Alliance Metadata Service - ' . $cnt . ' certificates downloaded.';
} else {
$msg = 'Fail: last fetch was at ' . date('r', $lastFetch) . ' - fetch only 1x every 48h';
}
$return = new stdClass();
$return->success = $success;
$return->msg = $msg;
header('Content-Type: application/json');
print(json_encode($return));
}
} catch (Throwable $ex) {
$return = new stdClass();
$return->success = false;
$return->msg = $ex->getMessage();
header('Content-Type: application/json');
print(json_encode($return));
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "lbuchs/webauthn",
"description": "A simple PHP WebAuthn (FIDO2) server library",
"keywords": [
"webauthn", "authentication"
],
"homepage": "https://github.com/lbuchs/webauthn",
"license": "MIT",
"authors": [
{
"name": "Lukas Buchs",
"role": "Developer"
}
],
"require": {
"php" : ">=8.0.0"
},
"autoload": {
"psr-4": {
"lbuchs\\WebAuthn\\": "src"
}
}
}
@@ -0,0 +1,18 @@
ACS FIDO Authenticator
----------------------
-----BEGIN CERTIFICATE-----
MIICQTCCAeegAwIBAgIUF/0wTPP6FEqxpsibJiLFtDj4qhwwCgYIKoZIzj0EAwIw
dTELMAkGA1UEBhMCSEsxEjAQBgNVBAgMCUhvbmcgS29uZzESMBAGA1UEBwwJSG9u
ZyBLb25nMSMwIQYDVQQKDBpBZHZhbmNlZCBDYXJkIFN5c3RlbXMgTHRkLjEZMBcG
A1UEAwwQQUNTIEZJRE8gUm9vdCBDQTAgFw0yMjA1MzAwOTIzMzVaGA8yMDUyMDUy
MjA5MjMzNVowdTELMAkGA1UEBhMCSEsxEjAQBgNVBAgMCUhvbmcgS29uZzESMBAG
A1UEBwwJSG9uZyBLb25nMSMwIQYDVQQKDBpBZHZhbmNlZCBDYXJkIFN5c3RlbXMg
THRkLjEZMBcGA1UEAwwQQUNTIEZJRE8gUm9vdCBDQTBZMBMGByqGSM49AgEGCCqG
SM49AwEHA0IABBwYgKVwjCV6+lv7gnpFERzU2uND8gdEkPCNcs/vFDs2sK42Juxn
hFnIgMB2DyU0IrXILjf/2XT0YSTd1sPiTSajUzBRMB0GA1UdDgQWBBTnQarpdSt4
sid7VjfNILIHrb2PoDAfBgNVHSMEGDAWgBTnQarpdSt4sid7VjfNILIHrb2PoDAP
BgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQDcoXJ3rzNMA/fZkh08
PoFrMx43GYMhZMfLPw/3MfJpGAIgectKwmJYM9J8SX8x/aQV4iGvKWoBfr1XPTAM
XOhVEYE=
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show More