Skip to content

Instantly share code, notes, and snippets.

@Koenkk
Created November 26, 2019 19:30
Show Gist options
  • Save Koenkk/3aeb003a91415f2b65049a1b59aec3e5 to your computer and use it in GitHub Desktop.
Save Koenkk/3aeb003a91415f2b65049a1b59aec3e5 to your computer and use it in GitHub Desktop.
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
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) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const endpoint_1 = __importDefault(require("./endpoint"));
const entity_1 = __importDefault(require("./entity"));
const utils_1 = require("../../utils");
const debug_1 = __importDefault(require("debug"));
const Zcl = __importStar(require("../../zcl"));
const debug = debug_1.default('zigbee-herdsman:controller:device');
class Device extends entity_1.default {
constructor(ID, type, ieeeAddr, networkAddress, manufacturerID, endpoints, manufacturerName, powerSource, modelID, applicationVersion, stackVersion, zclVersion, hardwareVersion, dateCode, softwareBuildID, interviewCompleted, meta) {
super();
this.ID = ID;
this._type = type;
this.ieeeAddr = ieeeAddr;
this._networkAddress = networkAddress;
this._manufacturerID = manufacturerID;
this._endpoints = endpoints;
this._manufacturerName = manufacturerName;
this._powerSource = powerSource;
this._modelID = modelID;
this._applicationVersion = applicationVersion;
this._stackVersion = stackVersion;
this._zclVersion = zclVersion;
this.hardwareVersion = hardwareVersion;
this._dateCode = dateCode;
this._softwareBuildID = softwareBuildID;
this._interviewCompleted = interviewCompleted;
this._interviewing = false;
this.meta = meta;
this._lastSeen = null;
}
// Getters/setters
get applicationVersion() { return this._applicationVersion; }
set applicationVersion(applicationVersion) { this._applicationVersion = applicationVersion; }
get endpoints() { return this._endpoints; }
get interviewCompleted() { return this._interviewCompleted; }
get interviewing() { return this._interviewing; }
get lastSeen() { return this._lastSeen; }
get manufacturerID() { return this._manufacturerID; }
get type() { return this._type; }
get dateCode() { return this._dateCode; }
set dateCode(dateCode) { this._dateCode = dateCode; }
set hardwareVersion(hardwareVersion) { this._hardwareVersion = hardwareVersion; }
get hardwareVersion() { return this._hardwareVersion; }
get manufacturerName() { return this._manufacturerName; }
set manufacturerName(manufacturerName) { this._manufacturerName = manufacturerName; }
set modelID(modelID) { this._modelID = modelID; }
get modelID() { return this._modelID; }
get networkAddress() { return this._networkAddress; }
set networkAddress(networkAddress) { this._networkAddress = networkAddress; }
get powerSource() { return this._powerSource; }
set powerSource(powerSource) {
this._powerSource = typeof powerSource === 'number' ? Zcl.PowerSource[powerSource] : powerSource;
}
get softwareBuildID() { return this._softwareBuildID; }
set softwareBuildID(softwareBuildID) { this._softwareBuildID = softwareBuildID; }
get stackVersion() { return this._stackVersion; }
set stackVersion(stackVersion) { this._stackVersion = stackVersion; }
get zclVersion() { return this._zclVersion; }
set zclVersion(zclVersion) { this._zclVersion = zclVersion; }
createEndpoint(ID) {
return __awaiter(this, void 0, void 0, function* () {
if (this.getEndpoint(ID)) {
throw new Error(`Device '${this.ieeeAddr}' already has an endpoint '${ID}'`);
}
const endpoint = endpoint_1.default.create(ID, undefined, undefined, [], [], this.networkAddress, this.ieeeAddr);
this.endpoints.push(endpoint);
this.save();
return endpoint;
});
}
getEndpoint(ID) {
return this.endpoints.find((e) => e.ID === ID);
}
updateLastSeen() {
this._lastSeen = Date.now();
}
/*
* CRUD
*/
static fromDatabaseEntry(entry) {
const networkAddress = entry.nwkAddr;
const ieeeAddr = entry.ieeeAddr;
const endpoints = Object.values(entry.endpoints).map((e) => {
return endpoint_1.default.fromDatabaseRecord(e, networkAddress, ieeeAddr);
});
const meta = entry.meta ? entry.meta : {};
if (entry.type === 'Group') {
throw new Error('Cannot load device from group');
}
return new Device(entry.id, entry.type, ieeeAddr, networkAddress, entry.manufId, endpoints, entry.manufName, entry.powerSource, entry.modelId, entry.appVersion, entry.stackVersion, entry.zclVersion, entry.hwVersion, entry.dateCode, entry.swBuildId, entry.interviewCompleted, meta);
}
toDatabaseEntry() {
const epList = this.endpoints.map((e) => e.ID);
const endpoints = {};
for (const endpoint of this.endpoints) {
endpoints[endpoint.ID] = endpoint.toDatabaseRecord();
}
return {
id: this.ID, type: this.type, ieeeAddr: this.ieeeAddr, nwkAddr: this.networkAddress,
manufId: this.manufacturerID, manufName: this.manufacturerName, powerSource: this.powerSource,
modelId: this.modelID, epList, endpoints, appVersion: this.applicationVersion,
stackVersion: this.stackVersion, hwVersion: this.hardwareVersion, dateCode: this.dateCode,
swBuildId: this.softwareBuildID, zclVersion: this.zclVersion, interviewCompleted: this.interviewCompleted,
meta: this.meta,
};
}
save() {
entity_1.default.database.update(this.toDatabaseEntry());
}
static loadFromDatabaseIfNecessary() {
if (!Device.devices) {
Device.devices = {};
const entries = entity_1.default.database.getEntries(['Coordinator', 'EndDevice', 'Router']);
for (const entry of entries) {
const device = Device.fromDatabaseEntry(entry);
Device.devices[device.ieeeAddr] = device;
}
}
}
static byIeeeAddr(ieeeAddr) {
Device.loadFromDatabaseIfNecessary();
return Device.devices[ieeeAddr];
}
static byNetworkAddress(networkAddress) {
Device.loadFromDatabaseIfNecessary();
return Object.values(Device.devices).find(d => d.networkAddress === networkAddress);
}
static byType(type) {
Device.loadFromDatabaseIfNecessary();
return Object.values(Device.devices).filter(d => d.type === type);
}
static all() {
Device.loadFromDatabaseIfNecessary();
return Object.values(Device.devices);
}
static create(type, ieeeAddr, networkAddress, manufacturerID, manufacturerName, powerSource, modelID, endpoints) {
Device.loadFromDatabaseIfNecessary();
if (Device.devices[ieeeAddr]) {
throw new Error(`Device with ieeeAddr '${ieeeAddr}' already exists`);
}
const endpointsMapped = endpoints.map((e) => {
return endpoint_1.default.create(e.ID, e.profileID, e.deviceID, e.inputClusters, e.outputClusters, networkAddress, ieeeAddr);
});
const ID = entity_1.default.database.newID();
const device = new Device(ID, type, ieeeAddr, networkAddress, manufacturerID, endpointsMapped, manufacturerName, powerSource, modelID, undefined, undefined, undefined, undefined, undefined, undefined, false, {});
entity_1.default.database.insert(device.toDatabaseEntry());
Device.devices[device.ieeeAddr] = device;
return device;
}
/*
* Zigbee functions
*/
interview() {
return __awaiter(this, void 0, void 0, function* () {
if (this.interviewing) {
const message = `Interview - interview already in progress for '${this.ieeeAddr}'`;
debug(message);
throw new Error(message);
}
let error;
this._interviewing = true;
debug(`Interview - start device '${this.ieeeAddr}'`);
try {
yield this.interviewInternal();
debug(`Interview - completed for device '${this.ieeeAddr}'`);
this._interviewCompleted = true;
}
catch (e) {
// Some devices, e.g. Xiaomi end devices have a different interview procedure, after pairing they
// report it's modelID trough a readResponse. The readResponse is received by the controller and set
// on the device.
const lookup = {
'lumi\..*': {
type: 'EndDevice', manufacturerID: 4151, manufacturerName: 'LUMI', powerSource: 'Battery'
},
'TERNCY-PP01': {
type: 'EndDevice', manufacturerID: 4648, manufacturerName: 'TERNCY', powerSource: 'Battery'
},
};
const match = Object.keys(lookup).find((key) => this.modelID && this.modelID.match(key));
if (match) {
const info = lookup[match];
debug(`Interview procedure failed but got modelID matching '${match}', assuming interview succeeded`);
this._type = info.type;
this._manufacturerID = info.manufacturerID;
this._manufacturerName = info.manufacturerName;
this._powerSource = info.powerSource;
this._interviewing = false;
this._interviewCompleted = true;
this.save();
}
else {
debug(`Interview - failed for device '${this.ieeeAddr}' with error '${e.stack}'`);
error = e;
}
}
finally {
this._interviewing = false;
this.save();
}
if (error) {
throw error;
}
});
}
interviewInternal() {
return __awaiter(this, void 0, void 0, function* () {
const nodeDescriptorQuery = () => __awaiter(this, void 0, void 0, function* () {
const nodeDescriptor = yield entity_1.default.adapter.nodeDescriptor(this.networkAddress);
this._manufacturerID = nodeDescriptor.manufacturerCode;
this._type = nodeDescriptor.type;
this.save();
debug(`Interview - got node descriptor for device '${this.ieeeAddr}'`);
});
try {
yield nodeDescriptorQuery();
}
catch (error) {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug(`Interview - 1 node descriptor request failed for '${this.ieeeAddr}', retrying...`);
try {
yield nodeDescriptorQuery();
}
catch (error) {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug(`Interview - 2 node descriptor request failed for '${this.ieeeAddr}', retrying...`);
try {
yield nodeDescriptorQuery();
}
catch (error) {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug(`Interview - 3 node descriptor request failed for '${this.ieeeAddr}', retrying...`);
try {
yield nodeDescriptorQuery();
}
catch (error) {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug(`Interview - 4 node descriptor request failed for '${this.ieeeAddr}', retrying...`);
try {
yield nodeDescriptorQuery();
}
catch (error) {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug(`Interview - 5 node descriptor request failed for '${this.ieeeAddr}', retrying...`);
yield nodeDescriptorQuery();
}
}
}
}
}
const activeEndpoints = yield entity_1.default.adapter.activeEndpoints(this.networkAddress);
// Some devices, e.g. TERNCY return endpoint 0 in the active endpoints request.
// This is not a valid endpoint number according to the ZCL, requesting a simple descriptor will result
// into an error. Therefore we filter it, more info: https://github.com/Koenkk/zigbee-herdsman/issues/82
this._endpoints = activeEndpoints.endpoints.filter((e) => e !== 0).map((e) => {
return endpoint_1.default.create(e, undefined, undefined, [], [], this.networkAddress, this.ieeeAddr);
});
this.save();
debug(`Interview - got active endpoints for device '${this.ieeeAddr}'`);
for (const endpoint of this.endpoints) {
const simpleDescriptor = yield entity_1.default.adapter.simpleDescriptor(this.networkAddress, endpoint.ID);
endpoint.profileID = simpleDescriptor.profileID;
endpoint.deviceID = simpleDescriptor.deviceID;
endpoint.inputClusters = simpleDescriptor.inputClusters;
endpoint.outputClusters = simpleDescriptor.outputClusters;
debug(`Interview - got simple descriptor for endpoint '${endpoint.ID}' device '${this.ieeeAddr}'`);
this.save();
}
if (this.endpoints.length !== 0) {
const endpoint = this.endpoints[0];
// Split into chunks of 3, otherwise some devices fail to respond.
for (const chunk of utils_1.ArraySplitChunks(Object.keys(Device.ReportablePropertiesMapping), 3)) {
const result = yield endpoint.read('genBasic', chunk);
for (const [key, value] of Object.entries(result)) {
Device.ReportablePropertiesMapping[key].set(value, this);
}
debug(`Interview - got '${chunk}' for device '${this.ieeeAddr}'`);
this.save();
}
}
else {
debug(`Interview - skip reading attributes because of no endpoint for device '${this.ieeeAddr}'`);
throw new Error(`Interview failed because of not endpiont ('${this.ieeeAddr}')`);
}
// Enroll IAS device
for (const endpoint of this.endpoints.filter((e) => e.supportsInputCluster('ssIasZone'))) {
debug(`Interview - ssIasZone enrolling '${this.ieeeAddr}' endpoint '${endpoint.ID}'`);
const coordinator = Device.byType('Coordinator')[0];
yield endpoint.write('ssIasZone', { 'iasCieAddr': coordinator.ieeeAddr });
// According to the spec, we should wait for an enrollRequest here, but the Bosch ISW-ZPR1 didn't send it.
yield utils_1.Wait(3000);
// Some devices don't do a defaultResponse
const disableDefaultResponse = this.manufacturerName === 'Konke' || this.manufacturerName.startsWith('_TYZB01_');
yield endpoint.command('ssIasZone', 'enrollRsp', { enrollrspcode: 0, zoneid: 23 }, { disableDefaultResponse });
debug(`Interview - successfully enrolled '${this.ieeeAddr}' endpoint '${endpoint.ID}'`);
}
});
}
removeFromNetwork() {
return __awaiter(this, void 0, void 0, function* () {
yield entity_1.default.adapter.removeDevice(this.networkAddress, this.ieeeAddr);
yield this.removeFromDatabase();
});
}
removeFromDatabase() {
return __awaiter(this, void 0, void 0, function* () {
Device.loadFromDatabaseIfNecessary();
entity_1.default.database.remove(this.ID);
delete Device.devices[this.ieeeAddr];
});
}
lqi() {
return __awaiter(this, void 0, void 0, function* () {
return entity_1.default.adapter.lqi(this.networkAddress);
});
}
routingTable() {
return __awaiter(this, void 0, void 0, function* () {
return entity_1.default.adapter.routingTable(this.networkAddress);
});
}
ping() {
return __awaiter(this, void 0, void 0, function* () {
// Zigbee does not have an official pining mechamism. Use a read request
// of a mandatory basic cluster attribute to keep it as lightweight as
// possible.
yield this.endpoints[0].read('genBasic', ['zclVersion']);
});
}
}
// This lookup contains all devices that are queried from the database, this is to ensure that always
// the same instance is returned.
Device.devices = null;
Device.ReportablePropertiesMapping = {
modelId: { key: 'modelID', set: (v, d) => { d.modelID = v; } },
manufacturerName: { key: 'manufacturerName', set: (v, d) => { d.manufacturerName = v; } },
powerSource: { key: 'powerSource', set: (v, d) => { d.powerSource = v; } },
zclVersion: { key: 'zclVersion', set: (v, d) => { d.zclVersion = v; } },
appVersion: { key: 'applicationVersion', set: (v, d) => { d.applicationVersion = v; } },
stackVersion: { key: 'stackVersion', set: (v, d) => { d.stackVersion = v; } },
hwVersion: { key: 'hardwareVersion', set: (v, d) => { d.hardwareVersion = v; } },
dateCode: { key: 'dateCode', set: (v, d) => { d.dateCode = v; } },
swBuildId: { key: 'softwareBuildID', set: (v, d) => { d.softwareBuildID = v; } },
};
exports.default = Device;
//# sourceMappingURL=device.js.map
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment