import type { Zone, ZoneStatus, Device, DevicePhoto, ActivityEntry, NewbookSite, AppConfig, MhiGateway, MhiStatus, ActiveAcTemplate, DaikinStatus, } from './types' const BASE = '/hvac/api' async function request(path: string, opts: RequestInit = {}): Promise { const res = await fetch(`${BASE}${path}`, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts, }) if (res.status === 401) { ;(window.top ?? window).location.href = '/login' throw new Error('Unauthenticated') } if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Request failed: ${res.status}`) } return res.json() } // Zones export function fetchZones(): Promise { return request('/zones') } export function updateZone(id: number, body: Record): Promise { return request(`/zones/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function createZone(body: { name: string; zone_type: 'room' | 'public_area'; occupied_temp?: number; vacant_temp?: number }): Promise { return request('/zones', { method: 'POST', body: JSON.stringify(body) }) } export function fetchNewbookSites(): Promise<{ sites: NewbookSite[] }> { return request('/zones/newbook-sites') } export function saveExcludedSites(excludedSiteIds: string[]): Promise<{ ok: boolean }> { return request('/zones/newbook-sites', { method: 'PUT', body: JSON.stringify({ excluded_site_ids: excludedSiteIds }) }) } export function testNewbookConnection(): Promise<{ ok: boolean; message?: string; error?: string }> { return request('/zones/newbook-test', { method: 'POST', body: JSON.stringify({}) }) } export function syncZonesFromNewbook(): Promise<{ ok: boolean; created: number; updated: number; total: number }> { return request('/zones/sync-newbook', { method: 'POST', body: JSON.stringify({}) }) } // Status / activity export function fetchStatus(): Promise<{ mqtt_connected: boolean weather_peak_c: number | null weather_tier: string | null active_ac_template: ActiveAcTemplate zones: ZoneStatus[] }> { return request('/status') } export function fetchActivity(zoneId?: number, limit = 100): Promise { const params = new URLSearchParams() if (zoneId) params.set('zone_id', String(zoneId)) params.set('limit', String(limit)) return request(`/status/activity?${params.toString()}`) } // Override export function overrideZone(id: number, tempC: number): Promise<{ ok: boolean; successful: number; total: number; auto_mode: boolean; error?: string }> { return request(`/zones/${id}/override`, { method: 'POST', body: JSON.stringify({ temp_c: tempC }) }) } // Devices export function fetchDevices(): Promise { return request('/devices') } export function discoverDevices(deviceType: string): Promise<{ ok: boolean; devices?: unknown[]; found?: number; new?: number; note?: string; error?: string }> { return request('/devices/discover', { method: 'POST', body: JSON.stringify({ device_type: deviceType }) }) } export function updateDevice(id: number, body: { zone_id?: number | null; location?: string }): Promise { return request(`/devices/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function deleteDevice(id: number): Promise<{ ok: boolean }> { return request(`/devices/${id}`, { method: 'DELETE' }) } export function createMaintenanceAsset(id: number): Promise<{ ok: boolean; notImplemented?: boolean; error?: string }> { return request(`/devices/${id}/create-maintenance-asset`, { method: 'POST', body: JSON.stringify({}) }) } // Device photos — multipart, so no JSON content-type header export async function uploadDevicePhoto(deviceId: number, file: File, photoType: 'device' | 'serial_plate'): Promise { const form = new FormData() form.append('photo_type', photoType) form.append('file', file) const res = await fetch(`${BASE}/devices/${deviceId}/photos`, { method: 'POST', credentials: 'include', body: form }) if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Upload failed: ${res.status}`) } return res.json() } export function fetchDevicePhotos(deviceId: number): Promise { return request(`/devices/${deviceId}/photos`) } export function deleteDevicePhoto(id: number): Promise<{ ok: boolean }> { return request(`/photos/${id}`, { method: 'DELETE' }) } export function photoUrl(filePath: string): string { return `${BASE}/uploads${filePath}` } // MHI Modbus gateways (manual aircon control) export function fetchMhiGateways(): Promise { return request('/mhi-gateways') } export function createMhiGateway(body: { name: string; host: string; port?: number; slaveId?: number; addressBase?: number }): Promise { return request('/mhi-gateways', { method: 'POST', body: JSON.stringify(body) }) } export function updateMhiGateway(id: number, body: Partial<{ name: string; host: string; port: number; slaveId: number; addressBase: number }>): Promise { return request(`/mhi-gateways/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) } export function testMhiGatewayConnection(id: number): Promise<{ ok: boolean; note?: string; error?: string }> { return request(`/mhi-gateways/${id}/test-connection`, { method: 'POST', body: JSON.stringify({}) }) } export function deleteMhiGateway(id: number): Promise<{ ok: boolean }> { return request(`/mhi-gateways/${id}`, { method: 'DELETE' }) } export async function importMhiRegisterMap(id: number, file: File): Promise { const form = new FormData() form.append('file', file) const res = await fetch(`${BASE}/mhi-gateways/${id}/import`, { method: 'POST', credentials: 'include', body: form }) if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Import failed: ${res.status}`) } return res.json() } export function assignMhiUnit(gatewayId: number, unitIndex: number, body: { zone_id: number | null; location: string }): Promise { return request(`/mhi-gateways/${gatewayId}/units/${unitIndex}/assign`, { method: 'POST', body: JSON.stringify(body) }) } // MHI manual control (device-level — mhi_modbus rows only) export function fetchMhiStatus(deviceId: number): Promise { return request(`/devices/${deviceId}/mhi-status`) } export function sendMhiControl(deviceId: number, body: { onOff?: boolean; mode?: string; tempC?: number; fanSpeed?: string }): Promise<{ ok: boolean }> { return request(`/devices/${deviceId}/mhi-control`, { method: 'POST', body: JSON.stringify(body) }) } // Daikin manual control (device-level — daikin rows only) export function fetchDaikinStatus(deviceId: number): Promise { return request(`/devices/${deviceId}/daikin-status`) } export function sendDaikinControl(deviceId: number, body: { onOff?: boolean; mode?: string; tempC?: number; fanSpeed?: string }): Promise<{ ok: boolean }> { return request(`/devices/${deviceId}/daikin-control`, { method: 'POST', body: JSON.stringify(body) }) } // Config / settings export function fetchConfig(): Promise { return request('/config') } export function updateConfig(key: string, value: unknown): Promise<{ ok: boolean }> { return request(`/config/${key}`, { method: 'PUT', body: JSON.stringify({ value }) }) } export function fetchMqttStatus(): Promise<{ connected: boolean; note: string }> { return request('/settings/mqtt-status') }