203 lines
7.6 KiB
PHP
203 lines
7.6 KiB
PHP
|
|
<?php declare(strict_types=1);
|
||
|
|
|
||
|
|
require __DIR__.'/bootstrap.php';
|
||
|
|
$pdo = db();
|
||
|
|
|
||
|
|
function json_out($data, int $code=200): never {
|
||
|
|
http_response_code($code);
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
|
|
exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
|
||
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||
|
|
$ADMIN = @trim(@file_get_contents(__DIR__.'/.admin_token')) ?: null;
|
||
|
|
|
||
|
|
/** Autentica device por token (Authorization: Bearer ... ou X-API-Key) */
|
||
|
|
function require_token_dev(PDO $pdo): string {
|
||
|
|
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||
|
|
if ($hdr && stripos($hdr,'Bearer ')===0) $hdr = trim(substr($hdr,7));
|
||
|
|
if ($hdr==='') $hdr = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||
|
|
if ($hdr==='') json_out(['error'=>'auth required'], 401);
|
||
|
|
|
||
|
|
$st = $pdo->prepare("SELECT id FROM device WHERE token=:t");
|
||
|
|
$st->execute([':t'=>$hdr]);
|
||
|
|
$id = $st->fetchColumn();
|
||
|
|
if (!$id) json_out(['error'=>'invalid token'], 401);
|
||
|
|
return (string)$id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Autentica admin por X-Admin-Token */
|
||
|
|
function require_admin_token(?string $admin): void {
|
||
|
|
if (!$admin) json_out(['error'=>'admin token missing'], 500);
|
||
|
|
$hdr = $_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '';
|
||
|
|
if ($hdr !== $admin) json_out(['error'=>'admin auth'], 401);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- HEALTH ---------- */
|
||
|
|
if ($path === '/api/health') json_out(['ok'=>true, 'php'=>PHP_VERSION]);
|
||
|
|
|
||
|
|
/* ---------- INGEST ---------- */
|
||
|
|
if ($path === '/api/ingest' && $method === 'POST') {
|
||
|
|
$dev = require_token_dev($pdo);
|
||
|
|
$raw = file_get_contents('php://input') ?: '';
|
||
|
|
$j = json_decode($raw, true);
|
||
|
|
if (!is_array($j)) json_out(['error'=>'invalid json'], 400);
|
||
|
|
|
||
|
|
$ts = isset($j['ts']) && is_numeric($j['ts']) ? (int)$j['ts'] : time();
|
||
|
|
$payload = $j; unset($payload['ts']);
|
||
|
|
|
||
|
|
// retry leve para "database is locked"
|
||
|
|
$tries = 0;
|
||
|
|
while (true) {
|
||
|
|
try {
|
||
|
|
$st = $pdo->prepare("INSERT INTO reading(device_id,ts,data) VALUES(:d,:t,:j)");
|
||
|
|
$st->execute([':d'=>$dev, ':t'=>$ts, ':j'=>json_encode($payload, JSON_UNESCAPED_UNICODE)]);
|
||
|
|
json_out(['stored'=>true, 'id'=>$pdo->lastInsertId()]);
|
||
|
|
} catch (PDOException $e) {
|
||
|
|
if (stripos($e->getMessage(), 'locked') !== false && ++$tries <= 5) { usleep(200000); continue; }
|
||
|
|
throw $e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- LAST ---------- */
|
||
|
|
if ($path === '/api/last' && $method === 'GET') {
|
||
|
|
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
|
||
|
|
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d ORDER BY ts DESC LIMIT 1");
|
||
|
|
$st->execute([':d'=>$dev]); $r=$st->fetch();
|
||
|
|
if (!$r) json_out(['device'=>$dev,'reading'=>null]);
|
||
|
|
$r['data'] = json_decode($r['data'], true);
|
||
|
|
json_out(['device'=>$dev,'reading'=>$r]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- DATA ---------- */
|
||
|
|
if ($path === '/api/data' && $method === 'GET') {
|
||
|
|
$dev = $_GET['device'] ?? null;
|
||
|
|
$from= isset($_GET['from']) ? (int)$_GET['from'] : time()-86400;
|
||
|
|
$to = isset($_GET['to']) ? (int)$_GET['to'] : time();
|
||
|
|
if (!$dev) json_out(['error'=>'device required'], 400);
|
||
|
|
|
||
|
|
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d AND ts BETWEEN :f AND :t ORDER BY ts ASC");
|
||
|
|
$st->execute([':d'=>$dev, ':f'=>$from, ':t'=>$to]);
|
||
|
|
$rows=$st->fetchAll();
|
||
|
|
foreach ($rows as &$r) $r['data']=json_decode($r['data'], true);
|
||
|
|
json_out(['device'=>$dev,'from'=>$from,'to'=>$to,'rows'=>$rows]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- STATE: SET ---------- */
|
||
|
|
if ($path === '/api/state/set' && $method === 'POST') {
|
||
|
|
require_admin_token($ADMIN);
|
||
|
|
$raw=file_get_contents('php://input') ?: '';
|
||
|
|
$j = json_decode($raw, true);
|
||
|
|
if (!is_array($j) || empty($j['device'])) json_out(['error'=>'device required'], 400);
|
||
|
|
|
||
|
|
$dev = $j['device']; $now = time();
|
||
|
|
$pairs=[];
|
||
|
|
if (isset($j['kv']) && is_array($j['kv'])) {
|
||
|
|
foreach ($j['kv'] as $k=>$v) $pairs[]=[(string)$k, json_encode($v, JSON_UNESCAPED_UNICODE)];
|
||
|
|
} elseif (isset($j['key'])) {
|
||
|
|
$pairs[]=[(string)$j['key'], json_encode($j['value'] ?? null, JSON_UNESCAPED_UNICODE)];
|
||
|
|
} else json_out(['error'=>'payload'], 400);
|
||
|
|
|
||
|
|
$st=$pdo->prepare(
|
||
|
|
"INSERT INTO state(device_id,key,value,updated_ts)
|
||
|
|
VALUES(:d,:k,:v,:t)
|
||
|
|
ON CONFLICT(device_id,key)
|
||
|
|
DO UPDATE SET value=excluded.value, updated_ts=excluded.updated_ts"
|
||
|
|
);
|
||
|
|
foreach ($pairs as $p) $st->execute([':d'=>$dev, ':k'=>$p[0], ':v'=>$p[1], ':t'=>$now]);
|
||
|
|
json_out(['ok'=>true,'updated'=>count($pairs)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- STATE: GET ---------- */
|
||
|
|
if ($path === '/api/state/get' && $method === 'GET') {
|
||
|
|
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
|
||
|
|
// Admin OU device com token
|
||
|
|
if (!($ADMIN && (($_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '') === $ADMIN))) {
|
||
|
|
require_token_dev($pdo);
|
||
|
|
}
|
||
|
|
$keys = isset($_GET['keys']) && $_GET['keys']!=='' ? explode(',', $_GET['keys']) : null;
|
||
|
|
|
||
|
|
if ($keys) {
|
||
|
|
$in = rtrim(str_repeat('?,', count($keys)), ',');
|
||
|
|
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=? AND key IN ($in)");
|
||
|
|
$st->execute(array_merge([$dev], $keys));
|
||
|
|
} else {
|
||
|
|
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=?");
|
||
|
|
$st->execute([$dev]);
|
||
|
|
}
|
||
|
|
$out=[];
|
||
|
|
while ($r=$st->fetch()) $out[$r['key']] = json_decode($r['value'], true);
|
||
|
|
json_out(['device'=>$dev,'kv'=>$out]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- COMMANDS: PUSH (admin) ---------- */
|
||
|
|
if ($path === '/api/commands/push' && $method === 'POST') {
|
||
|
|
require_admin_token($ADMIN);
|
||
|
|
$raw=file_get_contents('php://input') ?: '';
|
||
|
|
$j = json_decode($raw, true);
|
||
|
|
if (!is_array($j) || empty($j['device']) || empty($j['cmd'])) json_out(['error'=>'device/cmd required'], 400);
|
||
|
|
|
||
|
|
$st=$pdo->prepare("INSERT INTO command(device_id,ts,cmd,params,status) VALUES(:d,:t,:c,:p,:s)");
|
||
|
|
$st->execute([
|
||
|
|
':d'=>$j['device'], ':t'=>time(), ':c'=>$j['cmd'],
|
||
|
|
':p'=>isset($j['params']) ? json_encode($j['params'], JSON_UNESCAPED_UNICODE) : null,
|
||
|
|
':s'=>'queued'
|
||
|
|
]);
|
||
|
|
json_out(['queued'=>true,'id'=>$pdo->lastInsertId()]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- COMMANDS: POLL (device) ---------- */
|
||
|
|
if ($path === '/api/commands/poll' && $method === 'GET') {
|
||
|
|
$dev = require_token_dev($pdo);
|
||
|
|
$st=$pdo->prepare("SELECT id,cmd,params,ts FROM command WHERE device_id=:d AND status='queued' ORDER BY id ASC LIMIT 10");
|
||
|
|
$st->execute([':d'=>$dev]);
|
||
|
|
$rows=$st->fetchAll();
|
||
|
|
foreach ($rows as &$r) $r['params'] = $r['params'] ? json_decode($r['params'], true) : (object)[];
|
||
|
|
json_out(['device'=>$dev,'commands'=>$rows]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------- COMMANDS: ACK (robusto) ---------- */
|
||
|
|
if ($path === '/api/commands/ack' && $method === 'POST') {
|
||
|
|
// valida token se quiseres (mas não uses device_id porque a tabela não tem)
|
||
|
|
require_token_dev($pdo);
|
||
|
|
|
||
|
|
$raw = file_get_contents('php://input') ?: '';
|
||
|
|
$j = json_decode($raw, true) ?: [];
|
||
|
|
if (empty($j['id'])) json_out(['error' => 'id required'], 400);
|
||
|
|
|
||
|
|
// interpreta "success"/"ok"/"status" de forma robusta
|
||
|
|
$val = $j['success'] ?? $j['ok'] ?? $j['status'] ?? true;
|
||
|
|
if (is_bool($val)) {
|
||
|
|
$ok = $val;
|
||
|
|
} elseif (is_numeric($val)) {
|
||
|
|
$ok = ((int)$val) === 1;
|
||
|
|
} elseif (is_string($val)) {
|
||
|
|
$ok = in_array(strtolower(trim($val)), ['1','true','ok','done','yes','y'], true);
|
||
|
|
} else {
|
||
|
|
$ok = true; // default otimista
|
||
|
|
}
|
||
|
|
|
||
|
|
// se tiveres coluna ack_ts, ativa a linha correspondente
|
||
|
|
$sql = "UPDATE commands
|
||
|
|
SET status = :status
|
||
|
|
WHERE id = :i";
|
||
|
|
$args = [':status' => ($ok ? 'done' : 'failed'), ':i' => (int)$j['id']];
|
||
|
|
|
||
|
|
// descomenta se existir a coluna ack_ts
|
||
|
|
// $sql = "UPDATE commands SET status=:status, ack_ts=:a WHERE id=:i";
|
||
|
|
// $args[':a'] = time();
|
||
|
|
|
||
|
|
$st = $pdo->prepare($sql);
|
||
|
|
$st->execute($args);
|
||
|
|
|
||
|
|
json_out(['updated' => $st->rowCount(), 'id' => (int)$j['id'], 'success' => $ok]);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
/* ---------- 404 ---------- */
|
||
|
|
json_out(['error'=>'not found','path'=>$path], 404);
|