import { defineStore } from "pinia";
import { devicesApi } from "../api/modules/devices";
const DEFAULT_STATE_CONCURRENCY = 4;
function getDeviceId(device) {
return String(device?.id || "");
}
function makeDeviceStatePatch(device, patch) {
return {
deviceId: getDeviceId(device),
status: "idle",
message: "",
response: null,
connectionStatus: device?.connection_status || "unknown",
updatedAt: null,
...patch,
};
}
function normalizeStatusSuccess(device, result) {
const payload = result.data?.data?.device || {};
const response = payload.device_response || {};
return makeDeviceStatePatch(device, {
status: "ready",
message: response.status || "ok",
response,
connectionStatus: "active",
updatedAt: new Date().toISOString(),
});
}
function normalizeStatusError(device, result) {
const raw = result.error?.raw || {};
const connectionStatus = raw?.data?.connection_status || device?.connection_status || "unknown";
return makeDeviceStatePatch(device, {
status: "error",
message: result.error?.message || "Device state is unavailable",
response: raw,
connectionStatus,
updatedAt: new Date().toISOString(),
});
}
async function runLimited(items, concurrency, worker) {
let nextIndex = 0;
const poolSize = Math.max(1, Math.min(concurrency, items.length));
async function runWorker() {
while (nextIndex < items.length) {
const item = items[nextIndex];
nextIndex += 1;
await worker(item);
}
}
await Promise.all(Array.from({ length: poolSize }, runWorker));
}
export const useDevicesStore = defineStore("devices", {
state: () => ({
devices: [],
isLoading: false,
error: null,
isLoadingStates: false,
stateError: null,
stateByDeviceId: {},
stateRunId: 0,
rebootingIds: new Set(),
_listAbortController: null,
lastLoadedAt: null,
currentDevice: null,
currentDeviceStatus: null,
isLoadingDetail: false,
errorDetail: null,
_detailAbortController: null,
}),
getters: {
total(state) {
return state.devices.length;
},
isRebooting: (state) => (id) => state.rebootingIds.has(id),
},
actions: {
async loadDevices() {
this._listAbortController?.abort();
const controller = new AbortController();
this._listAbortController = controller;
this.isLoading = true;
this.error = null;
const result = await devicesApi.list({ signal: controller.signal });
this._listAbortController = null;
this.isLoading = false;
if (!result.ok) {
if (result.error?.type === "timeout") {
return result;
}
this.error = result.error;
return result;
}
this.devices = result.data?.data?.devices || [];
this.lastLoadedAt = new Date().toISOString();
return result;
},
setDeviceState(device, patch) {
const deviceId = getDeviceId(device);
if (!deviceId) {
return;
}
this.stateByDeviceId = {
...this.stateByDeviceId,
[deviceId]: makeDeviceStatePatch(device, patch),
};
},
async loadDeviceStates(options = {}) {
const runId = this.stateRunId + 1;
this.stateRunId = runId;
this.isLoadingStates = true;
this.stateError = null;
this.stateByDeviceId = {};
const devices = this.devices.slice();
const targets = [];
for (const device of devices) {
if (device.connection_status === "lost") {
this.setDeviceState(device, {
status: "skipped",
message: "Connection lost",
connectionStatus: "lost",
});
} else {
this.setDeviceState(device, {
status: "loading",
message: "Loading",
});
targets.push(device);
}
}
try {
await runLimited(targets, options.concurrency || DEFAULT_STATE_CONCURRENCY, async (device) => {
const result = await devicesApi.status(device.id);
if (this.stateRunId !== runId) {
return;
}
this.stateByDeviceId = {
...this.stateByDeviceId,
[getDeviceId(device)]: result.ok
? normalizeStatusSuccess(device, result)
: normalizeStatusError(device, result),
};
});
} catch (error) {
if (this.stateRunId === runId) {
this.stateError = {
type: "state_loader_error",
message: error?.message || "Device states loader failed",
};
}
} finally {
if (this.stateRunId === runId) {
this.isLoadingStates = false;
}
}
},
async rebootDevice(id) {
const deviceId = String(id);
this.rebootingIds.add(deviceId);
const result = await devicesApi.reboot(id);
this.rebootingIds.delete(deviceId);
return result;
},
async loadDeviceDetail(id) {
this._detailAbortController?.abort();
const controller = new AbortController();
this._detailAbortController = controller;
this.isLoadingDetail = true;
this.errorDetail = null;
this.currentDevice = null;
this.currentDeviceStatus = null;
const result = await devicesApi.detail(id);
this._detailAbortController = null;
this.isLoadingDetail = false;
if (!result.ok) {
if (result.error?.type === "timeout") {
return result;
}
this.errorDetail = result.error;
return result;
}
this.currentDevice = result.data?.data?.device || null;
return result;
},
async loadDeviceStatus(id) {
const result = await devicesApi.status(id);
if (!result.ok) {
this.currentDeviceStatus = {
ok: false,
error: result.error,
channels: [],
};
return result;
}
const payload = result.data?.data?.device || {};
const response = payload.device_response || {};
this.currentDeviceStatus = {
ok: true,
channels: response.channels || [],
raw: response,
};
return result;
},
async updateDeviceName(id, name) {
const result = await devicesApi.updateName(id, name);
if (result.ok && this.currentDevice) {
this.currentDevice = { ...this.currentDevice, name };
}
return result;
},
async updateDeviceDescription(id, description) {
const result = await devicesApi.updateDescription(id, description);
if (result.ok && this.currentDevice) {
this.currentDevice = { ...this.currentDevice, description };
}
return result;
},
async updateDeviceAlias(id, newAlias) {
const result = await devicesApi.updateAlias(id, newAlias);
if (result.ok && this.currentDevice) {
this.currentDevice = { ...this.currentDevice, alias: newAlias };
}
return result;
},
async removeDevice(id) {
return devicesApi.remove(id);
},
async unassignDevice(id) {
const result = await devicesApi.unassign(id);
if (result.ok && this.currentDevice) {
this.currentDevice = { ...this.currentDevice, area_id: null };
}
return result;
},
async assignToArea(id, areaId) {
const result = await devicesApi.placeInArea({ target_id: id, place_in_area_id: areaId });
if (result.ok && this.currentDevice) {
this.currentDevice = { ...this.currentDevice, area_id: areaId };
}
return result;
},
clearDeviceDetail() {
this.currentDevice = null;
this.currentDeviceStatus = null;
this.errorDetail = null;
this._detailAbortController?.abort();
this._detailAbortController = null;
},
},
});