UNPKG

zigbee-herdsman-zigate

Version:

An open source ZigBee gateway solution with node.js.

489 lines 25 kB
"use strict"; 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