klf-200-api
Version:
This module provides a wrapper to the socket API of a Velux KLF-200 interface. You will need at least firmware 0.2.0.0.71 on your KLF interface for this library to work.
635 lines (634 loc) • 26 kB
JavaScript
"use strict";
import debugModule from "debug";
import { GW_ACTIVATE_PRODUCTGROUP_REQ } from "./KLF200-API/GW_ACTIVATE_PRODUCTGROUP_REQ.js";
import { ActivateProductGroupStatus, convertPosition, } from "./KLF200-API/GW_COMMAND.js";
import { GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF } from "./KLF200-API/GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF.js";
import { GW_GET_ALL_GROUPS_INFORMATION_NTF } from "./KLF200-API/GW_GET_ALL_GROUPS_INFORMATION_NTF.js";
import { GW_GET_ALL_GROUPS_INFORMATION_REQ } from "./KLF200-API/GW_GET_ALL_GROUPS_INFORMATION_REQ.js";
import { GW_GET_GROUP_INFORMATION_NTF } from "./KLF200-API/GW_GET_GROUP_INFORMATION_NTF.js";
import { GW_GET_GROUP_INFORMATION_REQ } from "./KLF200-API/GW_GET_GROUP_INFORMATION_REQ.js";
import { GW_GET_NODE_INFORMATION_NTF } from "./KLF200-API/GW_GET_NODE_INFORMATION_NTF.js";
import { GW_GET_NODE_INFORMATION_REQ } from "./KLF200-API/GW_GET_NODE_INFORMATION_REQ.js";
import { GroupType } from "./KLF200-API/GW_GROUPS.js";
import { ChangeType, GW_GROUP_INFORMATION_CHANGED_NTF, } from "./KLF200-API/GW_GROUP_INFORMATION_CHANGED_NTF.js";
import { GW_SET_GROUP_INFORMATION_REQ } from "./KLF200-API/GW_SET_GROUP_INFORMATION_REQ.js";
import { GW_COMMON_STATUS, GatewayCommand } from "./KLF200-API/common.js";
import { Component } from "./utils/PropertyChangedEvent.js";
import { TypedEvent } from "./utils/TypedEvent.js";
import { isArrayEqual } from "./utils/UtilityFunctions.js";
const debug = debugModule(`klf-200-api:groups`);
/**
* The gateway can hold up to 100 groups. A group is a collection of actuator nodes in
conjunction with a name and some other come characteristics.
There are three different group types. House, Room and User defined. There can be only
one instance of the group type house. The GroupID = 0 is reserved for the house group.
An actuator can only be represented in one room group. So, if an actuator is assigned to
a room group it will automatically be removed from another existing room group.
*
* @export
* @class Group
* @extends {Component}
*/
export class Group extends Component {
Connection;
/**
* ID of the group.
*
* @type {number}
* @memberof Group
*/
GroupID;
_order;
_placement;
_name;
_velocity;
_nodeVariation;
_groupType;
/**
* List of node IDs which are part of the group.
*
* @type {number[]}
* @memberof Group
*/
_Nodes = [];
get Nodes() {
return this._Nodes;
}
_revision;
/**
* Creates an instance of Group based on the provided notification frame.
* You shouldn't create groups by yourself but rather use the [[Groups]] class
* to provide you with a list of all groups.
* @param {IConnection} Connection The connection that will be used to send and receive commands.
* @param {(GW_GET_GROUP_INFORMATION_NTF | GW_GET_ALL_GROUPS_INFORMATION_NTF | GW_GROUP_INFORMATION_CHANGED_NTF)} frame Notification frame that is used to set the properties of the Group class instance.
* @memberof Group
*/
constructor(Connection, frame) {
super();
this.Connection = Connection;
this.GroupID = frame.GroupID;
this._order = frame.Order;
this._placement = frame.Placement;
this._name = frame.Name;
this._velocity = frame.Velocity;
this._nodeVariation = frame.NodeVariation;
this._groupType = frame.GroupType;
this.Nodes.push(...frame.Nodes);
this._revision = frame.Revision;
this.Connection.on(async (frame) => {
debug(`Calling onNotificationHandler for GW_GET_GROUP_INFORMATION_NTF added in Group constructor.`);
await this.onNotificationHandler(frame);
}, [GatewayCommand.GW_GET_GROUP_INFORMATION_NTF]);
}
/**
* This method fires PropertyChanged events based on the changes
* in the frame provided by the parameter.
* This method will be used internally and you shouldn't need to
* use it on your own.
*
* @param {GW_GROUP_INFORMATION_CHANGED_NTF_Modified} frame Change notification frame to calculate the changes.
* @memberof Group
*/
async changeFromNotification(frame) {
if (this._order !== frame.Order) {
this._order = frame.Order;
await this.propertyChanged("Order");
}
if (this._placement !== frame.Placement) {
this._placement = frame.Placement;
await this.propertyChanged("Placement");
}
if (this._name !== frame.Name) {
this._name = frame.Name;
await this.propertyChanged("Name");
}
if (this._velocity !== frame.Velocity) {
this._velocity = frame.Velocity;
await this.propertyChanged("Velocity");
}
if (this._nodeVariation !== frame.NodeVariation) {
this._nodeVariation = frame.NodeVariation;
await this.propertyChanged("NodeVariation");
}
if (this._groupType !== frame.GroupType) {
this._groupType = frame.GroupType;
await this.propertyChanged("GroupType");
}
if (!isArrayEqual(this.Nodes, frame.Nodes)) {
this.Nodes.length = 0; // Clear nodes array
this.Nodes.push(...frame.Nodes);
await this.propertyChanged("Nodes");
}
this._revision = frame.Revision;
}
/**
* The order in which the groups should be displayed by a client application.
*
* @readonly
* @type {number}
* @memberof Group
*/
get Order() {
return this._order;
}
/**
* The placement of the product. Either a house index or a room index number.
*
* @readonly
* @type {number}
* @memberof Group
*/
get Placement() {
return this._placement;
}
/**
* Name of the group.
*
* @readonly
* @type {string}
* @memberof Group
*/
get Name() {
return this._name;
}
/**
* The velocity at which the products of the group are operated at if possible.
*
* @readonly
* @type {Velocity}
* @memberof Group
*/
get Velocity() {
return this._velocity;
}
/**
* Defines the variation of the group.
*
* @readonly
* @type {NodeVariation}
* @memberof Group
*/
get NodeVariation() {
return this._nodeVariation;
}
/**
* Type of the group.
*
* * House - The house group can't be changed and contains all node IDs.
* * Room - Each product can only be in one room.
* * User group - User groups can be defined freely.
*
* @readonly
* @type {GroupType}
* @memberof Group
*/
get GroupType() {
return this._groupType;
}
/**
* Change properties of the group.
*
* If there are no changes in the properties the method returns directly with a resolved promise.
*
* @param {number} order New value for the Order property.
* @param {number} placement New value for the Placement property.
* @param {string} name New value for the Name property.
* @param {Velocity} velocity New value for the Velocity property.
* @param {NodeVariation} nodeVariation New value for the NodeVariation property.
* @param {number[]} nodes New list of nodes.
* @returns {Promise<void>}
* @memberof Group
*/
async changeGroupAsync(order, placement, name, velocity, nodeVariation, nodes) {
try {
const changedProperties = [];
if (order !== this._order)
changedProperties.push("Order");
if (placement !== this._placement)
changedProperties.push("Placement");
if (name !== this._name)
changedProperties.push("Name");
if (velocity !== this._velocity)
changedProperties.push("Velocity");
if (nodeVariation !== this._nodeVariation)
changedProperties.push("NodeVariation");
if (!isArrayEqual(nodes, this.Nodes))
changedProperties.push("Nodes");
// If there are no changes in the properties return directly with a resolved promise.
if (changedProperties.length === 0)
return Promise.resolve();
const confirmationFrame = (await this.Connection.sendFrameAsync(new GW_SET_GROUP_INFORMATION_REQ(this.GroupID, this._revision, name, this._groupType, nodes, order, placement, velocity, nodeVariation)));
if (confirmationFrame.Status === GW_COMMON_STATUS.SUCCESS) {
return Promise.resolve();
}
else {
return Promise.reject(new Error(confirmationFrame.getError()));
}
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Sets a new value for the order number of the group.
*
* @param {number} newOrder New value for the order property.
* @returns {Promise<void>}
* @memberof Group
*/
async setOrderAsync(newOrder) {
return this.changeGroupAsync(newOrder, this._placement, this._name, this._velocity, this._nodeVariation, this.Nodes);
}
/**
* Sets a new value for the placement of the group.
*
* @param {number} newPlacement New value for the placement property.
* @returns {Promise<void>}
* @memberof Group
*/
async setPlacementAsync(newPlacement) {
return this.changeGroupAsync(this._order, newPlacement, this._name, this._velocity, this._nodeVariation, this.Nodes);
}
/**
* Renames the group.
*
* @param {string} newName New name of the group.
* @returns {Promise<void>}
* @memberof Group
*/
async setNameAsync(newName) {
return this.changeGroupAsync(this._order, this._placement, newName, this._velocity, this._nodeVariation, this.Nodes);
}
/**
* Sets the velocity for the group.
*
* @param {Velocity} newVelocity New velocity value for the group.
* @returns {Promise<void>}
* @memberof Group
*/
async setVelocityAsync(newVelocity) {
return this.changeGroupAsync(this._order, this._placement, this._name, newVelocity, this._nodeVariation, this.Nodes);
}
/**
* Sets the variation of the group to a new value.
*
* @param {NodeVariation} newNodeVariation New value for the variation of the group.
* @returns {Promise<void>}
* @memberof Group
*/
async setNodeVariationAsync(newNodeVariation) {
return this.changeGroupAsync(this._order, this._placement, this._name, this._velocity, newNodeVariation, this.Nodes);
}
/**
* Sets the group to contain the provided list of node IDs in the group.
*
* @param {number[]} newNodes Array of new node IDs for the group.
* @returns {Promise<void>}
* @memberof Group
*/
async setNodesAsync(newNodes) {
return this.changeGroupAsync(this._order, this._placement, this._name, this._velocity, this._nodeVariation, newNodes);
}
/**
* Sets the target position for all products of the group as raw value.
*
* @param {number} newPositionRaw New target position value as raw value.
* @param Velocity The velocity with which the scene will be run.
* @param PriorityLevel The priority level for the run command.
* @param CommandOriginator The command originator for the run command.
* @param ParameterActive The parameter that should be set by this command. MP or FP1-FP16.
* @param PriorityLevelLock Flag if the priority level lock should be used.
* @param PriorityLevels Up to 8 priority levels.
* @param LockTime Lock time for the priority levels in seconds (multiple of 30 or Infinity).
* @returns {Promise<number>} Returns the session ID of the command.
* @memberof Group
*/
async setTargetPositionRawAsync(newPositionRaw, Velocity = 0, PriorityLevel = 3, CommandOriginator = 1, ParameterActive = 0, PriorityLevelLock = 0, PriorityLevels = [], LockTime = Infinity) {
try {
const confirmationFrame = (await this.Connection.sendFrameAsync(new GW_ACTIVATE_PRODUCTGROUP_REQ(this.GroupID, newPositionRaw, PriorityLevel, CommandOriginator, ParameterActive, Velocity, PriorityLevelLock, PriorityLevels, LockTime)));
if (confirmationFrame.Status === ActivateProductGroupStatus.OK) {
return confirmationFrame.SessionID;
}
else {
return Promise.reject(new Error(confirmationFrame.getError()));
}
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Sets the target position for all products of the group
*
* @param {number} newPosition New target position value in percent.
* @param Velocity The velocity with which the scene will be run.
* @param PriorityLevel The priority level for the run command.
* @param CommandOriginator The command originator for the run command.
* @param ParameterActive The parameter that should be set by this command. MP or FP1-FP16.
* @param PriorityLevelLock Flag if the priority level lock should be used.
* @param PriorityLevels Up to 8 priority levels.
* @param LockTime Lock time for the priority levels in seconds (multiple of 30 or Infinity).
* @returns {Promise<number>} Returns the session ID of the command.
* @memberof Group
*/
async setTargetPositionAsync(newPosition, Velocity = 0, PriorityLevel = 3, CommandOriginator = 1, ParameterActive = 0, PriorityLevelLock = 0, PriorityLevels = [], LockTime = Infinity) {
try {
// Get product type from first node ID for conversion
const nodeID = this.Nodes[0];
// Setup the event handlers first to prevent a race condition
// where we don't see the events.
let resolve, reject;
const nodeTypeIDPromise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Setup notification to receive notification with actuator type
// Register notification handler
const dispose = this.Connection.on((frame) => {
try {
debug(`Calling handler for GW_GET_NODE_INFORMATION_NTF in Group.setTargetPositionAsync.`);
if (frame instanceof GW_GET_NODE_INFORMATION_NTF && frame.NodeID === nodeID) {
const nodeTypeID = frame.ActuatorType;
if (dispose) {
dispose.dispose();
}
resolve(nodeTypeID);
}
}
catch (error) {
if (dispose) {
dispose.dispose();
}
reject(error);
}
}, [GatewayCommand.GW_GET_NODE_INFORMATION_NTF]);
try {
const productInformation = (await this.Connection.sendFrameAsync(new GW_GET_NODE_INFORMATION_REQ(nodeID)));
if (productInformation.Status !== GW_COMMON_STATUS.SUCCESS) {
if (dispose) {
dispose.dispose();
}
return Promise.reject(new Error(productInformation.getError()));
}
}
catch (error) {
if (dispose) {
dispose.dispose();
}
return Promise.reject(error);
}
return this.setTargetPositionRawAsync(convertPosition(newPosition, await nodeTypeIDPromise), Velocity, PriorityLevel, CommandOriginator, ParameterActive, PriorityLevelLock, PriorityLevels, LockTime);
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Refresh the data of this group and read the attributes from the gateway.
*
* You can use this method to refresh the state of the group in case
* that you have missed changes, e.g. a simple remote control may change
* the state of the group and you won't receive an event for it.
*
* @returns {Promise<void>}
* @memberof Group
*/
async refreshAsync() {
try {
const confirmationFrame = (await this.Connection.sendFrameAsync(new GW_GET_GROUP_INFORMATION_REQ(this.GroupID)));
if (confirmationFrame.Status === GW_COMMON_STATUS.SUCCESS) {
return Promise.resolve();
}
else {
return Promise.reject(new Error(confirmationFrame.getError()));
}
}
catch (error) {
return Promise.reject(error);
}
}
async onNotificationHandler(frame) {
if (typeof this === "undefined")
return;
if (frame instanceof GW_GET_GROUP_INFORMATION_NTF) {
await this.onGetGroupInformation(frame);
}
}
async onGetGroupInformation(frame) {
if (frame.GroupID === this.GroupID) {
if (frame.Order !== this._order) {
this._order = frame.Order;
await this.propertyChanged("Order");
}
if (frame.Placement !== this._placement) {
this._placement = frame.Placement;
await this.propertyChanged("Placement");
}
if (frame.Name !== this._name) {
this._name = frame.Name;
await this.propertyChanged("Name");
}
if (frame.Velocity !== this._velocity) {
this._velocity = frame.Velocity;
await this.propertyChanged("Velocity");
}
if (frame.NodeVariation !== this._nodeVariation) {
this._nodeVariation = frame.NodeVariation;
await this.propertyChanged("NodeVariation");
}
if (frame.GroupType !== this._groupType) {
this._groupType = frame.GroupType;
await this.propertyChanged("GroupType");
}
const hasRemovedNodes = this.Nodes.every((item) => frame.Nodes.includes(item));
const hasNewNodes = frame.Nodes.every((item) => this.Nodes.includes(item));
if (hasRemovedNodes || hasNewNodes) {
this.Nodes.length = 0;
this.Nodes.push(...frame.Nodes);
await this.propertyChanged("Nodes");
}
this._revision = frame.Revision;
}
}
}
/**
* The Groups class represent all groups defined in the KLF-200.
*
* @export
* @class Groups
*/
export class Groups {
Connection;
groupType;
_onChangedGroup = new TypedEvent();
_onRemovedGroup = new TypedEvent();
/**
* Contains a list of groups.
* The index of each group corresponds to the
* group ID.
*
* @type {Group[]}
* @memberof Groups
*/
Groups = [];
constructor(Connection, groupType = GroupType.UserGroup) {
this.Connection = Connection;
this.groupType = groupType;
}
async initializeGroupsAsync() {
// Setup notification to receive notification with actuator type
let dispose;
try {
// Setup the event handlers first to prevent a race condition
// where we don't see the events.
let resolve, reject;
const notificationHandler = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
dispose = this.Connection.on((frame) => {
try {
debug(`Calling handler for GW_GET_ALL_GROUPS_INFORMATION_NTF, GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF, GW_GET_GROUP_INFORMATION_NTF in Groups.initializeGroupsAsync.`);
if (frame instanceof GW_GET_ALL_GROUPS_INFORMATION_NTF ||
frame instanceof GW_GET_GROUP_INFORMATION_NTF) {
this.Groups[frame.GroupID] = new Group(this.Connection, frame);
}
else if (frame instanceof GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF) {
if (dispose) {
dispose.dispose();
}
resolve();
}
}
catch (error) {
if (dispose) {
dispose.dispose();
}
reject(error);
}
}, [
GatewayCommand.GW_GET_ALL_GROUPS_INFORMATION_NTF,
GatewayCommand.GW_GET_ALL_GROUPS_INFORMATION_FINISHED_NTF,
GatewayCommand.GW_GET_GROUP_INFORMATION_NTF,
]);
const getAllGroupsInformation = (await this.Connection.sendFrameAsync(new GW_GET_ALL_GROUPS_INFORMATION_REQ(this.groupType)));
if (getAllGroupsInformation.Status !== GW_COMMON_STATUS.SUCCESS) {
if (dispose) {
dispose.dispose();
}
if (getAllGroupsInformation.Status !==
GW_COMMON_STATUS.INVALID_NODE_ID /* No groups available -> not a real error */) {
return Promise.reject(new Error(getAllGroupsInformation.getError()));
}
}
// Only wait for notifications if there are groups defined
if (getAllGroupsInformation.NumberOfGroups > 0) {
await notificationHandler;
}
else {
if (dispose) {
dispose.dispose();
}
}
// Finally, setup the event handler for notifications
this.Connection.on(async (frame) => {
debug(`Calling onNotificationHandler for GW_GROUP_INFORMATION_CHANGED_NTF added in Groups.initializeGroupsAsync.`);
await this.onNotificationHandler(frame);
}, [GatewayCommand.GW_GROUP_INFORMATION_CHANGED_NTF]);
}
catch (error) {
if (dispose) {
dispose.dispose();
}
return Promise.reject(error);
}
}
/**
* Adds a handler that will be called if a new group is added to the KLF-200 interface or a group has been changed.
*
* @param {Listener<number>} handler Event handler that is called if a new group is added or a group has been changed.
* @returns {Disposable} The event handler can be removed by using the dispose method of the returned object.
* @memberof Groups
*/
onChangedGroup(handler) {
return this._onChangedGroup.on(handler);
}
/**
* Adds a handler that will be called if a group is removed from the KLF-200 interface.
*
* @param {Listener<number>} handler Event handler that is called if a group is removed.
* @returns {Disposable} The event handler can be removed by using the dispose method of the returned object.
* @memberof Groups
*/
onRemovedGroup(handler) {
return this._onRemovedGroup.on(handler);
}
async notifyChangedGroup(groupId) {
await this._onChangedGroup.emit(groupId);
}
async notifyRemovedGroup(groupId) {
await this._onRemovedGroup.emit(groupId);
}
async onNotificationHandler(frame) {
if (frame instanceof GW_GROUP_INFORMATION_CHANGED_NTF) {
switch (frame.ChangeType) {
case ChangeType.Deleted:
// Remove group
delete this.Groups[frame.GroupID];
await this.notifyRemovedGroup(frame.GroupID);
break;
case ChangeType.Modified:
// Add or change group
if (typeof this.Groups[frame.GroupID] === "undefined") {
// Add node
this.Groups[frame.GroupID] = new Group(this.Connection, frame);
}
else {
// Change group
await this.Groups[frame.GroupID].changeFromNotification(frame);
}
await this.notifyChangedGroup(frame.GroupID);
default:
break;
}
}
}
/**
* Creates a new instance of the Groups class.
* During the initialization phase of the class
* a list of all groups will be retrieved from
* the KLF-200 interface and stored at the
* Groups array.
*
* Additionally, some notification handlers
* will be instantiated to watch for changes
* to the groups.
*
* @static
* @param {IConnection} Connection The connection object that handles the communication to the KLF interface.
* @param [groupType=GroupType.UserGroup] The group type for which the groups should be read. Default is {@link GroupType.UserGroup}.
* @returns {Promise<Groups>} Resolves to a new instance of the Groups class.
* @memberof Groups
*/
static async createGroupsAsync(Connection, groupType = GroupType.UserGroup) {
try {
const result = new Groups(Connection, groupType);
await result.initializeGroupsAsync();
return result;
}
catch (error) {
return Promise.reject(error);
}
}
/**
* Finds a group by its name and returns the group object.
*
* @param {string} groupName The name of the group.
* @returns {(Group | undefined)} Returns the group object if found, otherwise undefined.
* @memberof Groups
*/
findByName(groupName) {
return this.Groups.find((grp) => typeof grp !== "undefined" && grp.Name === groupName);
}
}
//# sourceMappingURL=groups.js.map