This commit is contained in:
@@ -1,5 +1,41 @@
|
|||||||
{
|
{
|
||||||
"elements": [
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "ExpansionPanel",
|
||||||
|
"caption": "Lizenzierung",
|
||||||
|
"expanded": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "ValidationTextBox",
|
||||||
|
"name": "Lizenzcode",
|
||||||
|
"caption": "Verbrauchskostenlizenzcode",
|
||||||
|
"placeholder": "ENX-XXXX-XXXX-XXXX-XXXX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"name": "LizenzInstallationID",
|
||||||
|
"caption": "Installations-ID wird erzeugt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Label",
|
||||||
|
"name": "LizenzInformation",
|
||||||
|
"caption": "Verbrauchskostenlizenzcode fehlt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "LizenzPruefen",
|
||||||
|
"caption": "Lizenz prüfen und binden",
|
||||||
|
"onClick": "IPS_RequestAction($id, 'FormLizenzPruefen', $Lizenzcode);"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "LizenzVerwalten",
|
||||||
|
"caption": "Lizenz verwalten",
|
||||||
|
"link": true,
|
||||||
|
"onClick": "echo 'https://license.enelix.ch';"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "Label",
|
"type": "Label",
|
||||||
"caption": "🧾 Einstellungen Verbrauchskostenreport"
|
"caption": "🧾 Einstellungen Verbrauchskostenreport"
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ class VerbrauchskostenreportPDF extends TCPDF
|
|||||||
|
|
||||||
class Verbrauchskostenreport extends IPSModule
|
class Verbrauchskostenreport extends IPSModule
|
||||||
{
|
{
|
||||||
|
private const LIZENZ_ENDPOINT = 'https://license.enelix.ch/api/v1/licenses/activate';
|
||||||
|
private const LIZENZ_RETRY_SEKUNDEN = 900;
|
||||||
|
private const KATALOG_GRUNDLIZENZ = 'cost_report';
|
||||||
|
private const KATALOG_STROMZAEHLER = 'cost_meter_power';
|
||||||
|
private const KATALOG_NEBENZAEHLER = 'cost_meter_aux';
|
||||||
|
|
||||||
/** @var array<int, array<string, array<string, float|string>>> */
|
/** @var array<int, array<string, array<string, float|string>>> */
|
||||||
private array $powerCostCache = [];
|
private array $powerCostCache = [];
|
||||||
|
|
||||||
@@ -47,6 +53,7 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->RegisterPropertyString('PowerMeters', '[]');
|
$this->RegisterPropertyString('PowerMeters', '[]');
|
||||||
$this->RegisterPropertyString('WaterMeters', '[]');
|
$this->RegisterPropertyString('WaterMeters', '[]');
|
||||||
$this->RegisterPropertyString('Tariffs', '[]');
|
$this->RegisterPropertyString('Tariffs', '[]');
|
||||||
|
$this->RegisterPropertyString('Lizenzcode', '');
|
||||||
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
$this->RegisterPropertyInteger('LogoMediaID', 0);
|
||||||
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
$this->RegisterPropertyString('PropertyText', 'Liegenschaft');
|
||||||
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
$this->RegisterPropertyString('FooterText', 'Belevo AG • 6122 Menznau • www.belevo.ch');
|
||||||
@@ -64,6 +71,15 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->RegisterPropertyString('CreditorCountry', 'CH');
|
$this->RegisterPropertyString('CreditorCountry', 'CH');
|
||||||
$this->RegisterPropertyString('BankIBAN', '');
|
$this->RegisterPropertyString('BankIBAN', '');
|
||||||
|
|
||||||
|
$this->RegisterAttributeString('LizenzInstallationID', '');
|
||||||
|
$this->RegisterAttributeString('LizenzLease', '{}');
|
||||||
|
$this->RegisterAttributeString('LizenzcodeHash', '');
|
||||||
|
$this->RegisterAttributeString('LizenzLetzterFehler', '');
|
||||||
|
$this->RegisterAttributeInteger('LizenzNaechsterVersuch', 0);
|
||||||
|
if ($this->ReadAttributeString('LizenzInstallationID') === '') {
|
||||||
|
$this->WriteAttributeString('LizenzInstallationID', $this->ErzeugeUuidV4());
|
||||||
|
}
|
||||||
|
|
||||||
$this->RegisterTariffVariableProfiles();
|
$this->RegisterTariffVariableProfiles();
|
||||||
|
|
||||||
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
|
$this->RegisterVariableInteger('FromDate', 'Startdatum', '~UnixTimestamp', 1);
|
||||||
@@ -106,6 +122,8 @@ class Verbrauchskostenreport extends IPSModule
|
|||||||
$this->InitializeVisuTariffDefaults();
|
$this->InitializeVisuTariffDefaults();
|
||||||
$this->UpdateTariffOverview();
|
$this->UpdateTariffOverview();
|
||||||
$this->UpdateUserOverview();
|
$this->UpdateUserOverview();
|
||||||
|
$this->AktualisiereLizenz($this->ReadPropertyString('Lizenzcode'), false);
|
||||||
|
$this->AktualisiereLizenzForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function Destroy()
|
public function Destroy()
|
||||||
@@ -630,6 +648,278 @@ form.onsubmit=function(){
|
|||||||
return $this->SaveTariffTableFromPayload(json_encode($rows));
|
return $this->SaveTariffTableFromPayload(json_encode($rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function AktualisiereLizenz(string $lizenzcode, bool $erzwingen): bool
|
||||||
|
{
|
||||||
|
$code = $this->NormalisiereLizenzcode($lizenzcode);
|
||||||
|
if (!$this->IstLizenzcodeGueltig($code)) {
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLetzterFehler',
|
||||||
|
$code === '' ? 'Verbrauchskostenlizenzcode fehlt.' : 'Verbrauchskostenlizenzcode ist ungültig.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$installationID = $this->ReadAttributeString('LizenzInstallationID');
|
||||||
|
if ($installationID === '') {
|
||||||
|
$installationID = $this->ErzeugeUuidV4();
|
||||||
|
$this->WriteAttributeString('LizenzInstallationID', $installationID);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeHash = hash('sha256', $code);
|
||||||
|
$gespeicherterHash = $this->ReadAttributeString('LizenzcodeHash');
|
||||||
|
$lease = $this->LeseLizenzLease();
|
||||||
|
$lokalGueltig = false;
|
||||||
|
if ($gespeicherterHash !== '' && hash_equals($gespeicherterHash, $codeHash)) {
|
||||||
|
try {
|
||||||
|
$this->PruefeLizenzLease($lease, $installationID);
|
||||||
|
$lokalGueltig = true;
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$lokalGueltig = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$erneuernAb = strtotime((string)($lease['refreshAfter'] ?? '')) ?: 0;
|
||||||
|
if (!$erzwingen && $lokalGueltig && time() < $erneuernAb) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!$erzwingen && time() < $this->ReadAttributeInteger('LizenzNaechsterVersuch')) {
|
||||||
|
return $lokalGueltig;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'LizenzNaechsterVersuch',
|
||||||
|
time() + self::LIZENZ_RETRY_SEKUNDEN
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
$neueLease = $this->RufeLizenzserverAuf($code, $installationID);
|
||||||
|
$this->PruefeLizenzLease($neueLease, $installationID);
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLease',
|
||||||
|
json_encode($neueLease, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
|
||||||
|
);
|
||||||
|
$this->WriteAttributeString('LizenzcodeHash', $codeHash);
|
||||||
|
$this->WriteAttributeString('LizenzLetzterFehler', '');
|
||||||
|
$this->WriteAttributeInteger(
|
||||||
|
'LizenzNaechsterVersuch',
|
||||||
|
strtotime((string)$neueLease['refreshAfter']) ?: time() + 86400
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
$this->WriteAttributeString(
|
||||||
|
'LizenzLetzterFehler',
|
||||||
|
substr($fehler->getMessage(), 0, 500)
|
||||||
|
);
|
||||||
|
return $lokalGueltig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function AktualisiereLizenzForm(?string $lizenzcode = null): void
|
||||||
|
{
|
||||||
|
$this->UpdateFormField(
|
||||||
|
'LizenzInstallationID',
|
||||||
|
'caption',
|
||||||
|
'Installations-ID: ' . $this->ReadAttributeString('LizenzInstallationID')
|
||||||
|
);
|
||||||
|
$this->UpdateFormField(
|
||||||
|
'LizenzInformation',
|
||||||
|
'caption',
|
||||||
|
$this->LizenzStatustext($lizenzcode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LizenzStatustext(?string $lizenzcode = null): string
|
||||||
|
{
|
||||||
|
$code = $this->NormalisiereLizenzcode(
|
||||||
|
$lizenzcode ?? $this->ReadPropertyString('Lizenzcode')
|
||||||
|
);
|
||||||
|
if ($code === '') {
|
||||||
|
return 'Verbrauchskostenlizenzcode fehlt.';
|
||||||
|
}
|
||||||
|
if (!$this->IstLizenzcodeGueltig($code)) {
|
||||||
|
return 'Verbrauchskostenlizenzcode ist ungültig.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = $this->ReadAttributeString('LizenzcodeHash');
|
||||||
|
$fehler = $this->ReadAttributeString('LizenzLetzterFehler');
|
||||||
|
if ($hash === '' || !hash_equals($hash, hash('sha256', $code))) {
|
||||||
|
return $fehler !== '' ? 'Nicht freigegeben: ' . $fehler : 'Lizenz noch nicht bestätigt.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lease = $this->LeseLizenzLease();
|
||||||
|
$this->PruefeLizenzLease(
|
||||||
|
$lease,
|
||||||
|
$this->ReadAttributeString('LizenzInstallationID')
|
||||||
|
);
|
||||||
|
$status = sprintf(
|
||||||
|
'Aktiv: %d Stromzähler und %d Wärme-/Wasserzähler lizenziert.',
|
||||||
|
$this->LizenzMenge($lease, self::KATALOG_STROMZAEHLER),
|
||||||
|
$this->LizenzMenge($lease, self::KATALOG_NEBENZAEHLER)
|
||||||
|
);
|
||||||
|
if ($fehler !== '') {
|
||||||
|
$status .= ' Online-Prüfung fehlgeschlagen; lokale Freigabe ist noch gültig. Fehler: '
|
||||||
|
. $fehler;
|
||||||
|
}
|
||||||
|
return $status;
|
||||||
|
} catch (Throwable $ungueltig) {
|
||||||
|
return 'Nicht freigegeben: ' . $ungueltig->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function PruefeLizenzLease(array $lease, string $installationID): void
|
||||||
|
{
|
||||||
|
if (($lease['status'] ?? null) !== 'active') {
|
||||||
|
throw new RuntimeException('Die Verbrauchskostenlizenz ist nicht aktiv.');
|
||||||
|
}
|
||||||
|
if (($lease['installationId'] ?? null) !== $installationID) {
|
||||||
|
throw new RuntimeException('Die Verbrauchskostenlizenz gehört zu einer anderen Installation.');
|
||||||
|
}
|
||||||
|
$offlineBis = strtotime((string)($lease['offlineUntil'] ?? ''));
|
||||||
|
if ($offlineBis === false || time() >= $offlineBis) {
|
||||||
|
throw new RuntimeException('Die Offline-Freigabe ist abgelaufen.');
|
||||||
|
}
|
||||||
|
if ($this->LizenzMenge($lease, self::KATALOG_GRUNDLIZENZ) < 1) {
|
||||||
|
throw new RuntimeException('Die Grundlizenz für den Verbrauchskostenreport fehlt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strom = count($this->LeseJsonListe('PowerMeters'));
|
||||||
|
$neben = count($this->LeseJsonListe('WaterMeters'));
|
||||||
|
$stromLizenziert = $this->LizenzMenge($lease, self::KATALOG_STROMZAEHLER);
|
||||||
|
$nebenLizenziert = $this->LizenzMenge($lease, self::KATALOG_NEBENZAEHLER);
|
||||||
|
if ($strom > $stromLizenziert) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Stromzähler: %d konfiguriert, aber nur %d lizenziert.',
|
||||||
|
$strom,
|
||||||
|
$stromLizenziert
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if ($neben > $nebenLizenziert) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'Wärme-/Wasserzähler: %d konfiguriert, aber nur %d lizenziert.',
|
||||||
|
$neben,
|
||||||
|
$nebenLizenziert
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LizenzMenge(array $lease, string $kennung): int
|
||||||
|
{
|
||||||
|
$menge = 0;
|
||||||
|
foreach (($lease['entitlements'] ?? []) as $berechtigung) {
|
||||||
|
if (!is_array($berechtigung) || ($berechtigung['status'] ?? null) !== 'active') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$schluessel = $berechtigung['catalog_key'] ?? $berechtigung['sku'] ?? null;
|
||||||
|
$anzahl = $berechtigung['quantity'] ?? 0;
|
||||||
|
if ($schluessel === $kennung && is_int($anzahl) && $anzahl > 0) {
|
||||||
|
$menge += $anzahl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $menge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LeseJsonListe(string $property): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$werte = json_decode(
|
||||||
|
$this->ReadPropertyString($property),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
return is_array($werte) ? array_values($werte) : [];
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function LeseLizenzLease(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$lease = json_decode(
|
||||||
|
$this->ReadAttributeString('LizenzLease'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
return is_array($lease) ? $lease : [];
|
||||||
|
} catch (Throwable $fehler) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function RufeLizenzserverAuf(string $lizenzcode, string $installationID): array
|
||||||
|
{
|
||||||
|
$curl = curl_init(self::LIZENZ_ENDPOINT);
|
||||||
|
if ($curl === false) {
|
||||||
|
throw new RuntimeException('Die HTTPS-Anfrage konnte nicht vorbereitet werden.');
|
||||||
|
}
|
||||||
|
$nutzlast = json_encode([
|
||||||
|
'code' => $lizenzcode,
|
||||||
|
'installationId' => $installationID,
|
||||||
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
try {
|
||||||
|
curl_setopt_array($curl, [
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $nutzlast,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json'],
|
||||||
|
CURLOPT_USERAGENT => 'Enelix-Verbrauchskostenreport/0.1',
|
||||||
|
]);
|
||||||
|
$antwort = curl_exec($curl);
|
||||||
|
if ($antwort === false) {
|
||||||
|
throw new RuntimeException('Lizenzserver nicht erreichbar: ' . curl_error($curl));
|
||||||
|
}
|
||||||
|
if (strlen($antwort) > 65536) {
|
||||||
|
throw new RuntimeException('Die Antwort des Lizenzservers ist zu gross.');
|
||||||
|
}
|
||||||
|
$httpStatus = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||||
|
} finally {
|
||||||
|
curl_close($curl);
|
||||||
|
}
|
||||||
|
|
||||||
|
$daten = json_decode($antwort, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
if (!is_array($daten)) {
|
||||||
|
throw new RuntimeException('Der Lizenzserver lieferte kein JSON-Objekt.');
|
||||||
|
}
|
||||||
|
if ($httpStatus !== 200) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'Lizenzserver HTTP ' . $httpStatus . ': '
|
||||||
|
. (is_string($daten['error'] ?? null) ? $daten['error'] : 'Unbekannter Fehler')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function NormalisiereLizenzcode(string $code): string
|
||||||
|
{
|
||||||
|
return strtoupper((string)preg_replace('/[^A-Z0-9]/i', '', $code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function IstLizenzcodeGueltig(string $code): bool
|
||||||
|
{
|
||||||
|
return preg_match('/^ENX[A-HJ-NP-Z2-9]{16}$/', $code) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ErzeugeUuidV4(): string
|
||||||
|
{
|
||||||
|
$bytes = random_bytes(16);
|
||||||
|
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
||||||
|
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
||||||
|
$hex = bin2hex($bytes);
|
||||||
|
return sprintf(
|
||||||
|
'%s-%s-%s-%s-%s',
|
||||||
|
substr($hex, 0, 8),
|
||||||
|
substr($hex, 8, 4),
|
||||||
|
substr($hex, 12, 4),
|
||||||
|
substr($hex, 16, 4),
|
||||||
|
substr($hex, 20, 12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function ReadUsers()
|
private function ReadUsers()
|
||||||
{
|
{
|
||||||
$users = json_decode($this->ReadPropertyString('Users'), true);
|
$users = json_decode($this->ReadPropertyString('Users'), true);
|
||||||
@@ -1348,6 +1638,14 @@ refreshPickers();
|
|||||||
public function RequestAction($Ident, $Value)
|
public function RequestAction($Ident, $Value)
|
||||||
{
|
{
|
||||||
switch ($Ident) {
|
switch ($Ident) {
|
||||||
|
case 'FormLizenzPruefen':
|
||||||
|
if (!is_string($Value)) {
|
||||||
|
throw new InvalidArgumentException('Der Lizenzcode muss als Text übergeben werden.');
|
||||||
|
}
|
||||||
|
$this->AktualisiereLizenz($Value, true);
|
||||||
|
$this->AktualisiereLizenzForm($Value);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'FromDate':
|
case 'FromDate':
|
||||||
case 'ToDate':
|
case 'ToDate':
|
||||||
case 'VisuTariffType':
|
case 'VisuTariffType':
|
||||||
@@ -1458,6 +1756,10 @@ refreshPickers();
|
|||||||
|
|
||||||
public function GenerateReport()
|
public function GenerateReport()
|
||||||
{
|
{
|
||||||
|
if (!$this->AktualisiereLizenz($this->ReadPropertyString('Lizenzcode'), false)) {
|
||||||
|
throw new RuntimeException($this->LizenzStatustext());
|
||||||
|
}
|
||||||
|
|
||||||
$from = GetValue($this->GetIDForIdent('FromDate'));
|
$from = GetValue($this->GetIDForIdent('FromDate'));
|
||||||
$to = GetValue($this->GetIDForIdent('ToDate'));
|
$to = GetValue($this->GetIDForIdent('ToDate'));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user