Newer
Older
smart-home-server / webclient-vue / src / features / devices / components / DeviceChannelsState.vue
<script setup>
import { computed } from "vue";
import { GnLoader, GnBadge } from "gnexus-ui-kit/vue";

const props = defineProps({
  deviceType: { type: String, default: "" },
  response: { type: Object, default: null },
  loading: { type: Boolean, default: false },
  error: { type: [String, Object], default: null },
  connectionStatus: { type: String, default: "unknown" },
});

const isOffline = computed(() => props.connectionStatus === "lost");
const channels = computed(() => props.response?.channels || []);
const sensors = computed(() => props.response?.sensors || {});

const hatchState = computed(() => props.response?.hatch?.state || "—");
const hatchPosition = computed(() => props.response?.hatch?.position_pct ?? "—");
const hatchVariant = computed(() =>
  String(hatchState.value).includes("open") ? "warning" : "primary"
);
const hatchShowPosition = computed(() =>
  String(hatchState.value).includes("open")
);

const isOk = computed(() => props.response?.status === "ok");

const jsonPreview = computed(() => {
  if (!props.response) return "";
  try {
    return JSON.stringify(props.response).slice(0, 200);
  } catch {
    return "";
  }
});

const hasContent = computed(() => {
  if (!isOk.value) return false;
  if (props.deviceType === "relay" || props.deviceType === "button") {
    return channels.value.length > 0;
  }
  if (props.deviceType === "sensor") {
    return Object.keys(sensors.value).length > 0;
  }
  if (props.deviceType === "hatch") {
    return !!props.response?.hatch;
  }
  return false;
});

function channelId(ch) {
  return ch.id ?? ch.channel;
}

function buttonVariant(indicator) {
  const map = {
    enabled: "success",
    disabled: "secondary",
    mute: "primary",
    waiting: "warning",
    error: "danger",
  };
  return map[indicator] || "secondary";
}
</script>

<template>
  <div class="device-channels-state">
    <GnLoader v-if="loading" circle size="sm" label="Loading state" />

    <GnBadge v-else-if="isOffline" variant="danger" size="sm">
      <i class="ph ph-wifi-slash"></i> Offline
    </GnBadge>

    <GnBadge v-else-if="error || !isOk" variant="danger" size="sm">
      <i class="ph ph-warning-octagon"></i>
      <template v-if="typeof error === 'string'">{{ error }}</template>
      <template v-else>Error</template>
    </GnBadge>

    <div v-else-if="hasContent" class="channels-grid">
      <!-- Relay -->
      <template v-if="deviceType === 'relay'">
        <GnBadge
          v-for="ch in channels"
          :key="channelId(ch)"
          :variant="ch.state === 'on' || ch.state === true ? 'success' : 'secondary'"
          size="sm"
        >
          <template v-if="channels.length > 1">{{ channelId(ch) }}: </template>
          <b>{{ ch.state == "off" ? "OFF" : "ON" }}</b>
        </GnBadge>
      </template>

      <!-- Button -->
      <template v-else-if="deviceType === 'button'">
        <GnBadge
          v-for="ch in channels"
          :key="channelId(ch)"
          :variant="buttonVariant(ch.indicator)"
          size="sm"
        >
          {{ channelId(ch) }}: <b>{{ ch.indicator }}</b>
        </GnBadge>
      </template>

      <!-- Sensor -->
      <template v-else-if="deviceType === 'sensor'">
        <GnBadge v-if="sensors.radar" variant="primary" size="sm">
          <i class="ph" :class="sensors.radar.presence ? 'ph-user-square' : 'ph-square'"></i>
          <template v-if="sensors.radar.presence">
            {{ sensors.radar.activity_score }}
            <i v-if="sensors.radar.activity_score_dynamics === 'increasing'" class="ph ph-caret-up"></i>
            <i v-else-if="sensors.radar.activity_score_dynamics === 'decreasing'" class="ph ph-caret-down"></i>
          </template>
        </GnBadge>

        <GnBadge v-if="sensors.temperature" variant="primary" size="sm">
          <i class="ph ph-thermometer"></i>
          {{ sensors.temperature.current }}°C
          <i v-if="sensors.temperature.dynamics === 'increasing'" class="ph ph-caret-up"></i>
          <i v-else-if="sensors.temperature.dynamics === 'decreasing'" class="ph ph-caret-down"></i>
        </GnBadge>

        <GnBadge v-if="sensors.humidity" variant="primary" size="sm">
          <i class="ph ph-drop-half-bottom"></i>
          {{ sensors.humidity.current }}%
          <i v-if="sensors.humidity.dynamics === 'increasing'" class="ph ph-caret-up"></i>
          <i v-else-if="sensors.humidity.dynamics === 'decreasing'" class="ph ph-caret-down"></i>
        </GnBadge>

        <GnBadge v-if="sensors.pressure" variant="primary" size="sm">
          {{ sensors.pressure.current }}hpa
          <i v-if="sensors.pressure.dynamics === 'increasing'" class="ph ph-caret-up"></i>
          <i v-else-if="sensors.pressure.dynamics === 'decreasing'" class="ph ph-caret-down"></i>
        </GnBadge>

        <GnBadge v-if="sensors.light" variant="primary" size="sm">
          <i class="ph ph-lightbulb"></i>
          {{ sensors.light.percent }}%
        </GnBadge>

        <GnBadge v-if="sensors.microphone" variant="primary" size="sm">
          <i class="ph ph-ear"></i>
          {{ sensors.microphone.current_noise }}dBi
          <i v-if="sensors.microphone.noise_dynamics === 'increasing'" class="ph ph-caret-up"></i>
          <i v-else-if="sensors.microphone.noise_dynamics === 'decreasing'" class="ph ph-caret-down"></i>
        </GnBadge>
      </template>

      <!-- Hatch -->
      <template v-else-if="deviceType === 'hatch'">
        <GnBadge :variant="hatchVariant" size="sm">
          {{ hatchState }}
          <template v-if="hatchShowPosition"> - {{ hatchPosition }}%</template>
        </GnBadge>
      </template>

      <template v-else>
        <span class="unknown-type text-muted">Unknown type</span>
        <pre v-if="jsonPreview" class="raw-json text-muted">{{ jsonPreview }}</pre>
      </template>
    </div>

    <GnBadge v-else variant="secondary" size="sm">No data</GnBadge>
  </div>
</template>

<style scoped>
.device-channels-state {
  display: inline-flex;
  align-items: center;
}
.channels-grid {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.unknown-type {
  font-size: 12px;
}
.raw-json {
  font-size: 10px;
  margin: 4px 0 0;
  max-width: 400px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>