@openhab-ui/setup-and-maintenance
Version:
Configuration and maintenance interface for openHAB
333 lines (316 loc) • 12.7 kB
JavaScript
import { store, fetchMethodWithTimeout, createNotification, MultiRestError } from '../app.js';
import { generateTemplateForSchema, Yaml } from '../uicomponents.js';
let schema = {
uri: 'http://openhab.org/schema/things-schema.json',
fileMatch: ["http://openhab.org/schema/things-schema.json"],
schema: {
type: 'array',
items: { "$ref": "#/definitions/item" },
definitions: {
item: {
type: "object",
description: "openHAB thing",
required: ["UID", "thingTypeUID", "label"],
properties: {
UID: { type: "string", description: "A unique ID for this thing", minLength: 2 },
label: { type: "string", description: "A friendly name", minLength: 2 },
tags: {
type: "array", "uniqueItems": true, description: "Tags of this thing",
items: {
"type": "string"
}
},
thingTypeUID: { type: "string", description: "The type of this thing. Provided by a binding.", enum: [] },
channels: {
type: "array", uniqueItems: true, description: "Thing channels",
items: {
type: "object",
properties: {
required: ["uid", "id", "channelTypeUID"],
uid: { type: "string", description: "A unique ID for this channel", minLength: 2 },
id: { type: "string", description: "The id part of the unique ID. Must match with UID", minLength: 2 },
channelTypeUID: { type: "string", description: "The channel type" },
linkedItems: { type: "array", uniqueItems: true, description: "The items that are linked to this channel" },
configuration: {
type: "object",
description: "A channel might have optional or required configuration",
properties: {}
},
properties: {
type: "object",
description: "Properties are runtime informations",
properties: {}
},
defaultTags: {
type: "array", "uniqueItems": true, description: "Tags that will be copied to a linked Item",
items: {
"type": "string"
}
},
label: { type: "string", description: "A friendly name", minLength: 2 },
kind: { type: "string", description: "A channel can be a state channel or a trigger channel", enum: ["STATE", "TRIGGER"] },
itemType: {
type: "string", description: "The item type for this channel",
enum: ['String', 'Number', 'Switch', 'Color', 'Contact', 'DateTime', 'Dimmer', 'Image', 'Location', 'Player',
'Rollershutter', 'Group']
},
}
}
},
configuration: {
type: "object",
description: "A thing might have optional or required configuration",
properties: {}
},
}
}
}
},
};
class ModelAdapter {
constructor() {
this.STORE_ITEM_INDEX_PROP = Object.freeze("UID");
this.runtimeKeys = Object.freeze(["link", "editable", "statusInfo", "properties", "actions"]);
this.items = []; this.thingtypes = [];
}
stores() { return { "things": "items" } };
sortStore() { return "things" };
getall(options = null) {
return store.get("thing-types", null, { force: true })
.then(v => this.thingtypes = v)
.then(() => this.get(null, null, options))
}
async get(table = null, objectid = null, options = null) {
this.items = await store.get("things", null, options);
this.adaptSchema();
}
getThingTypeFor(uid) {
for (const thingType of this.thingtypes) {
if (thingType.UID == uid)
return thingType;
}
return null;
}
getExtendedThingTypeFor(uid) {
return store.get("thing-types-extended", uid, { force: true });
}
getThingTypeConfigDescriptionsFor(uid) {
return store.get("config-descriptions", "thing-type:" + uid, { force: true });
}
getChannelTypesConfigDescriptionsFor(bindingID) {
return store.get("config-descriptions", null, { filter: "uri:channel-type:" + bindingID, force: true });
}
getChannelTypes(bindingID) {
return store.get("channel-types", null, { filter: "UID:" + bindingID + ":", force: true });
}
dispose() {
}
adaptSchema() {
schema.schema.definitions.item.properties.thingTypeUID.enum = this.thingtypes.map(i => i.UID);
}
}
const ThingsMixin = {
data: () => {
return {
actions: []
}
},
watch: {
listitem: {
handler: function (newVal, oldVal) {
// Create actions array
this.actions = [];
if (newVal.statusInfo.statusDetail === "DISABLED")
this.actions.push({ id: "enable", label: "Enable", description: "Enable this Thing" });
else
this.actions.push({ id: "disable", label: "Disable", description: "Disable this Thing. A disabled thing will not connect to its peer device" });
if (!this.item.actions) return null;
for (let action of this.item.actions) this.actions.push(action);
}, deep: false, immediate: true,
}
},
computed: {
statusInfo() {
return this.item.statusInfo ? this.item.statusInfo.status.toLowerCase().replace(/^\w/, c => c.toUpperCase()) : "Unknown";
},
statusDetails() {
return this.item.statusInfo ? this.item.statusInfo.statusDetail + " - " + this.item.statusInfo.description : "";
},
statusBadge() {
const status = this.item.statusInfo ? this.item.statusInfo.status : "";
switch (status) {
case "ONLINE": return "badge badge-success";
case "OFFLINE": return "badge badge-danger";
case "UNINITIALIZED": return "badge badge-info";
}
return "badge badge-light";
}
},
methods: {
commontags() {
return [];
},
description() {
const thingType = this.$list.store.getThingTypeFor(this.item.thingTypeUID);
if (thingType) return thingType.description;
return "No Thing description available";
},
triggerAction(actionEvent) {
if (actionEvent.detail == "enable") {
this.setEnabled(actionEvent, true);
} else if (actionEvent.detail == "disable") {
this.setEnabled(actionEvent, false);
} else {
console.log("triggered", actionEvent.target.dataset.uid, actionEvent.detail);
this.message = null;
this.messagetitle = "Performing action...";
this.inProgress = true;
this.changed = false;
setTimeout(() => {
this.inProgress = false;
}, 500);
}
},
haschannels() {
return this.item.channels.length > 0;
},
remove() {
this.message = null;
this.messagetitle = "Removing...";
this.inProgress = true;
const forceQuery = this.item.statusInfo.status == "REMOVING" ? "?force=true" : "";
fetchMethodWithTimeout(store.host + "/rest/things/" + this.item.UID + forceQuery, "DELETE", null)
.then(r => {
this.message = "Thing '" + this.item.label + "' removed";
this.inProgress = false;
}).catch(e => {
this.message = e.toString();
});
},
setEnabled(event, value) {
event.target.classList.add("disabled");
fetchMethodWithTimeout(store.host + "/rest/things/" + this.item.UID + "/enable", "PUT", value)
.then(r => {
event.target.classList.remove("disabled");
createNotification(null, `Thing ${this.item.label} is now ${value ? "enabled" : "disabled"}`, false, 2000);
}).catch(e => {
event.target.classList.remove("disabled");
createNotification(null, `Failed to en/disable ${this.item.label}: ${e}`, false, 4000);
});
},
}
};
function symbolChainEqual(chain1, chain2) {
if (chain1.length != chain2.length) return false;
for (let i = 0; i < chain1.length; ++i) {
if (chain1[i] != chain2[i]) return false;
}
return true;
}
const ItemListMixin = {
mounted() {
this.modelschema = schema; // Don't freeze: The schema is adapted dynamically
},
methods: {
async saveAll(updated, created, removed) {
let errorItems = [];
for (let item of updated) {
await fetchMethodWithTimeout(store.host + "/rest/things/" + item.UID, "PUT", JSON.stringify(item))
.catch(e => {
errorItems.push(item.name + ":" + e.toString());
});
}
for (let item of created) {
await fetchMethodWithTimeout(store.host + "/rest/things", "POST", JSON.stringify(item))
.catch(e => {
errorItems.push(item.name + ":" + e.toString());
});
}
for (let item of removed) {
await fetchMethodWithTimeout(store.host + "/rest/things/" + item.UID, "DELETE")
.catch(e => {
errorItems.push(item.name + ":" + e.toString());
});
}
if (errorItems.length) {
throw new MultiRestError("Some objects failed", errorItems);
} else {
createNotification(null, `Updated ${updated.length}, Created ${created.length}, Removed ${removed.length} objects`, false, 1500);
}
},
async editorCompletion(symbols, trigger) {
const context = this.last.context;
if (context && (symbols.length == 2 || symbols.length == 4) && symbols[1] == "channels") {
let channels = generateTemplateForSchema(context.schema,
context.thingType, context.channelConfigTypes, context.channelTypes,
context.focus, context.focusChannelindex, true);
let suggestions = [];
for (let channel of channels) {
let content = Yaml.dump([channel], 10, 4).replace(/- /g, "-\n ");
if (trigger) content = content.replace("-", "");
suggestions.push({
label: 'New ' + channel.label + " channel",
documentation: 'Creates a new object template',
insertText: content,
});
}
return suggestions;
}
return [];
},
// Editor symbol selected
async symbolSelected(event) {
const symbolChain = event.detail;
if (this.last && symbolChainEqual(this.last.symbol, symbolChain)) return;
if (!this.last) this.last = {};
this.last.symbol = symbolChain;
const thingTypeUID = symbolChain[0];
if (thingTypeUID && thingTypeUID.includes(" ")) return; // Whitespace? Early exit
let context;
if (this.last.thingTypeUID != thingTypeUID) {
this.last.thingTypeUID = thingTypeUID;
const bindingID = thingTypeUID.split(":")[0];
const thingType = await this.$list.store.getExtendedThingTypeFor(thingTypeUID);
// const configurationType = await this.$list.store.getThingTypeConfigDescriptionsFor(thingTypeUID);
const channelConfigTypes = await this.$list.store.getChannelTypesConfigDescriptionsFor(bindingID) || [];
const channelTypes = await this.$list.store.getChannelTypes(bindingID) || [];
let thingSchema = JSON.parse(JSON.stringify(schema.schema.definitions.item.properties));
thingSchema.thingTypeUID.enum = [];
context = { thingType, channelConfigTypes, channelTypes, schema: thingSchema }; // configurationType
this.last.context = context;
} else {
context = this.last.context;
}
if (!context.thingType) return;
if (symbolChain.length > 1 && symbolChain[1] == "configuration") { // thing config
context.focus = "thingconfig";
} else if (symbolChain.length > 1 && symbolChain[1] == "channels") {
if (symbolChain.length > 3 && symbolChain[3] == "configuration") { // channel config
context.focus = "channelconfig";
context.focusChannelindex = symbolChain[2];
} else {
context.focus = "channels";
}
} else {
context.focus = null;
}
if (this.ohcontexthelp === undefined) {
this.ohcontexthelp = document.querySelector("ui-context-help");
if (!this.ohcontexthelp) {
console.warn("Did not find context help element");
return;
}
}
this.ohcontexthelp.contenturl = null;
this.ohcontexthelp.contextdata = context;
this.ohcontexthelp.contenturl = "thing_type.fragment.html";
if (!this.showcontext) {
this.showcontext = true;
document.querySelector('body').classList.add('showcontext');
}
}
}
};
const mixins = [ThingsMixin];
const listmixins = [ItemListMixin];
export { mixins, listmixins, ModelAdapter };