Files
api/services/nginx/app/resources/edge-gateway-agent/lan-worker.php
T
Jeppe Bundgaard eeccacb2a7 Add unit tests for department lane dynamic image overrides and introduce classes for self-serve signal and virtual hardware management
- Add `DepartmentLaneDynamicImageRouteTest` to verify dynamic image preview handling for studio lanes.
- Introduce `selfserve_machine_signal` class to standardize signal normalization, recording, and gateway signal management workflows.
- Add `selfserve_virtual_hardware` class to handle virtual hardware configurations, including gateway and binding management.
- Enhance structure with auxiliary methods for payload normalization, workspace merging, and validation warnings.
2026-04-29 09:52:51 +02:00

183 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
function worker_json_response(int $status, array $payload): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_SLASHES);
}
function worker_read_json_body(): array
{
$raw = file_get_contents('php://input');
if (!is_string($raw) || trim($raw) === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function worker_http_get_json(string $url, int $timeoutSeconds = 8): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeoutSeconds,
CURLOPT_CONNECTTIMEOUT => min(5, $timeoutSeconds),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$raw = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($raw === false || $status >= 400) {
throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status);
}
$decoded = json_decode((string)$raw, true);
if (!is_array($decoded)) {
throw new RuntimeException('Invalid JSON response from device');
}
return $decoded;
}
function worker_fetch_shelly_state(string $localIp, int $channel, bool $includeInput = false): array
{
if ($localIp === '') {
throw new RuntimeException('Missing Shelly IP address');
}
$input = $includeInput ? worker_fetch_shelly_input_state($localIp, $channel) : null;
try {
$payload = worker_http_get_json(sprintf('http://%s/rpc/Switch.GetStatus?id=%d', $localIp, $channel));
return [
'online' => true,
'on' => (bool)($payload['output'] ?? false),
'output' => (bool)($payload['output'] ?? false),
'input_state' => $input['state'] ?? null,
'input' => $input,
'raw' => $payload,
];
} catch (Throwable) {
$payload = worker_http_get_json(sprintf('http://%s/relay/%d', $localIp, $channel));
return [
'online' => true,
'on' => (bool)($payload['ison'] ?? false),
'output' => (bool)($payload['ison'] ?? false),
'input_state' => $input['state'] ?? null,
'input' => $input,
'raw' => $payload,
];
}
}
function worker_fetch_shelly_input_state(string $localIp, int $channel): ?array
{
if ($localIp === '') {
throw new RuntimeException('Missing Shelly IP address');
}
try {
$payload = worker_http_get_json(sprintf('http://%s/rpc/Input.GetStatus?id=%d', $localIp, $channel), 2);
return [
'online' => true,
'state' => (bool)($payload['state'] ?? false),
'raw' => $payload,
];
} catch (Throwable) {
return null;
}
}
function worker_switch_shelly_state(string $localIp, int $channel, bool $on): array
{
try {
worker_http_get_json(sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false'));
} catch (Throwable) {
worker_http_get_json(sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off'));
}
return worker_fetch_shelly_state($localIp, $channel);
}
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
$path = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?? '/');
$body = worker_read_json_body();
$hostname = gethostname() ?: 'truckwash-edge';
try {
if ($method === 'GET' && $path === '/health') {
worker_json_response(200, [
'status' => 'healthy',
'service' => 'lan-worker',
'timestamp' => date(DateTimeInterface::ATOM),
]);
return;
}
if ($method === 'POST' && $path === '/discover') {
worker_json_response(200, [
'inventory' => [[
'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10),
'local_ip' => gethostbyname($hostname),
'model' => 'TruckWash Edge Gateway',
'channel_count' => 1,
'online' => true,
'capabilities' => [
'local_discovery' => true,
'relay_commands' => true,
'gateway_management_v2' => true,
],
'metadata' => [
'hostname' => $hostname,
'runtime' => 'compose-lan-worker',
'php_version' => PHP_VERSION,
],
]],
]);
return;
}
if ($method === 'POST' && $path === '/relay/status') {
$localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
$channel = (int)($body['channel'] ?? 0);
$includeInput = (bool)($body['include_input'] ?? $body['includeInput'] ?? false);
worker_json_response(200, worker_fetch_shelly_state($localIp, $channel, $includeInput));
return;
}
if ($method === 'POST' && $path === '/relay/input-status') {
$localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
$channel = (int)($body['channel'] ?? 0);
$input = worker_fetch_shelly_input_state($localIp, $channel);
if ($input === null) {
throw new RuntimeException('Shelly input status is not available');
}
worker_json_response(200, $input);
return;
}
if ($method === 'POST' && $path === '/relay/switch') {
$localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? ''));
$channel = (int)($body['channel'] ?? 0);
$on = (bool)($body['on'] ?? false);
worker_json_response(200, worker_switch_shelly_state($localIp, $channel, $on));
return;
}
worker_json_response(404, ['message' => 'Not found']);
} catch (Throwable $throwable) {
worker_json_response(422, [
'message' => $throwable->getMessage(),
'error_code' => 'EDGE_GATEWAY_WORKER_FAILED',
]);
}