Skip to content

Commit

Permalink
Green Power support (#92)
Browse files Browse the repository at this point in the history
* First working green power implementation.

* More working

* Adapter updates.

* Revert addToGroup

* Updates

* Updates.

* Update

* Update

* solve todo

* Fix tests.

* Updates

* finish tests
  • Loading branch information
Koenkk committed Apr 9, 2020
1 parent d309a3d commit 093af45
Show file tree
Hide file tree
Showing 15 changed files with 518 additions and 41 deletions.
3 changes: 3 additions & 0 deletions src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Debug from "debug";
const debug = Debug("zigbee-herdsman:adapter");

abstract class Adapter extends events.EventEmitter {
public readonly greenPowerGroup = 0x0b84;
protected networkOptions: TsType.NetworkOptions;
protected adapterOptions: TsType.AdapterOptions;
protected serialPortOptions: TsType.SerialPortOptions;
Expand Down Expand Up @@ -155,6 +156,8 @@ abstract class Adapter extends events.EventEmitter {

public abstract sendZclFrameToGroup(groupID: number, zclFrame: ZclFrame): Promise<void>;

public abstract sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number): Promise<void>;

/**
* InterPAN
*/
Expand Down
4 changes: 4 additions & 0 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,10 @@ class DeconzAdapter extends Adapter {
}
}

public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number): Promise<void> {
// TODO: not implemented yet
}

public async bind(
destinationNetworkAddress: number, sourceIeeeAddress: string, sourceEndpoint: number,
clusterID: number, destinationAddressOrGroup: string | number, type: 'endpoint' | 'group',
Expand Down
13 changes: 12 additions & 1 deletion src/adapter/z-stack/adapter/startZnp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const Endpoints = [
// Insta/Jung/Gira: OTA fallback EP (since it's buggy in firmware 10023202 when it tries to find a matching EP for
// OTA - it queries for ZLL profile, but then contacts with HA profile)
{...EndpointDefaults, endpoint: 47, appprofid: 0x0104},
{...EndpointDefaults, endpoint: 242, appprofid: 0xa1e0},
];

async function validateItem(
Expand Down Expand Up @@ -187,8 +188,15 @@ async function initialise(znp: Znp, version: ZnpVersion, options: TsType.Network
await znp.request(Subsystem.SYS, 'osalNvWrite', Items.znpHasConfigured(version));
}

async function addToGroup(znp: Znp, endpoint: number, group: number): Promise<void> {
const result = await znp.request(5, 'extFindGroup', {endpoint, groupid: group}, [0, 1]);
if (result.payload.status === 1) {
await znp.request(5, 'extAddGroup', {endpoint, groupid: group, namelen: 0, groupname:[]});
}
}

export default async (
znp: Znp, version: ZnpVersion, options: TsType.NetworkOptions, backupPath?: string
znp: Znp, version: ZnpVersion, options: TsType.NetworkOptions, greenPowerGroup: number, backupPath?: string,
): Promise<TsType.StartResult> => {
let result: TsType.StartResult = 'resumed';
let hasConfigured = false;
Expand Down Expand Up @@ -222,6 +230,9 @@ export default async (
await boot(znp);
await registerEndpoints(znp);

// Add to required group to receive greenPower messages.
await addToGroup(znp, 242, greenPowerGroup);

if (result === 'restored') {
// Write channellist again, otherwise it doesnt seem to stick.
await znp.request(Subsystem.SYS, 'osalNvWrite', Items.channelList(options.channelList));
Expand Down
18 changes: 17 additions & 1 deletion src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ZStackAdapter extends Adapter {

debug(`Detected znp version '${ZnpVersion[this.version.product]}' (${JSON.stringify(this.version)})`);

return StartZnp(this.znp, this.version.product, this.networkOptions, this.backupPath);
return StartZnp(this.znp, this.version.product, this.networkOptions, this.greenPowerGroup, this.backupPath);
}

public async stop(): Promise<void> {
Expand Down Expand Up @@ -328,6 +328,22 @@ class ZStackAdapter extends Adapter {
});
}

public async sendZclFrameToAll(endpoint: number, zclFrame: ZclFrame, sourceEndpoint: number): Promise<void> {
return this.queue.execute<void>(async () => {
await this.dataRequestExtended(
Constants.COMMON.addressMode.ADDR_16BIT, 0xFFFD, endpoint, 0, sourceEndpoint,
zclFrame.Cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(), 3000, false, 0
);

/**
* As a broadcast command is not confirmed and thus immidiately returns
* (contrary to network address requests) we will give the
* command some time to 'settle' in the network.
*/
await Wait(200);
});
}

public async lqi(networkAddress: number): Promise<LQI> {
return this.queue.execute<LQI>(async (): Promise<LQI> => {
const neighbors: LQINeighbor[] = [];
Expand Down
53 changes: 46 additions & 7 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {TsType as AdapterTsType, Adapter, Events as AdapterEvents} from '../adap
import {Entity, Device} from './model';
import {ZclFrameConverter} from './helpers';
import * as Events from './events';
import {KeyValue, DeviceType} from './tstype';
import {KeyValue, DeviceType, GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype';
import Debug from "debug";
import fs from 'fs';
import {Utils as ZclUtils, FrameControl} from '../zcl';
import Touchlink from './touchlink';
import GreenPower from './greenPower';

// @ts-ignore
import mixin from 'mixin-deep';
Expand Down Expand Up @@ -62,6 +63,7 @@ class Controller extends events.EventEmitter {
private options: Options;
private database: Database;
private adapter: Adapter;
private greenPower: GreenPower;
// eslint-disable-next-line
private permitJoinTimer: any;
// eslint-disable-next-line
Expand Down Expand Up @@ -105,6 +107,9 @@ class Controller extends events.EventEmitter {
Entity.injectAdapter(this.adapter);
Entity.injectDatabase(this.database);

this.greenPower = new GreenPower(this.adapter);
this.greenPower.on(GreenPowerEvents.deviceJoined, this.onDeviceJoinedGreenPower.bind(this));

// Register adapter events
this.adapter.on(AdapterEvents.Events.deviceJoined, this.onDeviceJoined.bind(this));
this.adapter.on(AdapterEvents.Events.zclData, (data) => this.onZclOrRawData('zcl', data));
Expand Down Expand Up @@ -134,7 +139,7 @@ class Controller extends events.EventEmitter {
debug.log('No coordinator in database, querying...');
Device.create(
'Coordinator', coordinator.ieeeAddr, coordinator.networkAddress, coordinator.manufacturerID,
undefined, undefined, undefined, coordinator.endpoints
undefined, undefined, undefined, true, coordinator.endpoints
);
}

Expand All @@ -161,16 +166,19 @@ class Controller extends events.EventEmitter {
if (permit && !this.getPermitJoin()) {
debug.log('Permit joining');
await this.adapter.permitJoin(254, !device ? null : device.networkAddress);
await this.greenPower.permitJoin(254);

// Zigbee 3 networks automatically close after max 255 seconds, keep network open.
this.permitJoinTimer = setInterval(async (): Promise<void> => {
debug.log('Permit joining');
await this.adapter.permitJoin(254, !device ? null : device.networkAddress);
await this.greenPower.permitJoin(254);
}, 200 * 1000);
} else if (permit && this.getPermitJoin()) {
debug.log('Joining already permitted');
} else {
debug.log('Disable joining');
await this.greenPower.permitJoin(0);
await this.adapter.permitJoin(0, null);

if (this.permitJoinTimer) {
Expand Down Expand Up @@ -337,6 +345,34 @@ class Controller extends events.EventEmitter {
this.emit(Events.Events.adapterDisconnected);
}

private async onDeviceJoinedGreenPower(payload: GreenPowerDeviceJoinedPayload): Promise<void> {
debug.log(`Green power device '${JSON.stringify(payload)}' joined`);

// Green power devices don't have an ieeeAddr, the sourceID is unique and static so use this.
let ieeeAddr = payload.sourceID.toString(16);
ieeeAddr = `0x${'0'.repeat(16 - ieeeAddr.length)}${ieeeAddr}`;

// Green power devices dont' have a modelID, create a modelID based on the deviceID (=type)
const modelID = `GreenPower_${payload.deviceID}`;

let device = Device.byIeeeAddr(ieeeAddr);
if (!device) {
debug.log(`New green power device '${ieeeAddr}' joined`);
debug.log(`Creating device '${ieeeAddr}'`);
device = Device.create(
'GreenPower', ieeeAddr, payload.networkAddress, null,
undefined, undefined, modelID, true, [],
);
device.save();

const deviceJoinedPayload: Events.DeviceJoinedPayload = {device};
this.emit(Events.Events.deviceJoined, deviceJoinedPayload);

const deviceInterviewPayload: Events.DeviceInterviewPayload = {status: 'successful', device};
this.emit(Events.Events.deviceInterview, deviceInterviewPayload);
}
}

private async onDeviceJoined(payload: AdapterEvents.DeviceJoinedPayload): Promise<void> {
debug.log(`Device '${payload.ieeeAddr}' joined`);

Expand All @@ -356,7 +392,7 @@ class Controller extends events.EventEmitter {
debug.log(`Creating device '${payload.ieeeAddr}'`);
device = Device.create(
undefined, payload.ieeeAddr, payload.networkAddress, undefined,
undefined, undefined, undefined, []
undefined, undefined, undefined, false, []
);

const eventData: Events.DeviceJoinedPayload = {device};
Expand Down Expand Up @@ -410,10 +446,13 @@ class Controller extends events.EventEmitter {
}
debug.log(`Received '${dataType}' data '${JSON.stringify(logDataPayload)}'`);

if (this.isZclDataPayload(dataPayload, 'zcl') && dataPayload.frame &&
dataPayload.frame.Cluster.name === 'touchlink') {
// This is handled by touchlink
return;
if (this.isZclDataPayload(dataPayload, dataType)) {
if (dataPayload.frame.Cluster.name === 'touchlink') {
// This is handled by touchlink
return;
} else if (dataPayload.frame.Cluster.name === 'greenPower') {
this.greenPower.onZclGreenPowerData(dataPayload);
}
}

const device = typeof dataPayload.address === 'string' ?
Expand Down
3 changes: 2 additions & 1 deletion src/controller/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface DeviceLeavePayload {
}

const CommandsLookup: {[s: string]: MessagePayloadType} = {
'notification': 'commandNotification',
'on': 'commandOn',
'offWithEffect': 'commandOffWithEffect',
'step': 'commandStep',
Expand Down Expand Up @@ -79,7 +80,7 @@ type MessagePayloadType =
'commandRecall' | 'commandArm' | 'commandPanic' | 'commandEmergency' | 'commandColorLoopSet' |
'commandOperationEventNotification' | 'commandStatusChangeNotification' | 'commandEnhancedMoveToHueAndSaturation' |
'commandUpOpen' | 'commandDownClose' | 'commandMoveToLevel' | 'commandMoveColorTemp' | 'commandGetData' |
'commandSetDataResponse' | 'commandGetWeeklyScheduleRsp' | 'commandQueryNextImageRequest';
'commandSetDataResponse' | 'commandGetWeeklyScheduleRsp' | 'commandQueryNextImageRequest' | 'commandNotification';

interface MessagePayload {
type: MessagePayloadType;
Expand Down
92 changes: 92 additions & 0 deletions src/controller/greenPower.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Adapter, Events as AdapterEvents} from '../adapter';
import * as Zcl from '../zcl';
import crypto from 'crypto';
import ZclTransactionSequenceNumber from './helpers/zclTransactionSequenceNumber';
import events from 'events';
import {GreenPowerEvents, GreenPowerDeviceJoinedPayload} from './tstype';

const zigBeeLinkKey = Buffer.from([
0x5A, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6C, 0x6C, 0x69, 0x61, 0x6E, 0x63, 0x65, 0x30, 0x39
]);

class GreenPower extends events.EventEmitter {
private adapter: Adapter;

public constructor(adapter: Adapter) {
super();
this.adapter = adapter;
}

private encryptSecurityKey(sourceID: number, securityKey: Buffer): Buffer {
const sourceIDInBytes = Buffer.from([
(sourceID & 0x000000ff),
(sourceID & 0x0000ff00) >> 8,
(sourceID & 0x00ff0000) >> 16,
(sourceID & 0xff000000) >> 24]
);


const nonce = Buffer.alloc(13);
for (let i = 0; i < 3; i++)
{
for (let j = 0; j < 4; j++)
{
nonce[4 * i + j] = sourceIDInBytes[j];
}
}
nonce[12] = 0x05;

const cipher = crypto.createCipheriv('aes-128-ccm', zigBeeLinkKey, nonce, {authTagLength: 16});
const encrypted = cipher.update(securityKey);
return Buffer.concat([encrypted, cipher.final()]);
}

public async onZclGreenPowerData(dataPayload: AdapterEvents.ZclDataPayload): Promise<void> {
if (dataPayload.frame.getCommand().name === 'commisioningNotification' &&
typeof dataPayload.address === 'number') {
const key = this.encryptSecurityKey(
dataPayload.frame.Payload.srcID, dataPayload.frame.Payload.commandFrame.securityKey
);

const payload = {
options: 0x00e548,
srcID: dataPayload.frame.Payload.srcID,
sinkGroupID: this.adapter.greenPowerGroup,
deviceID: dataPayload.frame.Payload.commandFrame.deviceID,
frameCounter: dataPayload.frame.Payload.commandFrame.outgoingCounter,
gpdKey: [...key],
};

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'pairing', 33, payload
);

await this.adapter.sendZclFrameToAll(242, frame, 242);

const eventData: GreenPowerDeviceJoinedPayload = {
sourceID: dataPayload.frame.Payload.srcID,
deviceID: dataPayload.frame.Payload.commandFrame.deviceID,
networkAddress: dataPayload.address,
};

this.emit(GreenPowerEvents.deviceJoined, eventData);
}
}

public async permitJoin(time: number): Promise<void> {
const payload = {
options: 0x0b,
commisioningWindow: time,
};

const frame = Zcl.ZclFrame.create(
Zcl.FrameType.SPECIFIC, Zcl.Direction.SERVER_TO_CLIENT, true,
null, ZclTransactionSequenceNumber.next(), 'commisioningMode', 33, payload
);

await this.adapter.sendZclFrameToAll(242, frame, 242);
}
}

export default GreenPower;
14 changes: 7 additions & 7 deletions src/controller/model/device.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {KeyValue, DatabaseEntry, DeviceType} from '../tstype';
import {TsType as AdapterTsType, Events as AdapterEvents} from '../../adapter';
import {Events as AdapterEvents} from '../../adapter';
import Endpoint from './endpoint';
import Entity from './entity';
import {Wait} from '../../utils';
Expand Down Expand Up @@ -260,7 +260,7 @@ class Device extends Entity {
private static loadFromDatabaseIfNecessary(): void {
if (!Device.devices) {
Device.devices = {};
const entries = Entity.database.getEntries(['Coordinator', 'EndDevice', 'Router']);
const entries = Entity.database.getEntries(['Coordinator', 'EndDevice', 'Router', 'GreenPower']);
for (const entry of entries) {
const device = Device.fromDatabaseEntry(entry);
Device.devices[device.ieeeAddr] = device;
Expand Down Expand Up @@ -289,12 +289,12 @@ class Device extends Entity {
}

public static create(
type: AdapterTsType.DeviceType, ieeeAddr: string, networkAddress: number,
type: DeviceType, ieeeAddr: string, networkAddress: number,
manufacturerID: number, manufacturerName: string,
powerSource: string, modelID: string,
powerSource: string, modelID: string, interviewCompleted: boolean,
endpoints: {
ID: number; profileID: number; deviceID: number; inputClusters: number[]; outputClusters: number[];
}[]
}[],
): Device {
Device.loadFromDatabaseIfNecessary();
if (Device.devices[ieeeAddr]) {
Expand All @@ -310,8 +310,8 @@ class Device extends Entity {
const ID = Entity.database.newID();
const device = new Device(
ID, type, ieeeAddr, networkAddress, manufacturerID, endpointsMapped, manufacturerName,
powerSource, modelID, undefined, undefined, undefined, undefined, undefined, undefined, false, {},
null,
powerSource, modelID, undefined, undefined, undefined, undefined, undefined, undefined,
interviewCompleted, {}, null,
);

Entity.database.insert(device.toDatabaseEntry());
Expand Down
14 changes: 12 additions & 2 deletions src/controller/tstype.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// eslint-disable-next-line
interface KeyValue {[s: string]: any};

type DeviceType = 'Coordinator' | 'Router' | 'EndDevice' | 'Unknown';
type DeviceType = 'Coordinator' | 'Router' | 'EndDevice' | 'Unknown' | 'GreenPower';

type EntityType = DeviceType | 'Group';

Expand All @@ -12,6 +12,16 @@ interface DatabaseEntry {
[s: string]: any;
}

enum GreenPowerEvents {
deviceJoined = "deviceJoined",
}

interface GreenPowerDeviceJoinedPayload {
sourceID: number;
deviceID: number;
networkAddress: number;
}

export {
KeyValue, DatabaseEntry, EntityType, DeviceType,
KeyValue, DatabaseEntry, EntityType, DeviceType, GreenPowerEvents, GreenPowerDeviceJoinedPayload,
};
Loading

0 comments on commit 093af45

Please sign in to comment.