Newer
Older
smart-home-server / webclient-vue / src / stores / devices.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 1 day ago 4 KB Add script detail pages with scope grouping
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,
  }),

  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;
    },
  },
});