From def1fbc313a3f4a6f297b05342aaa6f1ccfe7cc0 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Sat, 4 Nov 2017 17:56:43 +0100 Subject: [PATCH] Simplified operating lights --- README.md | 1 + build/lib/accessory.d.ts | 7 ++ build/lib/accessory.js | 21 ++++++ build/lib/ipsoObject.d.ts | 8 +++ build/lib/ipsoObject.js | 14 ++++ build/lib/light.d.ts | 40 +++++++++++ build/lib/light.js | 144 ++++++++++++++++++++++++++++++++++++++ build/tradfri-client.js | 6 +- src/lib/accessory.ts | 23 ++++++ src/lib/ipsoObject.ts | 12 ++++ src/lib/light.ts | 123 +++++++++++++++++++++++++++++++- src/tradfri-client.ts | 6 +- 12 files changed, 398 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f5c98fce..f5591204 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Library to talk to IKEA Trådfri Gateways without external binaries #### 0.4.0 (2017-11-04) * (AlCalzone) Reworked the observe api so it resides on TradfriClient now +* (AlCalzone) Simplified operating lights #### 0.3.0 (2017-11-02) * (AlCalzone) Changed authentication procedure to comply with IKEA's request diff --git a/build/lib/accessory.d.ts b/build/lib/accessory.d.ts index e56056d7..34218c82 100644 --- a/build/lib/accessory.d.ts +++ b/build/lib/accessory.d.ts @@ -1,3 +1,4 @@ +import { TradfriClient } from "../tradfri-client"; import { DeviceInfo } from "./deviceInfo"; import { IPSODevice } from "./ipsoDevice"; import { Light } from "./light"; @@ -18,4 +19,10 @@ export declare class Accessory extends IPSODevice { sensorList: Sensor[]; switchList: IPSODevice[]; otaUpdateState: number; + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + link(client: TradfriClient): this; } diff --git a/build/lib/accessory.js b/build/lib/accessory.js index 4ebcfe43..eb25353f 100644 --- a/build/lib/accessory.js +++ b/build/lib/accessory.js @@ -32,6 +32,27 @@ class Accessory extends ipsoDevice_1.IPSODevice { this.lastSeen = 0; this.otaUpdateState = 0; // boolean? } + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + link(client) { + super.link(client); + for (const light of this.lightList) { + light.link(client); + } + for (const plug of this.plugList) { + plug.link(client); + } + for (const sensor of this.sensorList) { + sensor.link(client); + } + for (const swtch of this.switchList) { + swtch.link(client); + } + return this; + } } __decorate([ ipsoObject_1.ipsoKey("5750"), diff --git a/build/lib/ipsoObject.d.ts b/build/lib/ipsoObject.d.ts index 99e9b7a6..a98a594b 100644 --- a/build/lib/ipsoObject.d.ts +++ b/build/lib/ipsoObject.d.ts @@ -1,3 +1,4 @@ +import { TradfriClient } from "../tradfri-client"; import { DictionaryLike } from "./object-polyfill"; export declare type PropertyTransform = (value: any, parent?: IPSOObject) => any; export declare type RequiredPredicate = (me: IPSOObject, reference: IPSOObject) => boolean; @@ -54,4 +55,11 @@ export declare class IPSOObject { * @param set Custom setter trap (optional). This is called after mandatory traps are in place and before default behavior */ createProxy(get?: (me: this, key: PropertyKey) => any, set?: (me: this, key: PropertyKey, value, receiver) => boolean): this; + protected client: TradfriClient; + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + link(client: TradfriClient): this; } diff --git a/build/lib/ipsoObject.js b/build/lib/ipsoObject.js index 76c800fb..58ad2af4 100644 --- a/build/lib/ipsoObject.js +++ b/build/lib/ipsoObject.js @@ -9,6 +9,7 @@ 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 }); +const tradfri_client_1 = require("../tradfri-client"); const logger_1 = require("./logger"); const object_polyfill_1 = require("./object-polyfill"); // =========================================================== @@ -469,9 +470,22 @@ class IPSOObject { }, }); } + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + link(client) { + this.client = client; + return this; + } } __decorate([ exports.doNotSerialize, __metadata("design:type", Boolean) ], IPSOObject.prototype, "isProxy", void 0); +__decorate([ + exports.doNotSerialize, + __metadata("design:type", tradfri_client_1.TradfriClient) +], IPSOObject.prototype, "client", void 0); exports.IPSOObject = IPSOObject; diff --git a/build/lib/light.d.ts b/build/lib/light.d.ts index 0de93cdc..e9555b0d 100644 --- a/build/lib/light.d.ts +++ b/build/lib/light.d.ts @@ -4,6 +4,7 @@ export declare type LightOperation = Partial; + /** Turn this lightbulb off */ + turnOff(): Promise; + /** Toggles this lightbulb on or off */ + toggle(value?: boolean): Promise; + private operateLight(operation, transitionTime?); + /** + * Changes this lightbulb's brightness + * @returns true if a request was sent, false otherwise + */ + setBrightness(value: number, transitionTime?: number): Promise; + /** + * Changes this lightbulb's color + * @param value The target color as a 6-digit hex string + * @returns true if a request was sent, false otherwise + */ + setColor(value: string, transitionTime?: number): Promise; + /** + * Changes this lightbulb's color temperature + * @param value The target color temperature in the range 0% (cold) to 100% (warm) + * @returns true if a request was sent, false otherwise + */ + setColorTemperature(value: number, transitionTime?: number): Promise; + /** + * Changes this lightbulb's color hue + * @returns true if a request was sent, false otherwise + */ + setHue(value: number, transitionTime?: number): Promise; + /** + * Changes this lightbulb's color saturation + * @returns true if a request was sent, false otherwise + */ + setSaturation(value: number, transitionTime?: number): Promise; } export declare type Spectrum = "none" | "white" | "rgb"; diff --git a/build/lib/light.js b/build/lib/light.js index 8786c0b4..fcce21c2 100644 --- a/build/lib/light.js +++ b/build/lib/light.js @@ -8,10 +8,21 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; Object.defineProperty(exports, "__esModule", { value: true }); +const tradfri_client_1 = require("../tradfri-client"); +const accessory_1 = require("./accessory"); const conversions_1 = require("./conversions"); const ipsoDevice_1 = require("./ipsoDevice"); const ipsoObject_1 = require("./ipsoObject"); +const math_1 = require("./math"); const predefined_colors_1 = require("./predefined-colors"); class Light extends ipsoDevice_1.IPSODevice { constructor(accessory) { @@ -22,6 +33,7 @@ class Light extends ipsoDevice_1.IPSODevice { * Returns the supported color spectrum of the lightbulb */ this._spectrum = null; + this._accessory = accessory; // get the model number to detect features if (accessory != null && accessory.deviceInfo != null && @@ -81,7 +93,139 @@ class Light extends ipsoDevice_1.IPSODevice { return this; } } + // ================================= + // Simplified API access + /** + * Ensures this instance is linked to a tradfri client and an accessory + * @throws Throws an error if it isn't + */ + ensureLink() { + if (!(this.client instanceof tradfri_client_1.TradfriClient)) { + throw new Error("Cannot use the simplified API on devices which aren't linked to a client instance."); + } + if (!(this._accessory instanceof accessory_1.Accessory)) { + throw new Error("Cannot use the simplified API on lightbulbs which aren't linked to an Accessory instance."); + } + } + /** Turn this lightbulb on */ + turnOn() { + return __awaiter(this, void 0, void 0, function* () { + this.ensureLink(); + yield this.client.operateLight(this._accessory, { + onOff: true, + }); + }); + } + /** Turn this lightbulb off */ + turnOff() { + return __awaiter(this, void 0, void 0, function* () { + this.ensureLink(); + yield this.client.operateLight(this._accessory, { + onOff: false, + }); + }); + } + /** Toggles this lightbulb on or off */ + toggle(value = !this.onOff) { + return __awaiter(this, void 0, void 0, function* () { + this.ensureLink(); + yield this.client.operateLight(this._accessory, { + onOff: value, + }); + }); + } + operateLight(operation, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + if (transitionTime != null) { + transitionTime = Math.max(0, transitionTime); + operation.transitionTime = transitionTime; + } + return this.client.operateLight(this._accessory, operation); + }); + } + /** + * Changes this lightbulb's brightness + * @returns true if a request was sent, false otherwise + */ + setBrightness(value, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + this.ensureLink(); + value = math_1.clamp(value, 0, 100); + return this.operateLight({ + dimmer: value, + }, transitionTime); + }); + } + /** + * Changes this lightbulb's color + * @param value The target color as a 6-digit hex string + * @returns true if a request was sent, false otherwise + */ + setColor(value, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + if (this.spectrum === "rgb") + throw new Error("setColor is only available for RGB lightbulbs"); + this.ensureLink(); + return this.operateLight({ + color: value, + }, transitionTime); + }); + } + /** + * Changes this lightbulb's color temperature + * @param value The target color temperature in the range 0% (cold) to 100% (warm) + * @returns true if a request was sent, false otherwise + */ + setColorTemperature(value, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + if (this.spectrum === "white") + throw new Error("setColorTemperature is only available for white spectrum lightbulbs"); + this.ensureLink(); + value = math_1.clamp(value, 0, 100); + return this.operateLight({ + colorTemperature: value, + }, transitionTime); + }); + } + /** + * Changes this lightbulb's color hue + * @returns true if a request was sent, false otherwise + */ + setHue(value, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + if (this.spectrum === "rgb") + throw new Error("setHue is only available for RGB lightbulbs"); + this.ensureLink(); + value = math_1.clamp(value, 0, 360); + return this.operateLight({ + hue: value, + }, transitionTime); + }); + } + /** + * Changes this lightbulb's color saturation + * @returns true if a request was sent, false otherwise + */ + setSaturation(value, transitionTime) { + return __awaiter(this, void 0, void 0, function* () { + if (this.spectrum === "rgb") + throw new Error("setSaturation is only available for RGB lightbulbs"); + this.ensureLink(); + value = math_1.clamp(value, 0, 100); + return this.operateLight({ + saturation: value, + }, transitionTime); + }); + } } +__decorate([ + ipsoObject_1.doNotSerialize, + __metadata("design:type", String) +], Light.prototype, "_modelName", void 0); +__decorate([ + ipsoObject_1.doNotSerialize, + __metadata("design:type", accessory_1.Accessory) +], Light.prototype, "_accessory", void 0); __decorate([ ipsoObject_1.ipsoKey("5706"), ipsoObject_1.doNotSerialize // this is done through colorX / colorY diff --git a/build/tradfri-client.js b/build/tradfri-client.js index 5eae7d2e..a6176b5b 100644 --- a/build/tradfri-client.js +++ b/build/tradfri-client.js @@ -208,7 +208,7 @@ class TradfriClient extends events_1.EventEmitter { // store a clone, so we don't have to care what the calling library does this.devices[instanceId] = accessory.clone(); // and notify all listeners about the update - this.emit("device updated", accessory); + this.emit("device updated", accessory.link(this)); } /** Sets up an observer for all groups */ observeGroupsAndScenes() { @@ -294,7 +294,7 @@ class TradfriClient extends events_1.EventEmitter { // store a clone, so we don't have to care what the calling library does groupInfo.group = group.clone(); // notify all listeners about the update - this.emit("group updated", group); + this.emit("group updated", group.link(this)); // load scene information this.observeResource(`${endpoints_1.endpoints.scenes}/${instanceId}`, (resp) => this.observeScenes_callback(instanceId, resp)); }); @@ -353,7 +353,7 @@ class TradfriClient extends events_1.EventEmitter { // store a clone, so we don't have to care what the calling library does this.groups[groupId].scenes[instanceId] = scene.clone(); // and notify all listeners about the update - this.emit("scene updated", groupId, scene); + this.emit("scene updated", groupId, scene.link(this)); } /** * Pings the gateway to check if it is alive diff --git a/src/lib/accessory.ts b/src/lib/accessory.ts index 0963009e..b695cbfa 100644 --- a/src/lib/accessory.ts +++ b/src/lib/accessory.ts @@ -1,3 +1,4 @@ +import { TradfriClient } from "../tradfri-client"; import { DeviceInfo } from "./deviceInfo"; import { IPSODevice } from "./ipsoDevice"; import { deserializeWith, ipsoKey, IPSOObject, PropertyTransform, required, serializeWith } from "./ipsoObject"; @@ -47,4 +48,26 @@ export class Accessory extends IPSODevice { @ipsoKey("9054") public otaUpdateState: number = 0; // boolean? + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + public link(client: TradfriClient): this { + super.link(client); + for (const light of this.lightList) { + light.link(client); + } + for (const plug of this.plugList) { + plug.link(client); + } + for (const sensor of this.sensorList) { + sensor.link(client); + } + for (const swtch of this.switchList) { + swtch.link(client); + } + return this; + } + } diff --git a/src/lib/ipsoObject.ts b/src/lib/ipsoObject.ts index 98e0ffb7..72622157 100644 --- a/src/lib/ipsoObject.ts +++ b/src/lib/ipsoObject.ts @@ -1,3 +1,4 @@ +import { TradfriClient } from "../tradfri-client"; import { log } from "./logger"; import { DictionaryLike, entries } from "./object-polyfill"; @@ -498,4 +499,15 @@ export class IPSOObject { }); } + @doNotSerialize protected client: TradfriClient; + /** + * Link this object to a TradfriClient for a simplified API. + * INTERNAL USE ONLY! + * @param client The client instance to link this object to + */ + public link(client: TradfriClient): this { + this.client = client; + return this; + } + } diff --git a/src/lib/light.ts b/src/lib/light.ts index 42af8552..30e4da54 100644 --- a/src/lib/light.ts +++ b/src/lib/light.ts @@ -1,7 +1,9 @@ +import { TradfriClient } from "../tradfri-client"; import { Accessory } from "./accessory"; import { conversions, deserializers, serializers } from "./conversions"; import { IPSODevice } from "./ipsoDevice"; import { deserializeWith, doNotSerialize, ipsoKey, IPSOObject, PropertyTransform, required, serializeWith } from "./ipsoObject"; +import { clamp } from "./math"; import { MAX_COLOR, predefinedColors } from "./predefined-colors"; // see https://github.com/hreichert/smarthome/blob/master/extensions/binding/org.eclipse.smarthome.binding.tradfri/src/main/java/org/eclipse/smarthome/binding/modules/internal/TradfriColor.java @@ -18,6 +20,8 @@ export class Light extends IPSODevice { constructor(accessory?: Accessory) { super(); + this._accessory = accessory; + // get the model number to detect features if (accessory != null && accessory.deviceInfo != null && @@ -28,7 +32,8 @@ export class Light extends IPSODevice { } } - private _modelName: string; + @doNotSerialize private _modelName: string; + @doNotSerialize private _accessory: Accessory; @ipsoKey("5706") @doNotSerialize // this is done through colorX / colorY @@ -140,6 +145,122 @@ export class Light extends IPSODevice { } } + // ================================= + // Simplified API access + /** + * Ensures this instance is linked to a tradfri client and an accessory + * @throws Throws an error if it isn't + */ + private ensureLink() { + if (!(this.client instanceof TradfriClient)) { + throw new Error("Cannot use the simplified API on devices which aren't linked to a client instance."); + } + if (!(this._accessory instanceof Accessory)) { + throw new Error("Cannot use the simplified API on lightbulbs which aren't linked to an Accessory instance."); + } + } + + /** Turn this lightbulb on */ + public async turnOn() { + this.ensureLink(); + await this.client.operateLight(this._accessory, { + onOff: true, + }); + } + + /** Turn this lightbulb off */ + public async turnOff() { + this.ensureLink(); + await this.client.operateLight(this._accessory, { + onOff: false, + }); + } + + /** Toggles this lightbulb on or off */ + public async toggle(value: boolean = !this.onOff) { + this.ensureLink(); + await this.client.operateLight(this._accessory, { + onOff: value, + }); + } + + private async operateLight(operation: LightOperation, transitionTime?: number): Promise { + if (transitionTime != null) { + transitionTime = Math.max(0, transitionTime); + operation.transitionTime = transitionTime; + } + return this.client.operateLight(this._accessory, operation); + } + + /** + * Changes this lightbulb's brightness + * @returns true if a request was sent, false otherwise + */ + public async setBrightness(value: number, transitionTime?: number): Promise { + this.ensureLink(); + + value = clamp(value, 0, 100); + return this.operateLight({ + dimmer: value, + }, transitionTime); + } + + /** + * Changes this lightbulb's color + * @param value The target color as a 6-digit hex string + * @returns true if a request was sent, false otherwise + */ + public async setColor(value: string, transitionTime?: number): Promise { + if (this.spectrum === "rgb") throw new Error("setColor is only available for RGB lightbulbs"); + this.ensureLink(); + + return this.operateLight({ + color: value, + }, transitionTime); + } + + /** + * Changes this lightbulb's color temperature + * @param value The target color temperature in the range 0% (cold) to 100% (warm) + * @returns true if a request was sent, false otherwise + */ + public async setColorTemperature(value: number, transitionTime?: number): Promise { + if (this.spectrum === "white") throw new Error("setColorTemperature is only available for white spectrum lightbulbs"); + this.ensureLink(); + + value = clamp(value, 0, 100); + return this.operateLight({ + colorTemperature: value, + }, transitionTime); + } + + /** + * Changes this lightbulb's color hue + * @returns true if a request was sent, false otherwise + */ + public async setHue(value: number, transitionTime?: number): Promise { + if (this.spectrum === "rgb") throw new Error("setHue is only available for RGB lightbulbs"); + this.ensureLink(); + + value = clamp(value, 0, 360); + return this.operateLight({ + hue: value, + }, transitionTime); + } + + /** + * Changes this lightbulb's color saturation + * @returns true if a request was sent, false otherwise + */ + public async setSaturation(value: number, transitionTime?: number): Promise { + if (this.spectrum === "rgb") throw new Error("setSaturation is only available for RGB lightbulbs"); + this.ensureLink(); + + value = clamp(value, 0, 100); + return this.operateLight({ + saturation: value, + }, transitionTime); + } } export type Spectrum = "none" | "white" | "rgb"; diff --git a/src/tradfri-client.ts b/src/tradfri-client.ts index d8b4e86b..9209ff1c 100644 --- a/src/tradfri-client.ts +++ b/src/tradfri-client.ts @@ -277,7 +277,7 @@ export class TradfriClient extends EventEmitter { // store a clone, so we don't have to care what the calling library does this.devices[instanceId] = accessory.clone(); // and notify all listeners about the update - this.emit("device updated", accessory); + this.emit("device updated", accessory.link(this)); } /** Sets up an observer for all groups */ @@ -377,7 +377,7 @@ export class TradfriClient extends EventEmitter { groupInfo.group = group.clone(); // notify all listeners about the update - this.emit("group updated", group); + this.emit("group updated", group.link(this)); // load scene information this.observeResource( @@ -449,7 +449,7 @@ export class TradfriClient extends EventEmitter { // store a clone, so we don't have to care what the calling library does this.groups[groupId].scenes[instanceId] = scene.clone(); // and notify all listeners about the update - this.emit("scene updated", groupId, scene); + this.emit("scene updated", groupId, scene.link(this)); } /**