import { ref, computed } from "vue";
import { defineStore } from "pinia";
import { devicesApi } from "../api/modules/devices";
import { useAsyncRequest } from "../composables/useAsyncRequest";
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", () => {
const devices = ref([]);
const stateByDeviceId = ref({});
const stateRunId = ref(0);
const rebootingIds = ref(new Set());
const currentDevice = ref(null);
const currentDeviceStatus = ref(null);
const lastLoadedAt = ref(null);
const isLoadingStates = ref(false);
const stateError = ref(null);
const listRequest = useAsyncRequest();
const detailRequest = useAsyncRequest();
const total = computed(() => devices.value.length);
const isRebooting = computed(() => (id) => rebootingIds.value.has(String(id)));
async function loadDevices() {
return listRequest.execute(async (signal) => {
const result = await devicesApi.list({ signal });
if (result.ok) {
devices.value = result.data?.data?.devices || [];
lastLoadedAt.value = new Date().toISOString();
}
return result;
});
}
function setDeviceState(device, patch) {
const deviceId = getDeviceId(device);
if (!deviceId) {
return;
}
stateByDeviceId.value = {
...stateByDeviceId.value,
[deviceId]: makeDeviceStatePatch(device, patch),
};
}
async function loadStatesFor(targets, options = {}) {
const runId = stateRunId.value + 1;
stateRunId.value = runId;
isLoadingStates.value = true;
stateError.value = null;
const items = [];
for (const device of targets) {
if (device.connection_status === "lost") {
setDeviceState(device, {
status: "skipped",
message: "Connection lost",
connectionStatus: "lost",
});
} else {
setDeviceState(device, {
status: "loading",
message: "Loading",
});
items.push(device);
}
}
try {
await runLimited(items, options.concurrency || DEFAULT_STATE_CONCURRENCY, async (device) => {
const result = await devicesApi.status(device.id);
if (stateRunId.value !== runId) {
return;
}
stateByDeviceId.value = {
...stateByDeviceId.value,
[getDeviceId(device)]: result.ok
? normalizeStatusSuccess(device, result)
: normalizeStatusError(device, result),
};
});
} catch (error) {
if (stateRunId.value === runId) {
stateError.value = {
type: "state_loader_error",
message: error?.message || "Device states loader failed",
};
}
} finally {
if (stateRunId.value === runId) {
isLoadingStates.value = false;
}
}
}
async function loadDeviceStates(options = {}) {
const runId = stateRunId.value + 1;
stateRunId.value = runId;
isLoadingStates.value = true;
stateError.value = null;
stateByDeviceId.value = {};
const targets = [];
for (const device of devices.value) {
if (device.connection_status === "lost") {
setDeviceState(device, {
status: "skipped",
message: "Connection lost",
connectionStatus: "lost",
});
} else {
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 (stateRunId.value !== runId) {
return;
}
stateByDeviceId.value = {
...stateByDeviceId.value,
[getDeviceId(device)]: result.ok
? normalizeStatusSuccess(device, result)
: normalizeStatusError(device, result),
};
});
} catch (error) {
if (stateRunId.value === runId) {
stateError.value = {
type: "state_loader_error",
message: error?.message || "Device states loader failed",
};
}
} finally {
if (stateRunId.value === runId) {
isLoadingStates.value = false;
}
}
}
async function rebootDevice(id) {
const deviceId = String(id);
rebootingIds.value = new Set(rebootingIds.value).add(deviceId);
const result = await devicesApi.reboot(id);
const next = new Set(rebootingIds.value);
next.delete(deviceId);
rebootingIds.value = next;
return result;
}
async function loadDeviceDetail(id) {
return detailRequest.execute(async (signal) => {
const result = await devicesApi.detail(id, { signal });
if (result.ok) {
currentDevice.value = result.data?.data?.device || null;
}
return result;
});
}
async function loadDeviceStatus(id) {
const result = await devicesApi.status(id);
if (!result.ok) {
currentDeviceStatus.value = {
ok: false,
error: result.error,
channels: [],
};
return result;
}
const payload = result.data?.data?.device || {};
const response = payload.device_response || {};
currentDeviceStatus.value = {
ok: true,
channels: response.channels || [],
raw: response,
};
return result;
}
async function updateDeviceName(id, name) {
const result = await devicesApi.updateName(id, name);
if (result.ok && currentDevice.value) {
currentDevice.value = { ...currentDevice.value, name };
}
return result;
}
async function updateDeviceDescription(id, description) {
const result = await devicesApi.updateDescription(id, description);
if (result.ok && currentDevice.value) {
currentDevice.value = { ...currentDevice.value, description };
}
return result;
}
async function updateDeviceAlias(id, newAlias) {
const result = await devicesApi.updateAlias(id, newAlias);
if (result.ok && currentDevice.value) {
currentDevice.value = { ...currentDevice.value, alias: newAlias };
}
return result;
}
async function removeDevice(id) {
return devicesApi.remove(id);
}
async function unassignDevice(id) {
const result = await devicesApi.unassign(id);
if (result.ok && currentDevice.value) {
currentDevice.value = { ...currentDevice.value, area_id: null };
}
return result;
}
async function assignToArea(id, areaId) {
const result = await devicesApi.placeInArea({ target_id: id, place_in_area_id: areaId });
if (result.ok && currentDevice.value) {
currentDevice.value = { ...currentDevice.value, area_id: areaId };
}
return result;
}
function clearDeviceDetail() {
currentDevice.value = null;
currentDeviceStatus.value = null;
detailRequest.clear();
}
return {
devices,
isLoading: listRequest.isLoading,
error: listRequest.error,
isLoadingStates,
stateError,
stateByDeviceId,
stateRunId,
rebootingIds,
lastLoadedAt,
currentDevice,
currentDeviceStatus,
isLoadingDetail: detailRequest.isLoading,
errorDetail: detailRequest.error,
total,
isRebooting,
loadDevices,
setDeviceState,
loadDeviceStates,
loadStatesFor,
rebootDevice,
loadDeviceDetail,
loadDeviceStatus,
updateDeviceName,
updateDeviceDescription,
updateDeviceAlias,
removeDevice,
unassignDevice,
assignToArea,
clearDeviceDetail,
};
});