diff --git a/Home Connect Cloud/form.json b/Home Connect Cloud/form.json index 75dc9f7..38afd53 100644 --- a/Home Connect Cloud/form.json +++ b/Home Connect Cloud/form.json @@ -1,5 +1,11 @@ { "elements": [ + { + "type": "Label", + "name": "RateLimitNotice", + "caption": "", + "visible": false + }, { "type": "Button", "caption": "Register", @@ -38,4 +44,4 @@ } ], "status": [] -} \ No newline at end of file +} diff --git a/Home Connect Cloud/locale.json b/Home Connect Cloud/locale.json index 567cae7..4241faf 100644 --- a/Home Connect Cloud/locale.json +++ b/Home Connect Cloud/locale.json @@ -16,6 +16,7 @@ "1000 calls in 1 day": "1000 Anfragen pro Tag", "50 calls in 1 minute": "50 Anfragen pro Minute", "A rate limit was reached. Requests are blocked until %s.": "Ein Anfragenlimit wurde erreicht. Weitere Anfragen werden bis %s blockiert", + "Home Connect requests are currently rate limited.": "Home-Connect-Anfragen sind aktuell limitiert.", "https://www.symcon.de/en/service/documentation/module-reference/home-connect/": "https://www.symcon.de/de/service/dokumentation/modulreferenz/home-connect" } } diff --git a/Home Connect Cloud/module.php b/Home Connect Cloud/module.php index 80332ec..22d10c6 100644 --- a/Home Connect Cloud/module.php +++ b/Home Connect Cloud/module.php @@ -32,6 +32,7 @@ public function Create() $this->RegisterAttributeString('Token', ''); $this->RegisterAttributeString('RateError', ''); + $this->RegisterAttributeInteger('RateLimitUntil', 0); $this->RequireParent('{2FADB4B7-FDAB-3C64-3E2C-068A4809849A}'); @@ -71,6 +72,16 @@ public function ForwardData($Data) { $data = json_decode($Data, true); $this->SendDebug('Forward', $Data, 0); + if ($this->isRateLimitActive()) { + $error = [ + 'error' => [ + 'key' => '429', + 'description' => $this->ReadAttributeString('RateError') ?: $this->Translate('Home Connect requests are currently rate limited.') + ] + ]; + $this->SendDebug('ForwardRateLimit', json_encode($error), 0); + return json_encode($error); + } try { if (isset($data['Payload'])) { $this->SendDebug('Payload', $data['Payload'], 0); @@ -189,9 +200,9 @@ public function GetConfigurationForParent() public function ResetRateLimit() { - if ($this->GetStatus() != IS_ACTIVE) { - $this->WriteAttributeString('RateError', ''); - } + $this->WriteAttributeString('RateError', ''); + $this->WriteAttributeInteger('RateLimitUntil', 0); + $this->updateRateLimitNotice(); $this->SetStatus(IS_ACTIVE); $this->SetTimerInterval('RateLimit', 0); } @@ -199,11 +210,15 @@ public function ResetRateLimit() public function GetConfigurationForm() { $form = json_decode(file_get_contents(__DIR__ . '/form.json'), true); - $form['status'][] = [ - 'code' => IS_EBASE, - 'icon' => 'error', - 'caption' => $this->ReadAttributeString('RateError'), - ]; + $rateError = $this->ReadAttributeString('RateError'); + foreach ($form['elements'] as &$element) { + if (($element['name'] ?? '') !== 'RateLimitNotice') { + continue; + } + $element['caption'] = $rateError; + $element['visible'] = $rateError !== ''; + break; + } return json_encode($form); } @@ -470,16 +485,40 @@ private function FetchData($url) return $result; } - private function getTimer($name) + private function isRateLimitActive(): bool + { + return $this->ReadAttributeInteger('RateLimitUntil') > time(); + } + + private function updateRateLimitNotice(): void { - foreach (IPS_GetTimerList() as $timerID) { - $timer = IPS_GetTimer($timerID); - if (($timer['InstanceID'] == $this->InstanceID) && ($timer['Name'] == $name)) { - return $timer; - break; + $rateError = $this->ReadAttributeString('RateError'); + $this->UpdateFormField('RateLimitNotice', 'caption', $rateError); + $this->UpdateFormField('RateLimitNotice', 'visible', $rateError !== ''); + } + + private function parseResponseHeaders(array $responseHeader): array + { + $head = []; + foreach ($responseHeader as $header) { + $values = explode(':', $header, 2); + if (isset($values[1])) { + $head[strtolower(trim($values[0]))] = trim($values[1]); } } - return false; + + return $head; + } + + private function getRateLimitDelay(array $responseHeader): int + { + $head = $this->parseResponseHeaders($responseHeader); + + if (isset($head['retry-after']) && is_numeric($head['retry-after'])) { + return max(1, (int) $head['retry-after']); + } + + return 60; } private function handleHttpErrors($code, $responseHeader) @@ -487,33 +526,27 @@ private function handleHttpErrors($code, $responseHeader) switch ($code) { //Too Many Requests case 429: - $head = []; - foreach ($responseHeader as $header) { - $values = explode(':', $header, 2); - if (isset($values[1])) { - $head[trim($values[0])] = trim($values[1]); - } - } - $this->SetTimerInterval('RateLimit', $head['Retry-After'] * 1000); - $timer = $this->getTimer('RateLimit'); - //Fallback to current time - $nextRun = $timer === false ? time() : $timer['NextRun']; + $head = $this->parseResponseHeaders($responseHeader); + $retryAfter = $this->getRateLimitDelay($responseHeader); + $nextRun = time() + $retryAfter; + $this->WriteAttributeInteger('RateLimitUntil', $nextRun); + $this->SetTimerInterval('RateLimit', $retryAfter * 1000); $this->WriteAttributeString( 'RateError', - isset($head['Rate-Limit-Type']) ? + isset($head['rate-limit-type']) ? sprintf( $this->Translate( 'The rate limit of %s was reached. Requests are blocked until %s.' ), - $head['Rate-Limit-Type'] == 'day' ? + $head['rate-limit-type'] == 'day' ? $this->Translate('1000 calls in 1 day') : $this->Translate('50 calls in 1 minute'), date('d.m.Y H:i:s', $nextRun), ) : sprintf($this->Translate('A rate limit was reached. Requests are blocked until %s.'), date('d.m.Y H:i:s', $nextRun)) ); - if ($this->GetStatus() != IS_EBASE) { - $this->SetStatus(IS_EBASE); - IPS_ApplyChanges($this->InstanceID); + $this->updateRateLimitNotice(); + if ($this->HasActiveParent() && $this->GetStatus() != IS_ACTIVE) { + $this->SetStatus(IS_ACTIVE); } return; diff --git a/Home Connect Device/module.php b/Home Connect Device/module.php index fb01e74..b7f7b16 100644 --- a/Home Connect Device/module.php +++ b/Home Connect Device/module.php @@ -68,6 +68,7 @@ public function Create() $this->RegisterAttributeString('Settings', '[]'); $this->RegisterAttributeString('OptionKeys', '[]'); + $this->RegisterAttributeString('InitializationSignature', ''); //Common States //States @@ -129,7 +130,7 @@ public function ApplyChanges() parent::ApplyChanges(); if (IPS_GetKernelRunlevel() === KR_READY) { - $this->refreshDeviceState(true); + $this->refreshDeviceState($this->needsInitialization()); } $this->SetReceiveDataFilter('.*' . $this->ReadPropertyString('HaID') . '.*'); @@ -142,7 +143,7 @@ public function MessageSink($Timestamp, $SenderID, $MessageID, $Data) $parentID = IPS_GetInstance($this->InstanceID)['ConnectionID']; if ($SenderID == $parentID && $MessageID == IM_CHANGESTATUS) { - $this->refreshDeviceState($Data[0] == IS_ACTIVE); + $this->refreshDeviceState($Data[0] == IS_ACTIVE && $this->needsInitialization()); return; } @@ -150,7 +151,7 @@ public function MessageSink($Timestamp, $SenderID, $MessageID, $Data) switch ($MessageID) { case FM_CONNECT: $this->RegisterMessage($Data[0], IM_CHANGESTATUS); - $this->refreshDeviceState(true); + $this->refreshDeviceState($this->needsInitialization()); return; case FM_DISCONNECT: @@ -384,6 +385,7 @@ public function InitializeDevice() $this->createEventProfile(); $this->MaintainVariable('Event', $this->Translate('Event'), VARIABLETYPE_STRING, 'HomeConnect.Event.' . $this->ReadPropertyString('DeviceType'), 0, true); $this->MaintainVariable('EventDescription', $this->Translate('Event Description'), VARIABLETYPE_STRING, '', 0, true); + $this->WriteAttributeString('InitializationSignature', $this->getInitializationSignature()); } } @@ -452,6 +454,27 @@ private function refreshDeviceState(bool $initializeDevice): void $this->SetStatus(IS_INACTIVE); } + private function needsInitialization(): bool + { + if ($this->ReadPropertyString('HaID') == '') { + return false; + } + + if (!@IPS_GetObjectIDByIdent('OperationState', $this->InstanceID)) { + return true; + } + + return $this->ReadAttributeString('InitializationSignature') !== $this->getInitializationSignature(); + } + + private function getInitializationSignature(): string + { + return json_encode([ + 'HaID' => $this->ReadPropertyString('HaID'), + 'DeviceType' => $this->ReadPropertyString('DeviceType') + ]); + } + private function createPrograms() { $rawPrograms = json_decode($this->RequestDataFromParent('homeappliances/' . $this->ReadPropertyString('HaID') . '/programs'), true); diff --git a/library.json b/library.json index bbaa585..7bf62cd 100644 --- a/library.json +++ b/library.json @@ -7,6 +7,6 @@ "version": "6.0" }, "version": "1.1", - "build": 8, - "date": 1778241600 + "build": 9, + "date": 1779364800 }