'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);