*/ function kepware_config(): array { static $config; if ($config === null) { $settings = include __DIR__ . '/../config/control-settings.php'; $config = $settings['kepware'] ?? []; } return $config; } } if (!function_exists('kepware_rest_request')) { /** * Issue an HTTP request to the Kepware REST server. * * @param string $method HTTP verb (GET, POST, ...). * @param string $path Relative REST path. * @param array|null $body Optional JSON payload. * * @return array */ function kepware_rest_request(string $method, string $path, ?array $body = null): array { $config = kepware_config(); $baseUrl = rtrim((string) ($config['base_url'] ?? ''), '/'); if ($baseUrl === '') { throw new RuntimeException('Kepware base URL is not configured.'); } $url = $baseUrl . '/' . ltrim($path, '/'); $username = (string) ($config['username'] ?? ''); $password = (string) ($config['password'] ?? ''); $timeout = (float) ($config['timeout'] ?? 5.0); $verify = (bool) ($config['verify_tls'] ?? false); $attempts = (int) ($config['retry_attempts'] ?? 1); if ($attempts < 1) { $attempts = 1; } $retryDelay = (float) ($config['retry_delay'] ?? 0.0); if ($retryDelay < 0) { $retryDelay = 0.0; } $postFields = null; if ($body !== null) { $postFields = json_encode($body, JSON_THROW_ON_ERROR); } $options = [ CURLOPT_URL => $url, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => $username . ':' . $password, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', ], CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => $timeout, CURLOPT_SSL_VERIFYPEER => $verify, CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0, ]; if ($postFields !== null) { $options[CURLOPT_POSTFIELDS] = $postFields; } $retryableCurlErrors = [ CURLE_COULDNT_CONNECT, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_RESOLVE_PROXY, CURLE_GOT_NOTHING, CURLE_OPERATION_TIMEDOUT, CURLE_PARTIAL_FILE, CURLE_RECV_ERROR, CURLE_SEND_ERROR, CURLE_SSL_CONNECT_ERROR, ]; for ($attempt = 1; $attempt <= $attempts; $attempt++) { $handle = curl_init(); if ($handle === false) { throw new RuntimeException('Unable to initialise cURL for Kepware request.'); } curl_setopt_array($handle, $options); $response = curl_exec($handle); $error = curl_error($handle); $errno = curl_errno($handle); $status = (int) curl_getinfo($handle, CURLINFO_HTTP_CODE); curl_close($handle); if ($response === false) { $shouldRetry = in_array($errno, $retryableCurlErrors, true) && $attempt < $attempts; if ($shouldRetry) { error_log(sprintf( 'Kepware request to %s attempt %d failed with cURL error %d: %s. Retrying in %.2f seconds.', $path, $attempt, $errno, $error, $retryDelay )); if ($retryDelay > 0.0) { usleep((int) round($retryDelay * 1_000_000)); } continue; } throw new RuntimeException('Kepware request failed: ' . $error, $errno); } $decoded = json_decode($response, true); if (!is_array($decoded)) { $snippet = substr($response, 0, 200); error_log('Kepware returned a non-JSON payload: ' . $snippet); if ($attempt < $attempts) { if ($retryDelay > 0.0) { usleep((int) round($retryDelay * 1_000_000)); } continue; } throw new RuntimeException('Invalid JSON response from Kepware: ' . $snippet); } if ($status >= 400) { if ($status >= 500 && $status < 600 && $attempt < $attempts) { error_log(sprintf( 'Kepware request to %s attempt %d returned HTTP %d. Retrying in %.2f seconds.', $path, $attempt, $status, $retryDelay )); if ($retryDelay > 0.0) { usleep((int) round($retryDelay * 1_000_000)); } continue; } throw new RuntimeException('Kepware request error (HTTP ' . $status . ').'); } if ($attempt > 1) { error_log(sprintf('Kepware request to %s succeeded on attempt %d.', $path, $attempt)); } return $decoded; } throw new RuntimeException('Kepware request failed after ' . $attempts . ' attempts.'); } } if (!function_exists('kepware_read')) { /** * Read tag values from Kepware. * * @param string[] $tagIds List of tag identifiers. * * @return array> */ function kepware_read(array $tagIds): array { if ($tagIds === []) { return []; } $query = http_build_query(['ids' => implode(',', $tagIds)]); $payload = kepware_rest_request('GET', 'read?' . $query); $results = []; foreach ($payload['readResults'] ?? [] as $result) { if (!isset($result['id'])) { continue; } $results[$result['id']] = [ 'value' => $result['v'] ?? null, 'timestamp' => $result['t'] ?? null, 'status' => $result['s'] ?? null, ]; } return $results; } } if (!function_exists('kepware_write')) { /** * Write tag values via Kepware. * * @param array> $writeRequest * * @return array> */ function kepware_write(array $writeRequest): array { if ($writeRequest === []) { return []; } $payload = kepware_rest_request('POST', 'write', $writeRequest); if (isset($payload['writeResults']) && is_array($payload['writeResults'])) { return $payload['writeResults']; } return $payload; } }