<?php
namespace SHServ\Tools\DeviceAPI;
class Base {
private string $ip_address;
private string $base_url;
private ?string $token;
/**
* @param string $ip_address IP устройства (без протокола и слеша в конце)
* @param string|null $token Токен авторизации (может быть null)
*/
public function __construct(string $ip_address, ?string $token = null) {
$this->ip_address = $ip_address;
$this->base_url = rtrim('http://' . $ip_address, '/');
$this->token = $token;
}
/**
* Локально обновить токен (без запроса к устройству).
*/
public function set_local_token(string $token): void {
$this->token = $token;
}
/**
* Получить текущий сохранённый токен.
*/
public function get_local_token(): ?string {
return $this->token;
}
/**
* GET /about (доступен всегда, без токена).
*/
public function get_about(): array {
return $this->request('GET', '/about');
}
/**
* GET /status
*
* В режиме normal требует токен, в setup может не требоваться.
* Токен отправляем, если он есть, но жёстко не требуем.
*/
public function get_status(): array {
return $this->request('GET', '/status');
}
/**
* POST /action
*
* В normal-режиме требует токен.
*
* @param string $action
* @param array $params
*/
public function post_action(string $action, array $params = []): array {
$body = [
'action' => $action,
'params' => (object)$params, // чтобы пустой массив ушёл как {}
];
return $this->request('POST', '/action', $body, true);
}
/**
* POST /set_token
*
* В setup-режиме не требует токен, в normal — требует.
* Мы отправляем токен, если он есть, но не делаем жёсткую проверку.
* При успешном ответе можно локально обновить токен клиента.
*/
public function remote_set_token(string $token): array {
$response = $this->request('POST', '/set_token', [
'token' => $token,
]);
if (($response['http_code'] ?? 0) === 200 && ($response['data']['status'] ?? null) === 'ok') {
$this->token = $token;
}
return $response;
}
/**
* POST /reboot
*
* Требует токен в normal-режиме.
*/
public function reboot(): array {
return $this->request('POST', '/reboot', (object)[], true);
}
/**
* POST /reset
*
* Требует токен в normal-режиме.
*/
public function reset(): array {
return $this->request('POST', '/reset', (object)[], true);
}
/**
* POST /set_device_name
*
* Требует токен в normal-режиме.
*/
public function set_device_name(string $device_name): array {
return $this->request('POST', '/set_device_name', [
'device_name' => $device_name,
], true);
}
/**
* Базовый HTTP-запрос к устройству.
*
* @param string $method GET|POST
* @param string $path Например, '/about'
* @param array|object|null $body Тело запроса для POST
* @param bool $require_token Обязателен ли токен для этого вызова
*
* @return array{
* http_code:int,
* headers:array<string,string>,
* raw:string|null,
* data:array<string,mixed>|null,
* error:string|null
* }
*/
private function request(string $method, string $path, $body = null, bool $require_token = false): array {
if ($require_token && $this->token === null) {
throw new \LogicException('Token is required for this endpoint but not set in DeviceApiClient');
}
$url = $this->base_url . $path;
$ch = curl_init();
if ($ch === false) {
throw new \RuntimeException('Failed to initialize cURL');
}
$headers = [
'Accept: application/json',
];
if ($this->token !== null) {
$headers[] = 'Authorization: Bearer ' . $this->token;
}
if (strtoupper($method) === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($body !== null) {
$json_body = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json_body === false) {
curl_close($ch);
throw new \RuntimeException('Failed to encode JSON body: ' . json_last_error_msg());
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_body);
$headers[] = 'Content-Type: application/json';
}
} else {
curl_setopt($ch, CURLOPT_HTTPGET, true);
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => $headers,
]);
$raw_response = curl_exec($ch);
if ($raw_response === false) {
$error_message = curl_error($ch);
curl_close($ch);
return [
'http_code' => 0,
'headers' => [],
'raw' => null,
'data' => null,
'error' => $error_message,
];
}
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$raw_headers = substr($raw_response, 0, $header_size);
$raw_body = substr($raw_response, $header_size);
$parsed_headers = $this->parse_headers($raw_headers);
$decoded = null;
$error = null;
if ($raw_body !== '') {
$decoded = json_decode($raw_body, true);
if (!is_array($decoded)) {
$error = 'Invalid JSON response from device';
$decoded = null;
}
}
return [
'http_code' => (int)$http_code,
'headers' => $parsed_headers,
'raw' => $raw_body,
'data' => $decoded,
'error' => $error,
];
}
/**
* Простейший парсер HTTP-заголовков.
*
* @param string $raw_headers
*
* @return array<string,string>
*/
private function parse_headers(string $raw_headers): array
{
$headers = [];
$lines = preg_split("/\r\n|\n|\r/", trim($raw_headers));
if (!is_array($lines)) {
return $headers;
}
foreach ($lines as $line) {
if (strpos($line, ':') === false) {
continue;
}
[$name, $value] = explode(':', $line, 2);
$name = trim($name);
$value = trim($value);
if ($name !== '') {
$headers[$name] = $value;
}
}
return $headers;
}
}