BetterFrame/server/src/shared/cloud-cameras/tplink.ts
Mitchell R a9484d1dd7
feat(cloud-cameras): type=cloud + bidirectional sync + PG default
Cloud cameras are now a distinct type ('cloud') managed entirely by
sync. Bidirectional: cameras added in vendor cloud appear automatically,
removed cameras get deleted. Cloud cameras and their entities are
read-only in admin UI — no manual editing.

- Camera type CHECK widened to include 'cloud'
- New columns: cloud_account_id, cloud_vendor_camera_id,
  cloud_stream_url, cloud_stream_type
- Repo: upsertCloudCamera, deleteCloudCamerasNotIn,
  listCloudCamerasByAccount
- Sync replaces import: full reconciliation per account
- Hik-Connect: fetch HLS preview URLs via previewURLs endpoint
- Tuya: fetch stream URLs during sync (not just on demand)
- Kiosk API: GET /api/kiosk/cameras/:id/stream returns fresh
  relay URL from vendor (session-based URLs expire)
- Cloud cameras show read-only detail page with cloud badge
- Coolify compose: postgres 18 as default, BF_DB=postgres,
  server depends_on postgres healthy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-23 11:36:49 +02:00

57 lines
2.2 KiB
TypeScript

/**
* TP-Link (Tapo/VIGI) camera integration.
*
* TP-Link has NO public cloud API. Tapo cameras support local RTSP +
* ONVIF. VIGI is enterprise-only with direct NVR access. For
* BetterFrame: connect to cameras directly via RTSP on the LAN.
*
* RTSP format: rtsp://user:pass@camera_ip/stream1 (main)
* rtsp://user:pass@camera_ip/stream2 (sub)
*
* This provider is essentially a direct-RTSP helper — no cloud relay.
* All auth on server — kiosk only gets RTSP URLs.
*/
import type { CloudCameraProvider, CloudCamera, CloudVendor } from "./types.js";
export class TpLinkProvider implements CloudCameraProvider {
vendor: CloudVendor = "tplink";
credentialFields() {
return [
{ name: "host", label: "Camera IP/Host", type: "text" as const, required: true },
{ name: "username", label: "Camera Username", type: "text" as const, required: true },
{ name: "password", label: "Camera Password", type: "password" as const, required: true },
];
}
async testCredentials(creds: Record<string, string>): Promise<{ ok: boolean; error?: string }> {
// No HTTP API — test by probing RTSP port (554).
const { createConnection } = await import("node:net");
const host = creds["host"] ?? "localhost";
return new Promise((resolve) => {
const sock = createConnection(
{ host, port: 554, timeout: 3000 },
() => { sock.destroy(); resolve({ ok: true }); },
);
sock.on("error", () => resolve({ ok: false, error: "RTSP port unreachable" }));
sock.on("timeout", () => { sock.destroy(); resolve({ ok: false, error: "Timeout" }); });
});
}
async listCameras(creds: Record<string, string>): Promise<CloudCamera[]> {
return [{
vendor_id: creds["host"] ?? "unknown",
name: `TP-Link Camera (${creds["host"]})`,
model: null,
rtsp_url: `rtsp://${creds["username"]}:${creds["password"]}@${creds["host"]}/stream1`,
relay_url: null,
online: true,
stream_type: "rtsp",
extra: {},
}];
}
async getStreamUrl(creds: Record<string, string>, _vendorCameraId: string): Promise<string | null> {
return `rtsp://${creds["username"]}:${creds["password"]}@${creds["host"]}/stream1`;
}
}