gateway-addon
Version:
Bindings for WebThings Gateway add-ons
380 lines (312 loc) • 9.29 kB
text/typescript
/**
* Device Model.
*
* Abstract base class for devices managed by an adapter.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Action } from './action';
import Ajv from 'ajv';
import { Adapter } from './adapter';
import { Property } from './property';
import { Event } from './event';
import {
Action as ActionSchema,
Any,
Event as EventSchema,
Device as DeviceSchema,
Property as PropertySchema,
Link,
} from './schema';
const ajv = new Ajv();
export class Device {
private adapter: Adapter;
private id: string;
private '@context' = 'https://webthings.io/schemas';
private '@type': string[] = [];
private name = '';
private title = '';
private description = '';
private properties = new Map<string, Property<Any>>();
private actions = new Map<string, ActionSchema>();
private events = new Map<string, EventSchema>();
private links: Link[] = [];
private baseHref?: string;
private pinRequired = false;
private pinPattern?: string;
private credentialsRequired = false;
constructor(adapter: Adapter, id: string) {
this.adapter = adapter;
this.id = `${id}`;
}
getId(): string {
return this.id;
}
getContext(): string {
return this['@context'];
}
setContext(context: string): void {
this['@context'] = context;
}
getTypes(): string[] {
return this['@type'];
}
setTypes(type: string[]): void {
this['@type'] = type;
}
addType(type: string): void {
if (!(type in this['@type'])) {
this['@type'].push(type);
}
}
getTitle(): string {
if (this.name && !this.title) {
this.title = this.name;
}
return this.title;
}
setTitle(title: string): void {
this.title = title;
}
getDescription(): string {
return this.description;
}
setDescription(description: string): void {
this.description = description;
}
getLinks(): Link[] {
return this.links;
}
setLinks(value: Link[]): void {
this.links = value;
}
addLink(link: Link): void {
this.links.push(link);
}
getBaseHref(): string | undefined {
return this.baseHref;
}
setBaseHref(baseHref: string): void {
this.baseHref = baseHref;
}
getPinRequired(): boolean {
return this.pinRequired;
}
setPinRequired(pinRequired: boolean): void {
this.pinRequired = pinRequired;
}
getPinPattern(): string | undefined {
return this.pinPattern;
}
setPinPattern(pinPattern: string): void {
this.pinPattern = pinPattern;
}
getCredentialsRequired(): boolean {
return this.credentialsRequired;
}
setCredentialsRequired(credentialsRequired: boolean): void {
this.credentialsRequired = credentialsRequired;
}
mapToDict<V>(map: Map<string, V>): Record<string, V> {
const dict: Record<string, V> = {};
map.forEach((property, propertyName) => {
dict[propertyName] = Object.assign({}, property);
});
return dict;
}
mapToDictFromFunction<V>(map: Map<string, { asDict: () => V }>): Record<string, V> {
const dict: Record<string, V> = {};
map.forEach((property, propertyName) => {
dict[propertyName] = property.asDict();
});
return dict;
}
asDict(): DeviceSchema {
return {
id: this.id,
title: this.title || this.name,
'@context': this['@context'],
'@type': this['@type'],
description: this.description,
properties: this.mapToDictFromFunction(this.properties),
actions: this.mapToDict(this.actions),
events: this.mapToDict(this.events),
links: this.links,
baseHref: this.baseHref,
pin: {
required: this.pinRequired,
pattern: this.pinPattern,
},
credentialsRequired: this.credentialsRequired,
};
}
/**
* @returns this object as a thing
*/
asThing(): DeviceSchema {
return {
id: this.id,
title: this.title || this.name,
'@context': this['@context'],
'@type': this['@type'],
description: this.description,
properties: this.mapToDictFromFunction(this.properties),
actions: this.mapToDict(this.actions),
events: this.mapToDict(this.events),
links: this.links,
baseHref: this.baseHref,
pin: {
required: this.pinRequired,
pattern: this.pinPattern,
},
credentialsRequired: this.credentialsRequired,
};
}
getPropertyDescriptions(): Record<string, unknown> {
const propDescs: Record<string, PropertySchema> = {};
this.properties.forEach((property, propertyName) => {
propDescs[propertyName] = property.asPropertyDescription();
});
return propDescs;
}
findProperty(propertyName: string): Property<Any> | undefined {
return this.properties.get(propertyName);
}
addProperty(property: Property<Any>): void {
this.properties.set(property.getName(), property);
}
/**
* @method getProperty
* @returns a promise which resolves to the retrieved value.
*/
getProperty(propertyName: string): Promise<Any> {
return new Promise((resolve, reject) => {
const property = this.findProperty(propertyName);
if (property) {
property.getValue().then((value) => {
resolve(value);
});
} else {
reject(`Property "${propertyName}" not found`);
}
});
}
hasProperty(propertyName: string): boolean {
return this.properties.has(propertyName);
}
notifyPropertyChanged(property: Property<Any>): void {
this.adapter.getManager().sendPropertyChangedNotification(property);
}
actionNotify(action: Action): void {
this.adapter.getManager().sendActionStatusNotification(action);
}
eventNotify(event: Event): void {
this.adapter.getManager().sendEventNotification(event);
}
connectedNotify(connected: boolean): void {
this.adapter.getManager().sendConnectedNotification(this, connected);
}
/**
* @method setProperty
* @returns a promise which resolves to the updated value.
*
* @note it is possible that the updated value doesn't match
* the value passed in.
*/
setProperty(propertyName: string, value: Any): Promise<Any> {
const property = this.findProperty(propertyName);
if (property) {
return property.setValue(value);
}
return Promise.reject(`Property "${propertyName}" not found`);
}
getAdapter(): Adapter {
return this.adapter;
}
/**
* @method requestAction
* @returns a promise which resolves when the action has been requested.
*/
requestAction(actionId: string, actionName: string, input: Any): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.actions.has(actionName)) {
reject(`Action "${actionName}" not found`);
return;
}
// Validate action input, if present.
const metadata = this.actions.get(actionName);
if (metadata) {
if (metadata.hasOwnProperty('input')) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const valid = ajv.validate(<any>metadata.input, input);
if (!valid) {
reject(`Action "${actionName}": input "${input}" is invalid`);
}
}
} else {
reject(`Action "${actionName}" not found`);
}
const action = new Action(actionId, this, actionName, input);
this.performAction(action).catch((err) => console.log(err));
resolve();
});
}
/**
* @method removeAction
* @returns a promise which resolves when the action has been removed.
*/
removeAction(actionId: string, actionName: string): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.actions.has(actionName)) {
reject(`Action "${actionName}" not found`);
return;
}
this.cancelAction(actionId, actionName).catch((err) => console.log(err));
resolve();
});
}
/**
* @method performAction
*/
performAction(_action: Action): Promise<void> {
return Promise.resolve();
}
/**
* @method cancelAction
*/
cancelAction(_actionId: string, _actionName: string): Promise<void> {
return Promise.resolve();
}
/**
* Add an action.
*
* @param {String} name Name of the action
* @param {Object} metadata Action metadata, i.e. type, description, etc., as
* an object
*/
addAction(name: string, metadata?: ActionSchema): void {
metadata = metadata ?? {};
if (metadata.hasOwnProperty('href')) {
const metadataWithHref = <{ href?: string }>(<unknown>metadata);
delete metadataWithHref.href;
}
this.actions.set(name, metadata);
}
/**
* Add an event.
*
* @param {String} name Name of the event
* @param {Object} metadata Event metadata, i.e. type, description, etc., as
* an object
*/
addEvent(name: string, metadata?: EventSchema): void {
metadata = metadata ?? {};
if (metadata.hasOwnProperty('href')) {
const metadataWithHref = <{ href?: string }>(<unknown>metadata);
delete metadataWithHref.href;
}
this.events.set(name, metadata);
}
}