Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Home Connect Cloud/form.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"elements": [
{
"type": "Label",
"name": "RateLimitNotice",
"caption": "",
"visible": false
},
{
"type": "Button",
"caption": "Register",
Expand Down Expand Up @@ -38,4 +44,4 @@
}
],
"status": []
}
}
1 change: 1 addition & 0 deletions Home Connect Cloud/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
95 changes: 64 additions & 31 deletions Home Connect Cloud/module.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function Create()
$this->RegisterAttributeString('Token', '');

$this->RegisterAttributeString('RateError', '');
$this->RegisterAttributeInteger('RateLimitUntil', 0);

$this->RequireParent('{2FADB4B7-FDAB-3C64-3E2C-068A4809849A}');

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -189,21 +200,25 @@ 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);
}

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);
}
Expand Down Expand Up @@ -470,50 +485,68 @@ 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)
{
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;

Expand Down
29 changes: 26 additions & 3 deletions Home Connect Device/module.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function Create()

$this->RegisterAttributeString('Settings', '[]');
$this->RegisterAttributeString('OptionKeys', '[]');
$this->RegisterAttributeString('InitializationSignature', '');

//Common States
//States
Expand Down Expand Up @@ -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') . '.*');
Expand All @@ -142,15 +143,15 @@ 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;
}

if ($SenderID == $this->InstanceID) {
switch ($MessageID) {
case FM_CONNECT:
$this->RegisterMessage($Data[0], IM_CHANGESTATUS);
$this->refreshDeviceState(true);
$this->refreshDeviceState($this->needsInitialization());
return;

case FM_DISCONNECT:
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions library.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"version": "6.0"
},
"version": "1.1",
"build": 8,
"date": 1778241600
"build": 9,
"date": 1779364800
}