UNPKG

@pajn/node-tradfri-client

Version:

Library to talk to IKEA Trådfri Gateways without external binaries

504 lines (503 loc) 21.3 kB
"use strict"; /// <reference types="reflect-metadata" /> var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IPSOObject = exports.doNotSerialize = exports.deserializeWith = exports.serializeWith = exports.required = exports.ipsoKey = void 0; const objects_1 = require("alcalzone-shared/objects"); const logger_1 = require("./logger"); // =========================================================== // define decorators so we can define all properties type-safe // tslint:disable:variable-name const METADATA_ipsoKey = Symbol("ipsoKey"); const METADATA_required = Symbol("required"); const METADATA_serializeWith = Symbol("serializeWith"); const METADATA_deserializeWith = Symbol("deserializeWith"); const METADATA_doNotSerialize = Symbol("doNotSerialize"); /** * Builds a property transform from a kernel and some options * @param kernel The transform kernel * @param options Some options regarding the property transform */ function buildPropertyTransform(kernel, options = {}) { if (options.splitArrays == null) options.splitArrays = true; if (options.neverSkip == null) options.neverSkip = false; const ret = kernel; ret.splitArrays = options.splitArrays; ret.neverSkip = options.neverSkip; return ret; } /** * Defines the ipso key neccessary to serialize a property to a CoAP object */ const ipsoKey = (key) => { return (target, property) => { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_ipsoKey, constr) || {}; // and enhance it (both ways) metadata[property] = key; metadata[key] = property; // store back to the object Reflect.defineMetadata(METADATA_ipsoKey, metadata, constr); }; }; exports.ipsoKey = ipsoKey; /** * Looks up previously stored property ipso key definitions. * Returns a property name if the key was given, or the key if a property name was given. * @param keyOrProperty - ipso key or property name to lookup */ function lookupKeyOrProperty(target, keyOrProperty /*| keyof T*/) { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_ipsoKey, constr); return metadata && metadata[keyOrProperty]; } /** * Declares that a property is required to be present in a serialized CoAP object */ const required = (predicate = true) => { return (target, property) => { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_required, constr) || {}; // and enhance it (both ways) metadata[property] = predicate; // store back to the object Reflect.defineMetadata(METADATA_required, metadata, constr); }; }; exports.required = required; /** * Checks if a property is required to be present in a serialized CoAP object * @param property - property name to lookup */ function isRequired(target, reference, property) { // get the class constructor const constr = target.constructor; logger_1.log(`${constr.name}: checking if ${property} is required...`, "silly"); // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_required, constr) || {}; if (metadata.hasOwnProperty(property)) { const ret = metadata[property]; if (typeof ret === "boolean") return ret; return ret(target, reference); } return false; } /** * Checks if a property is required to be present in a serialized CoAP object. * In contrast to `isRequired`, this leaves out properties that depend on others. * @param property - property name to lookup */ function isAlwaysRequired(target, property) { // get the class constructor const constr = target.constructor; logger_1.log(`${constr.name}: checking if ${property} is always required...`, "silly"); // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_required, constr) || {}; if (metadata.hasOwnProperty(property)) { const ret = metadata[property]; if (typeof ret === "boolean") return ret; } return false; } /** * Defines the required transformations to serialize a property to a CoAP object * @param kernel The transformation to apply during serialization * @param options Some options regarding the behavior of the property transform */ function serializeWith(kernel, options) { const transform = buildPropertyTransform(kernel, options); return (target, property) => { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_serializeWith, constr) || {}; metadata[property] = transform; // store back to the object Reflect.defineMetadata(METADATA_serializeWith, metadata, constr); }; } exports.serializeWith = serializeWith; // default serializers should not be skipped const defaultSerializers = { Boolean: buildPropertyTransform((bool) => bool ? 1 : 0, { neverSkip: true }), }; /** * Retrieves the serializer for a given property */ function getSerializer(target, property /* | keyof T*/) { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_serializeWith, constr) || {}; if (metadata.hasOwnProperty(property)) return metadata[property]; // If there's no custom serializer, try to find a default one const type = getPropertyType(target, property); if (type && type.name in defaultSerializers) { return defaultSerializers[type.name]; } } /** * Defines the required transformations to deserialize a property from a CoAP object * @param kernel The transformation to apply during deserialization * @param options Options for deserialisation */ function deserializeWith(kernel, options) { const transform = buildPropertyTransform(kernel, options); return (target, property) => { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_deserializeWith, constr) || {}; metadata[property] = transform; // store back to the object Reflect.defineMetadata(METADATA_deserializeWith, metadata, constr); }; } exports.deserializeWith = deserializeWith; /** * Defines that a property will not be serialized */ const doNotSerialize = (target, property) => { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_doNotSerialize, constr) || {}; metadata[property] = true; // store back to the object Reflect.defineMetadata(METADATA_doNotSerialize, metadata, constr); }; exports.doNotSerialize = doNotSerialize; /** * Checks if a given property will be serialized or not */ function isSerializable(target, property) { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_doNotSerialize, constr) || {}; // if doNotSerialize is defined, don't serialize! return !metadata.hasOwnProperty(property); } // default deserializers should not be skipped const defaultDeserializers = { Boolean: buildPropertyTransform((raw) => raw === 1 || raw === "true" || raw === "on" || raw === true, { neverSkip: true }), }; /** * Retrieves the deserializer for a given property */ function getDeserializer(target, property /*| keyof T*/) { // get the class constructor const constr = target.constructor; // retrieve the current metadata const metadata = Reflect.getMetadata(METADATA_deserializeWith, constr) || {}; if (metadata.hasOwnProperty(property)) { return metadata[property]; } // If there's no custom deserializer, try to find a default one const type = getPropertyType(target, property); if (type && type.name in defaultDeserializers) { return defaultDeserializers[type.name]; } } /** * Finds the design type for a given property */ // tslint:disable-next-line:ban-types function getPropertyType(target, property /* | keyof T*/) { return Reflect.getMetadata("design:type", target, property); } // common base class for all objects that are transmitted somehow class IPSOObject { constructor(options = {}) { /** If this object was proxied or not */ this.isProxy = false; this.options = options; } // // provide an index signature so TypeScript shuts up when using --noImplicitAny // [propName: string]: any; /** * Reads this instance's properties from the given object */ parse(obj) { for (const [key, value] of objects_1.entries(obj)) { let deserializer = getDeserializer(this, key); // key might be ipso key or property name let propName; // keyof this | string; if (deserializer == null) { // deserializers are defined by property name, so key is actually the key propName = lookupKeyOrProperty(this, key); if (!propName) { logger_1.log(`found unknown property with key ${key}`, "warn"); logger_1.log(`object was: ${JSON.stringify(obj)}`, "warn"); continue; } deserializer = getDeserializer(this, propName); } else { // the deserializer was found, so key is actually the property name propName = key; } // parse the value const requiresArraySplitting = deserializer ? deserializer.splitArrays : true; const parsedValue = this.parseValue(key, value, deserializer, requiresArraySplitting); // and remember it - we are now sure propname is a keyof this this[propName] = parsedValue; } return this; } // parses a value, depending on the value type and defined parsers parseValue(propKey /*| symbol*/, value, transform, requiresArraySplitting = true) { if (value instanceof Array && requiresArraySplitting) { // Array: parse every element return value.map(v => this.parseValue(propKey, v, transform, requiresArraySplitting)); } else if (typeof value === "object") { // Object: try to parse this, objects should be parsed in any case if (transform) { return transform(value, this); } else { logger_1.log(`could not find deserializer for key ${propKey}`, "warn"); } } else if (transform && (transform.neverSkip || !this.options.skipValueSerializers)) { return transform(value, this); } else { // otherwise just return the value return value; } } /** * Overrides this object's properties with those from another partial one */ merge(obj, allProperties = false) { for (const [key, value] of objects_1.entries(obj)) { if (allProperties || this.hasOwnProperty(key)) { // we can't be sure that this has a property `key` this[key] = value; } } return this; } /** serializes this object in order to transfer it via COAP */ serialize(reference) { // unproxy objects before serialization if (this.isProxy) return this.unproxy().serialize(reference); if (reference != null && reference instanceof IPSOObject && reference.isProxy) reference = reference.unproxy(); const ret = {}; const serializeValue = (propName, value, refValue, transform) => { const _required = isRequired(this, reference, propName); let _ret = value; if (value instanceof IPSOObject) { // if the value is another IPSOObject, then serialize that _ret = value.serialize(refValue); // if the serialized object contains no required properties, don't remember it if (value.isSerializedObjectEmpty(_ret)) return null; } else { // if the value is not the default one, then remember it if (refValue != null) { if (!_required && refValue === value) return null; } else { // there is no default value, just remember the actual value } } if (transform && (transform.neverSkip || !this.options.skipValueSerializers)) { _ret = transform(_ret, this); } else if (typeof _ret === "number" && this.options.skipValueSerializers) { _ret = Math.round(_ret); } return _ret; }; // check all set properties for (const propName of Object.keys(this)) { // check if this property is going to be serialized if ( // properties starting with "_" are private by convention !propName.startsWith("_") && // non-existent properties aren't going to be serialized this.hasOwnProperty(propName) && // the same goes for properties with @doNotSerialize isSerializable(this, propName)) { // find IPSO key const key = lookupKeyOrProperty(this, propName); // find value and reference (default) value let value = this[propName]; let refValue = null; if (reference != null && reference.hasOwnProperty(propName)) { refValue = reference[propName]; } // try to find serializer for this property const serializer = getSerializer(this, propName); const requiresArraySplitting = serializer ? serializer.splitArrays : true; if (value instanceof Array && requiresArraySplitting) { // serialize each item if (refValue != null) { // reference value exists, make sure we have the same amount of items if (!(refValue instanceof Array && refValue.length === value.length)) { throw new Error("cannot serialize arrays when the reference values don't match"); } // serialize each item with the matching reference value value = value.map((v, i) => serializeValue(propName, v, refValue[i], serializer)); } else { // no reference value, makes things easier value = value.map(v => serializeValue(propName, v, null, serializer)); } // now remove null items value = value.filter(v => v != null); if (value.length === 0) value = null; } else { // directly serialize the value value = serializeValue(propName, value, refValue, serializer); } // only output the value if it's != null // We cannot use !!value here because that would strip out "" and 0 if (!!key && value != null) ret[key] = value; } } return ret; } /** * Deeply clones an IPSO Object */ clone(...constructorArgs) { const constructor = this.constructor; const ret = new constructor(this.options, ...constructorArgs); // serialize the old values const serialized = this.serialize(); // and parse them back return ret.parse(serialized); } isSerializedObjectEmpty(obj) { // Prüfen, ob eine nicht-benötigte Eigenschaft angegeben ist. => nicht leer for (const key of Object.keys(obj)) { const propName = lookupKeyOrProperty(this, key); if (!isAlwaysRequired(this, propName)) { return false; } } return true; } // is overridden inside the proxy /** Returns the raw object without a wrapping proxy */ unproxy() { if (this.isProxy) { return this.underlyingObject; } return this; } /** * Creates a proxy for this device * @param get Custom getter trap (optional). This is called after mandatory traps are in place and before default behavior * @param set Custom setter trap (optional). This is called after mandatory traps are in place and before default behavior */ createProxy(get, set) { // per default create a proxy that proxies all IPSOObject instances (single or array) return new Proxy(this, { get: (me, key) => { // add some metadata if (key === "isProxy") return true; if (key === "underlyingObject") return me; if (key === "unproxy") return me.unproxy; // if defined, call the overloaded getter if (get != null) return get(me, key); // else continue with predefined behaviour // simply return functions if (typeof me[key] === "function") { if (key === "clone") { // clones of proxies should also be proxies return () => me.clone().createProxy(); } else { return me[key]; } } // proxy all IPSOObject-Arrays if (me[key] instanceof Array && me[key].length > 0 && me[key][0] instanceof IPSOObject) { return me[key].map((d) => d.createProxy()); } // proxy all IPSO-Object instances if (me[key] instanceof IPSOObject) return me[key].createProxy(); // return all other properties return me[key]; }, set: (me, key, value, receiver) => { // if defined, call the overloaded setter if (set != null) return set(me, key, value, receiver); // else continue with predefined behaviour // simply set all properties me[key] = value; return true; }, }); } /** * Link this object to a TradfriClient for a simplified API. * @internal * @param client The client instance to link this object to */ link(client) { this.client = client; return this; } /** * Fixes property values that are known to be bugged */ fixBuggedProperties() { // IPSOObject has none return this; } } __decorate([ exports.doNotSerialize, __metadata("design:type", Object) ], IPSOObject.prototype, "options", void 0); __decorate([ exports.doNotSerialize, __metadata("design:type", Boolean) ], IPSOObject.prototype, "isProxy", void 0); __decorate([ exports.doNotSerialize, __metadata("design:type", Object) ], IPSOObject.prototype, "client", void 0); exports.IPSOObject = IPSOObject;