Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64445e95fe | ||
|
|
6b8556567b | ||
|
|
29f752eafe | ||
|
|
4de95c1a29 | ||
|
|
ae455dba20 | ||
|
|
0e78302640 |
@@ -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
|
||||
@@ -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;
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
?>
|
||||
@@ -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('+','-','=','<','>','!='))) {
|
||||
$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> 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">></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> 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> 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">></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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
*/
|
||||
?>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 . "&=" . $_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 . "&=" . $_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 . "&=" . $_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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>";
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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>";
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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]}]}';
|
||||
?>
|
||||
@@ -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]}]}';
|
||||
@@ -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]}]}';
|
||||
@@ -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]}]}';
|
||||
?>
|
||||
@@ -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]}]}';
|
||||
@@ -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]}]}';
|
||||
@@ -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]}]}';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = '["=","≠",">","<"]';
|
||||
$type = "number";
|
||||
break;
|
||||
case "float":
|
||||
$operators = '["=","≠",">","<"]';
|
||||
$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 "]}";
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 337 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 320 KiB |
@@ -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 |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -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 |
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 += 'µsoft=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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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()});
|
||||
@@ -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)}
|
||||
@@ -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}">></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}">></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}">></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;
|
||||
});
|
||||
}
|
||||
@@ -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> " + 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;
|
||||
}
|
||||
@@ -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 } });
|
||||
}
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
deny from all
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,144 @@
|
||||
[](https://github.com/lbuchs/WebAuthn/blob/master/LICENSE)
|
||||
[](https://php.net)
|
||||
[](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 ✅
|
||||
* android-safetynet ✅
|
||||
* apple ✅
|
||||
* fido-u2f ✅
|
||||
* none ✅
|
||||
* packed ✅
|
||||
* tpm ✅
|
||||
|
||||
> [!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)
|
||||
@@ -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 += 'µsoft=' + (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> </div>
|
||||
<div> </div>
|
||||
<div>Simple working demo for the <a href="https://github.com/lbuchs/WebAuthn">lbuchs/WebAuthn</a> library.</div>
|
||||
<div>
|
||||
<div> </div>
|
||||
<table>
|
||||
<tbody><tr>
|
||||
<td>
|
||||
<button type="button" onclick="createRegistration()">➕ new registration</button>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" onclick="checkRegistration()">❔ check registration</button>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" onclick="clearRegistration()">␡ clear all registrations</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div> </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> </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> </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> </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> </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> </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> </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> </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> </div>
|
||||
|
||||
<div>
|
||||
<button type="button" onclick="queryFidoMetaDataService()">🗘 update root certificates from FIDO Alliance Metadata Service (MDS)</button>
|
||||
</div>
|
||||
|
||||
<div> </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 © 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 @@
|
||||
|
||||
@@ -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-----
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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-----
|
||||