Last active
January 17, 2020 23:00
-
-
Save Koenkk/a45bdd9da8f8df31b10490663dae78ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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, 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 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, 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']); | |
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, {}, 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(message); | |
throw new Error(message); | |
} | |
let error; | |
this._interviewing = true; | |
debug(`Interview 2001172025 - start device '${this.ieeeAddr}'`); | |
try { | |
yield this.interviewInternal(); | |
debug(`Interview - completed for device '${this.ieeeAddr}'`); | |
this._interviewCompleted = true; | |
} | |
catch (e) { | |
if (this.interviewQuirks()) { | |
debug(`Interview - completed for device '${this.ieeeAddr}' because of quirks`); | |
} | |
else { | |
debug(`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(`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(); | |
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(`Interview - got node descriptor for device '${this.ieeeAddr}'`); | |
}); | |
for (let attempt = 0; attempt < 6; attempt++) { | |
try { | |
yield nodeDescriptorQuery(); | |
break; | |
} | |
catch (error) { | |
if (this.interviewQuirks()) { | |
debug(`Interview - completed for device '${this.ieeeAddr}' because of quirks`); | |
return; | |
} | |
else { | |
// Most of the times the first node descriptor query fails and the seconds one succeeds. | |
debug(`Interview - node descriptor request failed for '${this.ieeeAddr}', attempt ${attempt + 1}`); | |
} | |
} | |
} | |
// 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(`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 so that e.g. below modelID is read from the first endpoint. | |
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(`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), 2)) { | |
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.read('ssIasZone', ['iasCieAddr', 'zoneState']); | |
yield endpoint.write('ssIasZone', { 'iasCieAddr': coordinator.ieeeAddr }); | |
const disableDefaultResponse = this.manufacturerName === 'Konke' || this.manufacturerName.startsWith('_TYZB01_'); | |
// According to the spec, we should wait for an enrollRequest here, but the Bosch ISW-ZPR1 didn't send it. | |
setTimeout(() => {endpoint.read('ssIasZone', ['iasCieAddr', 'zoneState']);}, 2000); | |
yield endpoint.command('ssIasZone', 'enrollRsp', { enrollrspcode: 0, zoneid: 23 }, { disableDefaultResponse }); | |
yield utils_1.Wait(3000); | |
// Some devices don't do a defaultResponse | |
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