zigbee-herdsman-zigate
Version:
An open source ZigBee gateway solution with node.js.
489 lines • 25 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 };
};
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"));
/**
* @ignore
*/
const OneJanuary2000 = new Date('January 01, 2000 00:00:00 UTC+00:00').getTime();
const debug = {
error: debug_1.default('zigbee-herdsman:controller:device:error'),
log: debug_1.default('zigbee-herdsman:controller:device:log'),
};
class Device extends entity_1.default {
constructor(ID, type, ieeeAddr, networkAddress, manufacturerID, endpoints, manufacturerName, powerSource, modelID, applicationVersion, stackVersion, zclVersion, hardwareVersion, dateCode, softwareBuildID, interviewCompleted, meta, lastSeen) {
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 = lastSeen;
}
// Getters/setters
get ieeeAddr() { return this._ieeeAddr; }
set ieeeAddr(ieeeAddr) { this._ieeeAddr = ieeeAddr; }
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; }
set type(type) { this._type = type; }
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;
for (const endpoint of this._endpoints) {
endpoint.deviceNetworkAddress = 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);
}
// There might be multiple endpoints with same DeviceId but it is not supported and first endpoint is returned
getEndpointByDeviceType(deviceType) {
const deviceID = Zcl.EndpointDeviceType[deviceType];
return this.endpoints.find((d) => d.deviceID === deviceID);
}
updateLastSeen() {
this._lastSeen = Date.now();
}
onZclData(dataPayload, endpoint) {
return __awaiter(this, void 0, void 0, function* () {
const frame = dataPayload.frame;
// Respond to enroll requests
if (frame.isSpecific() && frame.isCluster('ssIasZone') && frame.isCommand('enrollReq')) {
debug.log(`IAS - '${this.ieeeAddr}' responding to enroll response`);
const payload = { enrollrspcode: 0, zoneid: 23 };
yield endpoint.command('ssIasZone', 'enrollRsp', payload, { disableDefaultResponse: true });
}
// Reponse to genTime reads
if (frame.isGlobal() && frame.isCluster('genTime') && frame.isCommand('read')) {
const time = Math.round(((new Date()).getTime() - OneJanuary2000) / 1000);
const response = {};
const values = {
timeStatus: 3,
time: time,
timeZone: ((new Date()).getTimezoneOffset() * -1) * 60,
localTime: time - (new Date()).getTimezoneOffset() * 60,
};
const cluster = Zcl.Utils.getCluster('genTime');
for (const entry of frame.Payload) {
const name = cluster.getAttribute(entry.attrId).name;
if (values.hasOwnProperty(name)) {
response[name] = values[name];
}
else {
debug.error(`'${this.ieeeAddr}' read unsupported attribute from genTime '${name}'`);
}
}
try {
yield endpoint.readResponse(frame.Cluster.ID, frame.Header.transactionSequenceNumber, response);
}
catch (error) {
debug.error(`genTime response to ${this.ieeeAddr} failed`);
}
}
// Send a default response if necessary.
const isDefaultResponse = frame.isGlobal() && frame.getCommand().name === 'defaultRsp';
const commandHasResponse = frame.getCommand().hasOwnProperty('response');
if (!frame.Header.frameControl.disableDefaultResponse && !isDefaultResponse && !commandHasResponse) {
try {
yield endpoint.defaultResponse(frame.getCommand().ID, 0, frame.Cluster.ID, frame.Header.transactionSequenceNumber);
}
catch (error) {
debug.error(`Default response to ${this.ieeeAddr} failed`);
}
}
});
}
/*
* 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, entry.lastSeen || null);
}
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, lastSeen: this.lastSeen,
};
}
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', 'GreenPower']);
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, interviewCompleted, 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, interviewCompleted, {}, null);
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.log(message);
throw new Error(message);
}
let error;
this._interviewing = true;
debug.log(`Interview - start device '${this.ieeeAddr}'`);
try {
yield this.interviewInternal();
debug.log(`Interview - completed for device '${this.ieeeAddr}'`);
this._interviewCompleted = true;
}
catch (e) {
if (this.interviewQuirks()) {
debug.log(`Interview - completed for device '${this.ieeeAddr}' because of quirks ('${e}')`);
}
else {
debug.log(`Interview - failed for device '${this.ieeeAddr}' with error '${e.stack}'`);
error = e;
}
}
finally {
this._interviewing = false;
this.save();
}
if (error) {
throw error;
}
});
}
interviewQuirks() {
// 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.log(`Interview procedure failed but got modelID matching '${match}', assuming interview succeeded`);
this._type = this._type || info.type;
this._manufacturerID = this._manufacturerID || info.manufacturerID;
this._manufacturerName = this._manufacturerName || info.manufacturerName;
this._powerSource = this._powerSource || info.powerSource;
this._interviewing = false;
this._interviewCompleted = true;
this.save();
return true;
}
else {
return false;
}
}
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.log(`Interview - got node descriptor for device '${this.ieeeAddr}'`);
});
let gotNodeDescriptor = false;
for (let attempt = 0; attempt < 6; attempt++) {
try {
yield nodeDescriptorQuery();
gotNodeDescriptor = true;
break;
}
catch (error) {
if (this.interviewQuirks()) {
debug.log(`Interview - completed for device '${this.ieeeAddr}' because of quirks ('${error}')`);
return;
}
else {
// Most of the times the first node descriptor query fails and the seconds one succeeds.
debug.log(`Interview - node descriptor request failed for '${this.ieeeAddr}', attempt ${attempt + 1}`);
}
}
}
if (!gotNodeDescriptor) {
throw new Error(`Interview failed because can not get node descriptor ('${this.ieeeAddr}')`);
}
// e.g. Xiaomi Aqara Opple devices fail to respond to the first active endpoints request, therefore try 2 times
// https://github.com/Koenkk/zigbee-herdsman/pull/103
let activeEndpoints;
for (let attempt = 0; attempt < 2; attempt++) {
try {
activeEndpoints = yield entity_1.default.adapter.activeEndpoints(this.networkAddress);
break;
}
catch (error) {
debug.log(`Interview - active endpoints request failed for '${this.ieeeAddr}', attempt ${attempt + 1}`);
}
}
if (!activeEndpoints) {
throw new Error(`Interview failed because can not get active endpoints ('${this.ieeeAddr}')`);
}
// Make sure that the endpoint are sorted.
activeEndpoints.endpoints.sort();
// 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.log(`Interview - got active endpoints for device '${this.ieeeAddr}'`);
debug.log(this.endpoints);
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.log(`Interview - got simple descriptor for endpoint '${endpoint.ID}' device '${this.ieeeAddr}'`);
this.save();
// Read attributes, nice to have but not required for succesfull pairing as most of the attributes
// are not mandatory in ZCL specification.
if (endpoint.supportsInputCluster('genBasic')) {
for (const [key, item] of Object.entries(Device.ReportablePropertiesMapping)) {
if (!this[item.key]) {
try {
const result = yield endpoint.read('genBasic', [key]);
item.set(result[key], this);
debug.log(`Interview - got '${item.key}' for device '${this.ieeeAddr}'`);
}
catch (error) {
debug.log(`Failed to read attribute '${item.key}' from endpoint '${endpoint.ID}' (${error})`);
}
}
}
}
}
// Enroll IAS device
for (const endpoint of this.endpoints.filter((e) => e.supportsInputCluster('ssIasZone'))) {
debug.log(`Interview - IAS - enrolling '${this.ieeeAddr}' endpoint '${endpoint.ID}'`);
const stateBefore = yield endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState']);
debug.log(`Interview - IAS - before enrolling state: '${JSON.stringify(stateBefore)}'`);
// Do not enroll when device has already been enrolled
const coordinator = Device.byType('Coordinator')[0];
if (stateBefore.zoneState !== 1 || stateBefore.iasCieAddr !== coordinator.ieeeAddr) {
debug.log(`Interview - IAS - not enrolled, enrolling`);
yield endpoint.write('ssIasZone', { 'iasCieAddr': coordinator.ieeeAddr });
debug.log(`Interview - IAS - wrote iasCieAddr`);
// According to the ZCL, after the iasCieAddr is written, the device will do an
// enroll request. This should be responded with an enroll response.
// As some devices send these enroll requests randomly (while the iasCieAddr has not been written yet)
// we always respond to these enroll requests in the onZclData() function.
// Therefore we don't have to do it here anymore.
// https://github.com/Koenkk/zigbee2mqtt/issues/3012
let enrolled = false;
for (let attempt = 0; attempt < 20; attempt++) {
yield utils_1.Wait(500);
const stateAfter = yield endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState']);
debug.log(`Interview - IAS - after enrolling state (${attempt}): '${JSON.stringify(stateAfter)}'`);
if (stateAfter.zoneState === 1) {
enrolled = true;
break;
}
}
if (enrolled) {
debug.log(`Interview - IAS successfully enrolled '${this.ieeeAddr}' endpoint '${endpoint.ID}'`);
}
else {
throw new Error(`Interview failed because of failed IAS enroll (zoneState didn't change ('${this.ieeeAddr}')`);
}
}
else {
debug.log(`Interview - IAS - already enrolled, skipping enroll`);
}
}
});
}
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();
for (const endpoint of this.endpoints) {
endpoint.removeFromAllGroupsDatabase();
}
if (entity_1.default.database.has(this.ID)) {
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'], { disableRecovery: true });
});
}
}
// 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