mirror of
https://github.com/koush/scrypted.git
synced 2026-04-29 03:10:25 +01:00
598 lines
19 KiB
Vue
598 lines
19 KiB
Vue
<template>
|
|
<v-layout wrap>
|
|
<v-flex xs12 v-if="deviceAlerts.length">
|
|
<v-alert dark dismissible @input="removeAlert(alert)" v-for="alert in deviceAlerts" :key="alert.id" xs12 md6 lg6
|
|
color="primary" icon="mdi-vuetify" border="left">
|
|
<template v-slot:prepend>
|
|
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
|
|
getAlertIcon(alert)
|
|
}}</v-icon>
|
|
</template>
|
|
<div class="caption">{{ alert.title }}</div>
|
|
<div v-linkified:options="{ className: 'alert-link' }" v-html="alert.message.replace('origin:', origin)"></div>
|
|
</v-alert>
|
|
</v-flex>
|
|
|
|
<v-flex v-for="iface in aboveInterfaces" :key="iface" xs12>
|
|
<component v-if="noCardInterfaces.includes(iface)" :value="deviceState" :device="device" :is="iface"></component>
|
|
<v-card v-else>
|
|
<CardTitle>{{ getInterfaceFriendlyName(iface) }}</CardTitle>
|
|
<component :value="deviceState" :device="device" :is="iface"></component>
|
|
</v-card>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-if="showConsole" ref="consoleEl">
|
|
<ConsoleCard :deviceId="id"></ConsoleCard>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-if="showRepl" ref="replEl">
|
|
<REPLCard :deviceId="id"></REPLCard>
|
|
</v-flex>
|
|
<v-flex xs12 md7>
|
|
<v-layout row wrap>
|
|
<v-flex xs12 v-for="iface in leftAboveInterfaces" :key="iface" class="pb-0">
|
|
<component v-if="noCardInterfaces.includes(iface)" :value="deviceState" :device="device" :is="iface">
|
|
</component>
|
|
<v-card v-else>
|
|
<CardTitle>{{ getInterfaceFriendlyName(iface) }}</CardTitle>
|
|
<component :value="deviceState" :device="device" :is="iface"></component>
|
|
</v-card>
|
|
</v-flex>
|
|
<v-flex xs12 class="pb-0">
|
|
<v-card raised>
|
|
<v-card-title>
|
|
{{ (device && device.name) || "No Device Name" }}
|
|
<v-layout mr-1 row justify-end align-center v-if="cardHeaderInterfaces.length">
|
|
<component :value="deviceState" :device="device" :is="iface" v-for="iface in cardHeaderInterfaces"
|
|
:key="iface"></component>
|
|
</v-layout>
|
|
</v-card-title>
|
|
|
|
<v-card-subtitle v-if="ownerDevice && pluginData">
|
|
<a @click="openDevice(ownerDevice.id)">{{ ownerDevice.name }}</a>
|
|
(Native ID: {{ pluginData.nativeId }})
|
|
<div></div>
|
|
</v-card-subtitle>
|
|
|
|
<v-flex v-if="cardButtonInterfaces.length">
|
|
<v-layout align-center justify-center>
|
|
<component v-for="iface in cardButtonInterfaces" :key="iface" :value="deviceState" :device="device"
|
|
:is="iface"></component>
|
|
</v-layout>
|
|
</v-flex>
|
|
|
|
<v-card-actions v-if="!ownerDevice">
|
|
<v-spacer></v-spacer>
|
|
<v-btn small outlined color="blue" @click="reloadPlugin" class="mr-2">Reload Plugin</v-btn>
|
|
<PluginAdvancedUpdate v-if="pluginData" :pluginData="pluginData" @installed="reload" />
|
|
</v-card-actions>
|
|
|
|
<v-card-actions>
|
|
<component v-for="iface in cardActionInterfaces" :key="iface" :value="deviceState" :device="device"
|
|
:is="iface"></component>
|
|
<v-spacer></v-spacer>
|
|
|
|
<v-btn color="info" text @click="openConsole">Console</v-btn>
|
|
|
|
<v-tooltip bottom v-if="device.info && device.info.managementUrl">
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn x-small v-on="on" color="info" text @click="openManagementUrl">
|
|
<v-icon x-small>fa-wrench</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>Open Device Management Url</span>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip bottom>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn x-small v-on="on" color="info" text @click="openRepl">
|
|
<v-icon x-small>fa-terminal</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>REPL</span>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip bottom>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn x-small v-on="on" color="info" text @click="openLogs">
|
|
<v-icon x-small>fa-history</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>Events</span>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip bottom v-if="pluginData">
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn x-small v-on="on" color="info" text @click="showStorage = !showStorage">
|
|
<v-icon x-small>fa-hdd</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>Storage</span>
|
|
</v-tooltip>
|
|
|
|
<v-dialog v-model="showDelete" width="500">
|
|
<template #activator="{ on: dialog }">
|
|
<v-tooltip bottom>
|
|
<template #activator="{ on: tooltip }">
|
|
<v-btn x-small v-on="{ ...tooltip, ...dialog }" color="error" text>
|
|
<v-icon x-small>fa-trash</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<span>Delete</span>
|
|
</v-tooltip>
|
|
</template>
|
|
|
|
<v-card>
|
|
<CardTitle style="margin-bottom: 8px" class="red white--text" primary-title>Delete Device</CardTitle>
|
|
|
|
<v-card-text>This will permanently delete the device. It can not be
|
|
undone.</v-card-text>
|
|
|
|
<v-divider></v-divider>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn color="primary" text @click="showDelete = false">Cancel</v-btn>
|
|
<v-btn color="red" text @click="remove">Delete Device</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-if="deviceComponent && deviceComponent !== 'Script'">
|
|
<component @save="saveStorage" :is="deviceComponent" v-model="deviceData" :id="id" ref="componentCard">
|
|
</component>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-for="iface in leftInterfaces" :key="iface" class="pb-0">
|
|
<component v-if="noCardInterfaces.includes(iface)" :value="deviceState" :device="device" :is="iface">
|
|
</component>
|
|
<v-card v-else>
|
|
<CardTitle>{{ getInterfaceFriendlyName(iface) }}</CardTitle>
|
|
<component :value="deviceState" :device="device" :is="iface"></component>
|
|
</v-card>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-if="showStorage">
|
|
<v-card raised>
|
|
<CardTitle>Storage</CardTitle>
|
|
<v-container>
|
|
<v-layout>
|
|
<v-flex xs12>
|
|
<Storage v-model="pluginData.storage" @input="onChange" @save="saveStorage"></Storage>
|
|
</v-flex>
|
|
</v-layout>
|
|
</v-container>
|
|
</v-card>
|
|
</v-flex>
|
|
</v-layout>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 md5>
|
|
<v-layout row wrap>
|
|
<v-flex xs12 v-for="iface in rightInterfaces" :key="iface" class="pb-0">
|
|
<component v-if="noCardInterfaces.includes(iface)" :value="deviceState" :device="device" :is="iface">
|
|
</component>
|
|
<v-card v-else>
|
|
<CardTitle>{{ getInterfaceFriendlyName(iface) }}</CardTitle>
|
|
<component :value="deviceState" :device="device" :is="iface"></component>
|
|
</v-card>
|
|
</v-flex>
|
|
|
|
<v-flex v-if="showLogs" ref="logsEl">
|
|
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
|
|
</v-flex>
|
|
|
|
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
|
|
<Settings :device="device"></Settings>
|
|
</v-flex>
|
|
</v-layout>
|
|
</v-flex>
|
|
</v-layout>
|
|
</template>
|
|
<script>
|
|
import VueSlider from "vue-slider-component";
|
|
import "vue-slider-component/theme/material.css";
|
|
|
|
import LogCard from "./builtin/LogCard.vue";
|
|
import ConsoleCard from "./ConsoleCard.vue";
|
|
import REPLCard from "./REPLCard.vue";
|
|
import {
|
|
getComponentWebPath,
|
|
getDeviceViewPath,
|
|
removeAlert,
|
|
getAlertIcon,
|
|
hasFixedPhysicalLocation,
|
|
getInterfaceFriendlyName,
|
|
deviceIsEditable,
|
|
} from "./helpers";
|
|
import { ScryptedInterface } from "@scrypted/types";
|
|
import RTCSignalingClient from "../interfaces/RTCSignalingClient.vue";
|
|
import Notifier from "../interfaces/Notifier.vue";
|
|
import OnOff from "../interfaces/OnOff.vue";
|
|
import Brightness from "../interfaces/Brightness.vue";
|
|
import Battery from "../interfaces/Battery.vue";
|
|
import Lock from "../interfaces/Lock.vue";
|
|
import ColorSettingHsv from "../interfaces/ColorSettingHsv.vue";
|
|
import ColorSettingRgb from "../interfaces/ColorSettingRgb.vue";
|
|
import OauthClient from "../interfaces/OauthClient.vue";
|
|
import Camera from "../interfaces/Camera.vue";
|
|
import VideoClips from "../interfaces/VideoClips.vue";
|
|
import Thermometer from "../interfaces/sensors/Thermometer.vue";
|
|
import HumiditySensor from "../interfaces/sensors/HumiditySensor.vue";
|
|
import EntrySensor from "../interfaces/sensors/EntrySensor.vue";
|
|
import MotionSensor from "../interfaces/sensors/MotionSensor.vue";
|
|
import BinarySensor from "../interfaces/sensors/BinarySensor.vue";
|
|
import AudioSensor from "../interfaces/sensors/AudioSensor.vue";
|
|
import OccupancySensor from "../interfaces/sensors/OccupancySensor.vue";
|
|
import Settings from "../interfaces/Settings.vue";
|
|
import StartStop from "../interfaces/StartStop.vue";
|
|
import Dock from "../interfaces/Dock.vue";
|
|
import Pause from "../interfaces/Pause.vue";
|
|
import Program from "../interfaces/Program.vue";
|
|
import ColorSettingTemperature from "../interfaces/ColorSettingTemperature.vue";
|
|
import Entry from "../interfaces/Entry.vue";
|
|
import HttpRequestHandler from "../interfaces/HttpRequestHandler.vue";
|
|
import PasswordStore from "../interfaces/PasswordStore.vue";
|
|
import Scene from "../interfaces/Scene.vue";
|
|
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
|
|
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
|
|
import DeviceProvider from "../interfaces/DeviceProvider.vue";
|
|
import MixinProvider from "../interfaces/MixinProvider.vue";
|
|
import Readme from "../interfaces/Readme.vue";
|
|
import Scriptable from "../interfaces/automation/Scriptable.vue";
|
|
import Storage from "../common/Storage.vue";
|
|
import { checkUpdate } from "./plugin/plugin";
|
|
import Automation from "./automation/Automation.vue";
|
|
import PluginAdvancedUpdate from "./plugin/PluginAdvancedUpdate.vue";
|
|
import Vue from "vue";
|
|
import CardTitle from "../components/CardTitle.vue";
|
|
import colors from "vuetify/es5/util/colors";
|
|
import AvailableMixins from "./AvailableMixins.vue";
|
|
import Mixin from "./Mixin.vue";
|
|
|
|
const cardHeaderInterfaces = [
|
|
ScryptedInterface.OccupancySensor,
|
|
ScryptedInterface.EntrySensor,
|
|
ScryptedInterface.MotionSensor,
|
|
ScryptedInterface.BinarySensor,
|
|
ScryptedInterface.AudioSensor,
|
|
ScryptedInterface.HumiditySensor,
|
|
ScryptedInterface.Thermometer,
|
|
ScryptedInterface.Battery,
|
|
ScryptedInterface.Lock,
|
|
ScryptedInterface.OnOff,
|
|
];
|
|
|
|
const rightInterfaces = [
|
|
ScryptedInterface.Brightness,
|
|
ScryptedInterface.ColorSettingTemperature,
|
|
ScryptedInterface.RTCSignalingClient,
|
|
ScryptedInterface.Notifier,
|
|
ScryptedInterface.ColorSettingHsv,
|
|
ScryptedInterface.ColorSettingRgb,
|
|
ScryptedInterface.VideoClips,
|
|
ScryptedInterface.TemperatureSetting,
|
|
ScryptedInterface.PasswordStore,
|
|
ScryptedInterface.PositionSensor,
|
|
ScryptedInterface.Program,
|
|
ScryptedInterface.Settings,
|
|
ScryptedInterface.MixinProvider,
|
|
];
|
|
|
|
const leftInterfaces = [
|
|
ScryptedInterface.DeviceProvider,
|
|
ScryptedInterface.Readme,
|
|
];
|
|
const leftAboveInterfaces = [ScryptedInterface.Camera];
|
|
|
|
const noCardInterfaces = [
|
|
ScryptedInterface.Camera,
|
|
ScryptedInterface.Settings,
|
|
ScryptedInterface.Scriptable,
|
|
];
|
|
|
|
const aboveInterfaces = [ScryptedInterface.Scriptable];
|
|
|
|
const cardActionInterfaces = [
|
|
ScryptedInterface.OauthClient,
|
|
ScryptedInterface.HttpRequestHandler,
|
|
];
|
|
|
|
const cardButtonInterfaces = [
|
|
ScryptedInterface.Dock,
|
|
ScryptedInterface.Pause,
|
|
ScryptedInterface.StartStop,
|
|
ScryptedInterface.Entry,
|
|
ScryptedInterface.Scene,
|
|
];
|
|
|
|
function filterInterfaces(interfaces) {
|
|
return function () {
|
|
if (!this.device) {
|
|
return [];
|
|
}
|
|
let ret = interfaces.filter((iface) =>
|
|
this.$store.state.systemState[this.id].interfaces.value.includes(iface)
|
|
);
|
|
|
|
if (this.pluginData?.nativeId?.startsWith("script:")) {
|
|
ret = ret.filter((iface) => iface !== ScryptedInterface.Program);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
export default {
|
|
components: {
|
|
CardTitle,
|
|
AvailableMixins,
|
|
|
|
DeviceProvider,
|
|
MixinProvider,
|
|
|
|
StartStop,
|
|
Dock,
|
|
Pause,
|
|
Entry,
|
|
Scene,
|
|
|
|
Brightness,
|
|
ColorSettingRgb,
|
|
ColorSettingHsv,
|
|
RTCSignalingClient,
|
|
Notifier,
|
|
Camera,
|
|
VideoClips,
|
|
PasswordStore,
|
|
Settings,
|
|
ColorSettingTemperature,
|
|
TemperatureSetting,
|
|
PositionSensor,
|
|
|
|
Lock,
|
|
OnOff,
|
|
Battery,
|
|
Thermometer,
|
|
HumiditySensor,
|
|
EntrySensor,
|
|
MotionSensor,
|
|
BinarySensor,
|
|
AudioSensor,
|
|
OccupancySensor,
|
|
|
|
OauthClient,
|
|
HttpRequestHandler,
|
|
|
|
PluginAdvancedUpdate,
|
|
VueSlider,
|
|
LogCard,
|
|
ConsoleCard,
|
|
REPLCard,
|
|
Readme,
|
|
|
|
Storage,
|
|
|
|
Automation,
|
|
Program,
|
|
Scriptable,
|
|
},
|
|
mixins: [Mixin],
|
|
data() {
|
|
return this.initialState();
|
|
},
|
|
mounted() {
|
|
if (this.needsLoad) {
|
|
this.reload();
|
|
}
|
|
this.device?.listen(undefined, (eventSource, eventDetails, eventData) => {
|
|
if (eventDetails.eventInterface === "Storage") this.reloadStorage();
|
|
});
|
|
},
|
|
destroyed() {
|
|
this.cleanupListener();
|
|
},
|
|
watch: {
|
|
devices() {
|
|
// console.log('device change detected.');
|
|
},
|
|
id() {
|
|
Object.assign(this.$data, this.initialState());
|
|
},
|
|
needsLoad() {
|
|
if (this.needsLoad) {
|
|
this.reload();
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
deviceIsEditable,
|
|
getInterfaceFriendlyName,
|
|
hasFixedPhysicalLocation,
|
|
getComponentWebPath,
|
|
removeAlert,
|
|
getAlertIcon,
|
|
initialState() {
|
|
return {
|
|
colors,
|
|
showLogs: false,
|
|
showConsole: false,
|
|
showRepl: false,
|
|
showDelete: false,
|
|
pluginData: undefined,
|
|
loading: false,
|
|
deviceComponent: undefined,
|
|
deviceData: undefined,
|
|
showStorage: false,
|
|
};
|
|
},
|
|
escapeHtml(html) {
|
|
return html
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
},
|
|
async openConsole() {
|
|
this.showConsole = !this.showConsole;
|
|
if (this.showConsole) {
|
|
await Vue.nextTick();
|
|
this.$vuetify.goTo(this.$refs.consoleEl);
|
|
}
|
|
},
|
|
openManagementUrl() {
|
|
window.location = this.device.info.managementUrl;
|
|
},
|
|
async openRepl() {
|
|
this.showRepl = !this.showRepl;
|
|
if (this.showRepl) {
|
|
await Vue.nextTick();
|
|
this.$vuetify.goTo(this.$refs.replEl);
|
|
}
|
|
},
|
|
async openLogs() {
|
|
this.showLogs = !this.showLogs;
|
|
if (this.showLogs) {
|
|
await Vue.nextTick();
|
|
this.$vuetify.goTo(this.$refs.logsEl);
|
|
}
|
|
},
|
|
onChange() {
|
|
// console.log(JSON.stringify(this.device));
|
|
},
|
|
cleanupListener() {
|
|
if (this.listener) {
|
|
this.listener.removeListener();
|
|
return;
|
|
}
|
|
},
|
|
getMetadata(prop) {
|
|
const metadata = this.$store.state.systemState[this.id].metadata;
|
|
return metadata && metadata.value && metadata.value[prop];
|
|
},
|
|
async reloadPlugin() {
|
|
const plugins = await this.$scrypted.systemManager.getComponent(
|
|
"plugins"
|
|
);
|
|
await plugins.reload(this.pluginData.packageJson.name);
|
|
},
|
|
async reloadStorage() {
|
|
if (!this.pluginData) return;
|
|
|
|
const plugins = await this.$scrypted.systemManager.getComponent(
|
|
"plugins"
|
|
);
|
|
this.pluginData.storage = await plugins.getStorage(this.id);
|
|
},
|
|
async reload() {
|
|
this.loading = true;
|
|
const plugins = await this.$scrypted.systemManager.getComponent(
|
|
"plugins"
|
|
);
|
|
const pluginData = {
|
|
updateAvailable: false,
|
|
versions: null,
|
|
};
|
|
const device = this.device;
|
|
pluginData.nativeId = await plugins.getNativeId(this.id);
|
|
pluginData.storage = await plugins.getStorage(this.id);
|
|
pluginData.pluginId = this.device.pluginId;
|
|
pluginData.packageJson = await plugins.getPackageJson(this.device.pluginId);
|
|
this.pluginData = pluginData;
|
|
checkUpdate(this.device.pluginId, pluginData.packageJson.version).then(
|
|
(result) => Object.assign(pluginData, result)
|
|
);
|
|
|
|
if (this.device.pluginId === "@scrypted/core") {
|
|
const storage = await plugins.getStorage(device.id);
|
|
this.deviceData = storage["data"];
|
|
if (pluginData.nativeId?.startsWith("automation:")) {
|
|
this.deviceComponent = "Automation";
|
|
} else if (pluginData.nativeId?.startsWith("script:")) {
|
|
this.deviceComponent = "Script";
|
|
this.showConsole = true;
|
|
}
|
|
}
|
|
|
|
this.loading = false;
|
|
},
|
|
remove() {
|
|
const id = this.id;
|
|
this.$router.back();
|
|
this.$scrypted.systemManager.removeDevice(id);
|
|
},
|
|
async saveStorage() {
|
|
const plugins = await this.$scrypted.systemManager.getComponent(
|
|
"plugins"
|
|
);
|
|
if (this.deviceData) {
|
|
this.pluginData.storage.data = this.deviceData;
|
|
}
|
|
await plugins.setStorage(this.device.id, this.pluginData.storage);
|
|
},
|
|
openDevice(id) {
|
|
this.$router.push(getDeviceViewPath(id));
|
|
},
|
|
},
|
|
computed: {
|
|
ScryptedInterface() {
|
|
return ScryptedInterface;
|
|
},
|
|
origin() {
|
|
return window.location.origin;
|
|
},
|
|
ownerDevice() {
|
|
if (this.device.providerId === this.device.id) return;
|
|
return this.$scrypted.systemManager.getDeviceById(this.device.providerId);
|
|
},
|
|
deviceState() {
|
|
var ret = {};
|
|
Object.entries(this.$store.state.systemState[this.id]).forEach(
|
|
([key, property]) => (ret[key] = property.value)
|
|
);
|
|
return ret;
|
|
},
|
|
cardButtonInterfaces: filterInterfaces(cardButtonInterfaces),
|
|
cardActionInterfaces: filterInterfaces(cardActionInterfaces),
|
|
leftInterfaces: filterInterfaces(leftInterfaces),
|
|
leftAboveInterfaces: filterInterfaces(leftAboveInterfaces),
|
|
noCardInterfaces: filterInterfaces(noCardInterfaces),
|
|
rightInterfaces: filterInterfaces(rightInterfaces),
|
|
cardHeaderInterfaces: filterInterfaces(cardHeaderInterfaces),
|
|
aboveInterfaces: filterInterfaces(aboveInterfaces),
|
|
deviceAlerts() {
|
|
return this.$store.state.scrypted.alerts.filter((alert) =>
|
|
alert.path.startsWith(getDeviceViewPath(this.id))
|
|
);
|
|
},
|
|
devices() {
|
|
return this.$store.state.scrypted.devices;
|
|
},
|
|
id() {
|
|
return this.$route.params.id;
|
|
},
|
|
canLoad() {
|
|
return this.devices.includes(this.id);
|
|
},
|
|
needsLoad() {
|
|
return !this.pluginData && this.canLoad && !this.loading;
|
|
},
|
|
device() {
|
|
return this.$scrypted.systemManager.getDeviceById(this.id);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<style>
|
|
a.alert-link {
|
|
color: white;
|
|
}
|
|
</style>
|
|
</script>
|
|
<style scoped>
|
|
.shift-up {
|
|
margin-top: -8px;
|
|
}
|
|
</style> |