diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index ed429a4c44..296c4a62d6 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -2389,7 +2389,6 @@ export class EmberAdapter extends Adapter { destinationAddressOrGroup as number, // not used with UNICAST_BINDING destinationEndpoint, // not used with MULTICAST_BINDING ); - console.log(zdoPayload); const [status, apsFrame] = await this.sendZDORequest( destinationNetworkAddress, Zdo.ClusterId.BIND_REQUEST, diff --git a/src/controller/controller.ts b/src/controller/controller.ts index a336ad43d1..44ef8a06c2 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -164,11 +164,11 @@ class Controller extends events.EventEmitter { } logger.debug('Clearing database...', NS); - for (const group of Group.all()) { + for (const group of Group.allIterator()) { group.removeFromDatabase(); } - for (const device of Device.all()) { + for (const device of Device.allIterator()) { device.removeFromDatabase(); } } @@ -334,14 +334,17 @@ class Controller extends events.EventEmitter { this.adapterDisconnected = true; } + + Device.resetCache(); + Group.resetCache(); } private databaseSave(): void { - for (const device of Device.all()) { + for (const device of Device.allIterator()) { device.save(false); } - for (const group of Group.all()) { + for (const group of Group.allIterator()) { group.save(false); } @@ -352,7 +355,7 @@ class Controller extends events.EventEmitter { this.databaseSave(); if (this.options.backupPath && (await this.adapter.supportsBackup())) { logger.debug('Creating coordinator backup', NS); - const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr)); + const backup = await this.adapter.backup(this.getDeviceIeeeAddresses()); const unifiedBackup = await BackupUtils.toUnifiedBackup(backup); const tmpBackupPath = this.options.backupPath + '.tmp'; fs.writeFileSync(tmpBackupPath, JSON.stringify(unifiedBackup, null, 2)); @@ -363,9 +366,14 @@ class Controller extends events.EventEmitter { public async coordinatorCheck(): Promise<{missingRouters: Device[]}> { if (await this.adapter.supportsBackup()) { - const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr)); + const backup = await this.adapter.backup(this.getDeviceIeeeAddresses()); const devicesInBackup = backup.devices.map((d) => `0x${d.ieeeAddress.toString('hex')}`); - const missingRouters = this.getDevices().filter((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr)); + const missingRouters = []; + + for (const device of this.getDevicesIterator((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr))) { + missingRouters.push(device); + } + return {missingRouters}; } else { throw new Error("Coordinator does not coordinator check because it doesn't support backups"); @@ -396,6 +404,13 @@ class Controller extends events.EventEmitter { return Device.all(); } + /** + * Get iterator for all devices + */ + public getDevicesIterator(predicate?: (value: Device) => boolean): Generator { + return Device.allIterator(predicate); + } + /** * Get all devices with a specific type */ @@ -417,6 +432,19 @@ class Controller extends events.EventEmitter { return Device.byNetworkAddress(networkAddress); } + /** + * Get IEEE address for all devices + */ + public getDeviceIeeeAddresses(): string[] { + const deviceIeeeAddresses = []; + + for (const device of Device.allIterator()) { + deviceIeeeAddresses.push(device.ieeeAddr); + } + + return deviceIeeeAddresses; + } + /** * Get group by ID */ @@ -431,6 +459,13 @@ class Controller extends events.EventEmitter { return Group.all(); } + /** + * Get iterator for all groups + */ + public getGroupsIterator(predicate?: (value: Group) => boolean): Generator { + return Group.allIterator(predicate); + } + /** * Create a Group */ diff --git a/src/controller/database.ts b/src/controller/database.ts index 33796c32ce..f8c054979c 100644 --- a/src/controller/database.ts +++ b/src/controller/database.ts @@ -20,15 +20,21 @@ class Database { const entries: {[id: number]: DatabaseEntry} = {}; if (fs.existsSync(path)) { - const rows = fs - .readFileSync(path, 'utf-8') - .split('\n') - .map((r) => r.trim()) - .filter((r) => r != ''); - for (const row of rows) { - const json = JSON.parse(row); - if (json.hasOwnProperty('id')) { - entries[json.id] = json; + const file = fs.readFileSync(path, 'utf-8'); + + for (const row of file.split('\n')) { + if (!row) { + continue; + } + + try { + const json = JSON.parse(row); + + if (json.id != undefined) { + entries[json.id] = json; + } + } catch (error) { + logger.error(`Corrupted database line, ignoring. ${error}`, NS); } } } @@ -36,42 +42,48 @@ class Database { return new Database(entries, path); } - public getEntries(type: EntityType[]): DatabaseEntry[] { - return Object.values(this.entries).filter((e) => type.includes(e.type)); + public *getEntriesIterator(type: EntityType[]): Generator { + for (const id in this.entries) { + const entry = this.entries[id]; + + if (type.includes(entry.type)) { + yield entry; + } + } } - public insert(DatabaseEntry: DatabaseEntry): void { - if (this.entries[DatabaseEntry.id]) { - throw new Error(`DatabaseEntry with ID '${DatabaseEntry.id}' already exists`); + public insert(databaseEntry: DatabaseEntry): void { + if (this.entries[databaseEntry.id]) { + throw new Error(`DatabaseEntry with ID '${databaseEntry.id}' already exists`); } - this.entries[DatabaseEntry.id] = DatabaseEntry; + this.entries[databaseEntry.id] = databaseEntry; this.write(); } - public update(DatabaseEntry: DatabaseEntry, write: boolean): void { - if (!this.entries[DatabaseEntry.id]) { - throw new Error(`DatabaseEntry with ID '${DatabaseEntry.id}' does not exist`); + public update(databaseEntry: DatabaseEntry, write: boolean): void { + if (!this.entries[databaseEntry.id]) { + throw new Error(`DatabaseEntry with ID '${databaseEntry.id}' does not exist`); } - this.entries[DatabaseEntry.id] = DatabaseEntry; + this.entries[databaseEntry.id] = databaseEntry; if (write) { this.write(); } } - public remove(ID: number): void { - if (!this.entries[ID]) { - throw new Error(`DatabaseEntry with ID '${ID}' does not exist`); + public remove(id: number): void { + if (!this.entries[id]) { + throw new Error(`DatabaseEntry with ID '${id}' does not exist`); } - delete this.entries[ID]; + delete this.entries[id]; this.write(); } - public has(ID: number): boolean { - return this.entries.hasOwnProperty(ID); + public has(id: number): boolean { + return Boolean(this.entries[id]); } public newID(): number { @@ -81,13 +93,15 @@ class Database { public write(): void { logger.debug(`Writing database to '${this.path}'`, NS); - const lines = []; - for (const DatabaseEntry of Object.values(this.entries)) { - const json = JSON.stringify(DatabaseEntry); - lines.push(json); + let lines = ''; + + for (const id in this.entries) { + lines += JSON.stringify(this.entries[id]) + `\n`; } + const tmpPath = this.path + '.tmp'; - fs.writeFileSync(tmpPath, lines.join('\n')); + + fs.writeFileSync(tmpPath, lines.slice(0, -1)); // remove last newline, no effect if empty string // Ensure file is on disk https://github.com/Koenkk/zigbee2mqtt/issues/11759 const fd = fs.openSync(tmpPath, 'r+'); fs.fsyncSync(fd); diff --git a/src/controller/model/device.ts b/src/controller/model/device.ts index 6127cff2cc..f1efd17d9c 100755 --- a/src/controller/model/device.ts +++ b/src/controller/model/device.ts @@ -57,7 +57,6 @@ class Device extends Entity { private _linkquality?: number; private _skipDefaultResponse: boolean; private _customReadResponse?: CustomReadResponse; - private _deleted: boolean; private _lastDefaultResponseSequenceNumber: number; private _checkinInterval: number; private _pendingRequestTimeout: number; @@ -92,7 +91,7 @@ class Device extends Entity { return this._manufacturerID; } get isDeleted(): boolean { - return this._deleted; + return Boolean(Device.deletedDevices[this.ieeeAddr]); } set type(type: DeviceType) { this._type = type; @@ -129,6 +128,7 @@ class Device extends Entity { } set networkAddress(networkAddress: number) { this._networkAddress = networkAddress; + for (const endpoint of this._endpoints) { endpoint.deviceNetworkAddress = networkAddress; } @@ -180,6 +180,7 @@ class Device extends Entity { } set checkinInterval(checkinInterval: number) { this._checkinInterval = checkinInterval; + this.resetPendingRequestTimeout(); } get pendingRequestTimeout(): number { @@ -196,7 +197,8 @@ class Device extends Entity { // This lookup contains all devices that are queried from the database, this is to ensure that always // the same instance is returned. - private static devices: {[ieeeAddr: string]: Device} = null; + private static devices: {[ieeeAddr: string]: Device} | null = null; + private static deletedDevices: {[ieeeAddr: string]: Device} = {}; public static readonly ReportablePropertiesMapping: { [s: string]: { @@ -496,12 +498,22 @@ class Device extends Entity { * CRUD */ + /** + * Reset runtime lookups. + */ + public static resetCache(): void { + Device.devices = null; + Device.deletedDevices = {}; + } + private static fromDatabaseEntry(entry: DatabaseEntry): Device { const networkAddress = entry.nwkAddr; const ieeeAddr = entry.ieeeAddr; - const endpoints = Object.values(entry.endpoints).map((e): Endpoint => { - return Endpoint.fromDatabaseRecord(e, networkAddress, ieeeAddr); - }); + const endpoints: Endpoint[] = []; + + for (const id in entry.endpoints) { + endpoints.push(Endpoint.fromDatabaseRecord(entry.endpoints[id], networkAddress, ieeeAddr)); + } const meta = entry.meta ? entry.meta : {}; @@ -550,6 +562,7 @@ class Device extends Entity { private toDatabaseEntry(): DatabaseEntry { const epList = this.endpoints.map((e): number => e.ID); const endpoints: KeyValue = {}; + for (const endpoint of this.endpoints) { endpoints[endpoint.ID] = endpoint.toDatabaseRecord(); } @@ -585,8 +598,8 @@ class Device extends Entity { private static loadFromDatabaseIfNecessary(): void { if (!Device.devices) { Device.devices = {}; - const entries = Entity.database.getEntries(['Coordinator', 'EndDevice', 'Router', 'GreenPower', 'Unknown']); - for (const entry of entries) { + + for (const entry of Entity.database.getEntriesIterator(['Coordinator', 'EndDevice', 'Router', 'GreenPower', 'Unknown'])) { const device = Device.fromDatabaseEntry(entry); Device.devices[device.ieeeAddr] = device; } @@ -601,28 +614,69 @@ class Device extends Entity { public static byIeeeAddr(ieeeAddr: string, includeDeleted: boolean = false): Device { Device.loadFromDatabaseIfNecessary(); - const device = Device.devices[ieeeAddr]; - return device?._deleted && !includeDeleted ? undefined : device; + + return includeDeleted ? (Device.deletedDevices[ieeeAddr] ?? Device.devices[ieeeAddr]) : Device.devices[ieeeAddr]; } public static byNetworkAddress(networkAddress: number, includeDeleted: boolean = false): Device { Device.loadFromDatabaseIfNecessary(); - return Object.values(Device.devices).find((d) => (includeDeleted || !d._deleted) && d.networkAddress === networkAddress); + + if (includeDeleted) { + for (const ieeeAddress in Device.deletedDevices) { + const device = Device.deletedDevices[ieeeAddress]; + + /* istanbul ignore else */ + if (device.networkAddress === networkAddress) { + return device; + } + } + } + + for (const ieeeAddress in Device.devices) { + const device = Device.devices[ieeeAddress]; + + /* istanbul ignore else */ + if (device.networkAddress === networkAddress) { + return device; + } + } } public static byType(type: DeviceType): Device[] { - return Device.all().filter((d) => d.type === type); + const devices: Device[] = []; + + for (const device of Device.allIterator((d) => d.type === type)) { + devices.push(device); + } + + return devices; } public static all(): Device[] { Device.loadFromDatabaseIfNecessary(); - return Object.values(Device.devices).filter((d) => !d._deleted); + return Object.values(Device.devices); + } + + public static *allIterator(predicate?: (value: Device) => boolean): Generator { + Device.loadFromDatabaseIfNecessary(); + + for (const ieeeAddr in Device.devices) { + const device = Device.devices[ieeeAddr]; + + if (!predicate || predicate(device)) { + yield device; + } + } } public undelete(interviewCompleted?: boolean): void { - assert(this._deleted, `Device '${this.ieeeAddr}' is not deleted`); - this._deleted = false; + assert(Device.deletedDevices[this.ieeeAddr], `Device '${this.ieeeAddr}' is not deleted`); + + Device.devices[this.ieeeAddr] = this; + delete Device.deletedDevices[this.ieeeAddr]; + this._interviewCompleted = interviewCompleted ?? this._interviewCompleted; + Entity.database.insert(this.toDatabaseEntry()); } @@ -644,8 +698,9 @@ class Device extends Entity { }[], ): Device { Device.loadFromDatabaseIfNecessary(); - if (Device.devices[ieeeAddr] && !Device.devices[ieeeAddr]._deleted) { - throw new Error(`Device with ieeeAddr '${ieeeAddr}' already exists`); + + if (Device.devices[ieeeAddr]) { + throw new Error(`Device with IEEE address '${ieeeAddr}' already exists`); } const endpointsMapped = endpoints.map((e): Endpoint => { @@ -1006,7 +1061,8 @@ class Device extends Entity { Entity.database.remove(this.ID); } - this._deleted = true; + Device.deletedDevices[this.ieeeAddr] = this; + delete Device.devices[this.ieeeAddr]; // Clear all data in case device joins again this._interviewCompleted = false; diff --git a/src/controller/model/endpoint.ts b/src/controller/model/endpoint.ts index 84aa4eb864..f4ff779f3b 100644 --- a/src/controller/model/endpoint.ts +++ b/src/controller/model/endpoint.ts @@ -213,13 +213,14 @@ class Endpoint extends Entity { public static fromDatabaseRecord(record: KeyValue, deviceNetworkAddress: number, deviceIeeeAddress: string): Endpoint { // Migrate attrs to attributes - for (const entry of Object.values(record.clusters).filter((e) => e.hasOwnProperty('attrs'))) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - entry.attributes = entry.attrs; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete entry.attrs; + for (const entryKey in record.clusters) { + const entry = record.clusters[entryKey]; + + /* istanbul ignore else */ + if (entry.attrs != undefined) { + entry.attributes = entry.attrs; + delete entry.attrs; + } } return new Endpoint( @@ -821,7 +822,7 @@ class Endpoint extends Entity { } public removeFromAllGroupsDatabase(): void { - for (const group of Group.all()) { + for (const group of Group.allIterator()) { if (group.hasMember(this)) { group.removeMember(this); } diff --git a/src/controller/model/group.ts b/src/controller/model/group.ts index be19529cfc..570039a040 100644 --- a/src/controller/model/group.ts +++ b/src/controller/model/group.ts @@ -30,7 +30,7 @@ class Group extends Entity { // This lookup contains all groups that are queried from the database, this is to ensure that always // the same instance is returned. - private static groups: {[groupID: number]: Group} = null; + private static groups: {[groupID: number]: Group} | null = null; private constructor(databaseID: number, groupID: number, members: Set, meta: KeyValue) { super(); @@ -44,10 +44,19 @@ class Group extends Entity { * CRUD */ + /** + * Reset runtime lookups. + */ + public static resetCache(): void { + Group.groups = null; + } + private static fromDatabaseEntry(entry: DatabaseEntry): Group { const members = new Set(); + for (const member of entry.members) { const device = Device.byIeeeAddr(member.deviceIeeeAddr); + if (device) { const endpoint = device.getEndpoint(member.endpointID); members.add(endpoint); @@ -58,9 +67,11 @@ class Group extends Entity { } private toDatabaseRecord(): DatabaseEntry { - const members = Array.from(this.members).map((member) => { - return {deviceIeeeAddr: member.getDevice().ieeeAddr, endpointID: member.ID}; - }); + const members: DatabaseEntry['members'] = []; + + for (const member of this.members) { + members.push({deviceIeeeAddr: member.getDevice().ieeeAddr, endpointID: member.ID}); + } return {id: this.databaseID, type: 'Group', groupID: this.groupID, members, meta: this.meta}; } @@ -68,8 +79,8 @@ class Group extends Entity { private static loadFromDatabaseIfNecessary(): void { if (!Group.groups) { Group.groups = {}; - const entries = Entity.database.getEntries(['Group']); - for (const entry of entries) { + + for (const entry of Entity.database.getEntriesIterator(['Group'])) { const group = Group.fromDatabaseEntry(entry); Group.groups[group.groupID] = group; } @@ -86,12 +97,27 @@ class Group extends Entity { return Object.values(Group.groups); } + public static *allIterator(predicate?: (value: Group) => boolean): Generator { + Group.loadFromDatabaseIfNecessary(); + + for (const ieeeAddr in Group.groups) { + const group = Group.groups[ieeeAddr]; + + /* istanbul ignore else */ + if (!predicate || predicate(group)) { + yield group; + } + } + } + public static create(groupID: number): Group { assert(typeof groupID === 'number', 'GroupID must be a number'); // Don't allow groupID 0, from the spec: // "Scene identifier 0x00, along with group identifier 0x0000, is reserved for the global scene used by the OnOff cluster" assert(groupID >= 1, 'GroupID must be at least 1'); + Group.loadFromDatabaseIfNecessary(); + if (Group.groups[groupID]) { throw new Error(`Group with groupID '${groupID}' already exists`); } @@ -148,6 +174,7 @@ class Group extends Entity { options = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER); const cluster = Zcl.Utils.getCluster(clusterKey, null, {}); const payload: {attrId: number; dataType: number; attrData: number | string | boolean}[] = []; + for (const [nameOrID, value] of Object.entries(attributes)) { if (cluster.hasAttribute(nameOrID)) { const attribute = cluster.getAttribute(nameOrID); @@ -175,6 +202,7 @@ class Group extends Entity { {}, options.reservedBits, ); + await Entity.adapter.sendZclFrameToGroup(this.groupID, frame, options.srcEndpoint); } catch (error) { error.message = `${log} failed (${error.message})`; @@ -187,6 +215,7 @@ class Group extends Entity { options = this.getOptionsWithDefaults(options, Zcl.Direction.CLIENT_TO_SERVER); const cluster = Zcl.Utils.getCluster(clusterKey, null, {}); const payload: {attrId: number}[] = []; + for (const attribute of attributes) { payload.push({attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID}); } @@ -237,6 +266,7 @@ class Group extends Entity { {}, options.reservedBits, ); + await Entity.adapter.sendZclFrameToGroup(this.groupID, frame, options.srcEndpoint); } catch (error) { error.message = `${log} failed (${error.message})`; @@ -246,14 +276,13 @@ class Group extends Entity { } private getOptionsWithDefaults(options: Options, direction: Zcl.Direction): Options { - const providedOptions = options || {}; return { direction, srcEndpoint: null, reservedBits: 0, manufacturerCode: null, transactionSequenceNumber: null, - ...providedOptions, + ...(options || {}), }; } } diff --git a/src/zspec/zcl/definition/foundation.ts b/src/zspec/zcl/definition/foundation.ts index d136fd34ce..f133e94044 100644 --- a/src/zspec/zcl/definition/foundation.ts +++ b/src/zspec/zcl/definition/foundation.ts @@ -28,7 +28,7 @@ export type FoundationCommandName = | 'discoverExt' | 'discoverExtRsp'; -interface FoundationDefinition { +export interface FoundationDefinition { ID: number; parseStrategy: 'repetitive' | 'flat' | 'oneof'; parameters: readonly ParameterDefinition[]; diff --git a/src/zspec/zcl/utils.ts b/src/zspec/zcl/utils.ts index 783cded1b3..e9b7545ece 100644 --- a/src/zspec/zcl/utils.ts +++ b/src/zspec/zcl/utils.ts @@ -1,7 +1,6 @@ import {Clusters} from './definition/cluster'; import {DataType, DataTypeClass} from './definition/enums'; -import {Foundation} from './definition/foundation'; -import {FoundationCommandName} from './definition/foundation'; +import {Foundation, FoundationDefinition, FoundationCommandName} from './definition/foundation'; import {Attribute, Cluster, ClusterDefinition, ClusterName, Command, CustomClusters} from './definition/tstype'; const DATA_TYPE_CLASS_DISCRETE = [ @@ -61,6 +60,13 @@ const DATA_TYPE_CLASS_ANALOG = [ DataType.UTC, ]; +const FOUNDATION_DISCOVER_RSP_IDS = [ + Foundation.discoverRsp.ID, + Foundation.discoverCommandsRsp.ID, + Foundation.discoverCommandsGenRsp.ID, + Foundation.discoverExtRsp.ID, +]; + export function getDataTypeClass(dataType: DataType): DataTypeClass { if (DATA_TYPE_CLASS_DISCRETE.includes(dataType)) { return DataTypeClass.DISCRETE; @@ -308,3 +314,17 @@ export function getGlobalCommand(key: number | string): Command { export function isClusterName(name: string): name is ClusterName { return name in Clusters; } + +export function getFoundationCommand(id: number): FoundationDefinition { + for (const commandName in Foundation) { + const command = Foundation[commandName as FoundationCommandName]; + + if (command.ID === id) { + return command; + } + } +} + +export function isFoundationDiscoverRsp(id: number): boolean { + return FOUNDATION_DISCOVER_RSP_IDS.includes(id); +} diff --git a/src/zspec/zcl/zclFrame.ts b/src/zspec/zcl/zclFrame.ts index 58b1028792..c15970c3e4 100644 --- a/src/zspec/zcl/zclFrame.ts +++ b/src/zspec/zcl/zclFrame.ts @@ -1,6 +1,6 @@ import {BuffaloZcl} from './buffaloZcl'; import {Direction, DataType, BuffaloZclDataType, FrameType, ParameterCondition} from './definition/enums'; -import {FoundationCommandName, Foundation} from './definition/foundation'; +import {FoundationCommandName} from './definition/foundation'; import {Status} from './definition/status'; import {BuffaloZclOptions, Cluster, Command, ClusterName, CustomClusters, ParameterDefinition} from './definition/tstype'; import * as Utils from './utils'; @@ -83,7 +83,7 @@ export class ZclFrame { } private writePayloadGlobal(buffalo: BuffaloZcl): void { - const command = Object.values(Foundation).find((c): boolean => c.ID === this.command.ID); + const command = Utils.getFoundationCommand(this.command.ID); if (command.parseStrategy === 'repetitive') { for (const entry of this.payload) { @@ -110,11 +110,7 @@ export class ZclFrame { /* istanbul ignore else */ if (command.parseStrategy === 'oneof') { /* istanbul ignore else */ - if ( - [Foundation.discoverRsp, Foundation.discoverCommandsRsp, Foundation.discoverCommandsGenRsp, Foundation.discoverExtRsp].includes( - command, - ) - ) { + if (Utils.isFoundationDiscoverRsp(command.ID)) { buffalo.writeUInt8(this.payload.discComplete); for (const entry of this.payload.attrInfos) { @@ -208,7 +204,7 @@ export class ZclFrame { } private static parsePayloadGlobal(header: ZclHeader, buffalo: BuffaloZcl): ZclPayload { - const command = Object.values(Foundation).find((c): boolean => c.ID === header.commandIdentifier); + const command = Utils.getFoundationCommand(header.commandIdentifier); if (command.parseStrategy === 'repetitive') { const payload = []; @@ -261,11 +257,7 @@ export class ZclFrame { /* istanbul ignore else */ if (command.parseStrategy === 'oneof') { /* istanbul ignore else */ - if ( - [Foundation.discoverRsp, Foundation.discoverCommandsRsp, Foundation.discoverCommandsGenRsp, Foundation.discoverExtRsp].includes( - command, - ) - ) { + if (Utils.isFoundationDiscoverRsp(command.ID)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload: {discComplete: number; attrInfos: {[k: string]: any}[]} = { discComplete: buffalo.readUInt8(), diff --git a/src/zspec/zdo/buffaloZdo.ts b/src/zspec/zdo/buffaloZdo.ts index 188faf75b1..e582830a86 100644 --- a/src/zspec/zdo/buffaloZdo.ts +++ b/src/zspec/zdo/buffaloZdo.ts @@ -705,7 +705,6 @@ export class BuffaloZdo extends Buffalo { const length = this.readUInt8() + 1; // add offset (spec quirk...) - console.log(this.position, length); // validation: invalid if not at least ${length} bytes to read if (!this.isMoreBy(length)) { throw new Error(`Malformed TLV. Invalid data length for tagId=${tagId}, expected ${length}.`); diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index dbec5afeb3..88e7f04cad 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -1531,7 +1531,6 @@ describe('Ember Adapter Layer', () => { it('Triggers watchdog counters', async () => { await jest.advanceTimersByTimeAsync(3610000); expect(mockEzspReadAndClearCounters).toHaveBeenCalledTimes(1); - console.log(loggerSpies.info.mock.calls); expect(loggerSpies.info).toHaveBeenCalledTimes(2); expect(loggerSpies.info.mock.calls[0][0]).toMatch(/[NCP COUNTERS]/); expect(loggerSpies.info.mock.calls[1][0]).toMatch(/[ASH COUNTERS]/); diff --git a/test/controller.test.ts b/test/controller.test.ts index bfdc8f1063..359bfae81a 100755 --- a/test/controller.test.ts +++ b/test/controller.test.ts @@ -600,13 +600,14 @@ jest.mock('../src/adapter/zigate/adapter/zigateAdapter', () => { }); }); -const getTempFile = (filename) => { - const tempPath = path.resolve('temp'); - if (!fs.existsSync(tempPath)) { - fs.mkdirSync(tempPath); +const TEMP_PATH = path.resolve('temp'); + +const getTempFile = (filename: string): string => { + if (!fs.existsSync(TEMP_PATH)) { + fs.mkdirSync(TEMP_PATH); } - return path.join(tempPath, filename); + return path.join(TEMP_PATH, filename); }; // Mock static methods @@ -683,6 +684,7 @@ describe('Controller', () => { afterAll(async () => { jest.useRealTimers(); + fs.rmSync(TEMP_PATH, {recursive: true, force: true}); }); beforeEach(async () => { @@ -697,8 +699,9 @@ describe('Controller', () => { enroll170 = true; options.network.channelList = [15]; Object.keys(events).forEach((key) => (events[key] = [])); - Device['devices'] = null; - Group['groups'] = null; + Device.resetCache(); + Group.resetCache(); + if (fs.existsSync(options.databasePath)) { fs.unlinkSync(options.databasePath); } @@ -784,6 +787,27 @@ describe('Controller', () => { expect(databaseSaveSpy).toHaveBeenCalledTimes(1); }); + it('Controller stop, should reset runtime lookups', async () => { + await controller.start(); + + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + await mockAdapterEvents['deviceJoined']({networkAddress: 128, ieeeAddr: '0x128'}); + await mockAdapterEvents['deviceLeave']({networkAddress: 128, ieeeAddr: '0x128'}); + controller.createGroup(1); + expect(Device.byIeeeAddr('0x129', false)).toBeInstanceOf(Device); + expect(Device.byIeeeAddr('0x128', true)).toBeInstanceOf(Device); + expect(Group.byGroupID(1)).toBeInstanceOf(Group); + + await controller.stop(); + + // @ts-expect-error private + expect(Device.devices).toStrictEqual(null); + // @ts-expect-error private + expect(Device.deletedDevices).toStrictEqual({}); + // @ts-expect-error private + expect(Group.groups).toStrictEqual(null); + }); + it('Controller start', async () => { await controller.start(); expect(mockAdapterStart).toHaveBeenCalledTimes(1); @@ -1777,6 +1801,66 @@ describe('Controller', () => { expect(mockAdapterGetNetworkParameters).toHaveBeenCalledTimes(1); }); + it('Iterates over all devices', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + let devices = 0; + + for (const device of controller.getDevicesIterator()) { + expect(device).toBeInstanceOf(Device); + + devices += 1; + } + + expect(devices).toStrictEqual(2); // + coordinator + }); + + it('Iterates over devices with predicate', async () => { + await controller.start(); + await mockAdapterEvents['deviceJoined']({networkAddress: 129, ieeeAddr: '0x129'}); + let devices = 0; + + for (const device of controller.getDevicesIterator((d) => d.networkAddress === 129)) { + expect(device).toBeInstanceOf(Device); + + devices += 1; + } + + expect(devices).toStrictEqual(1); + }); + + it('Iterates over all groups', async () => { + await controller.start(); + controller.createGroup(1); + controller.createGroup(2); + + let groups = 0; + + for (const group of controller.getGroupsIterator()) { + expect(group).toBeInstanceOf(Group); + + groups += 1; + } + + expect(groups).toStrictEqual(2); + }); + + it('Iterates over groups with predicate', async () => { + await controller.start(); + controller.createGroup(1); + controller.createGroup(2); + + let groups = 0; + + for (const group of controller.getGroupsIterator((d) => d.groupID === 1)) { + expect(group).toBeInstanceOf(Group); + + groups += 1; + } + + expect(groups).toStrictEqual(1); + }); + it('Join a device', async () => { await controller.start(); expect(databaseContents().includes('0x129')).toBeFalsy(); @@ -2070,6 +2154,7 @@ describe('Controller', () => { expect(events.deviceLeave.length).toBe(1); expect(events.deviceLeave[0]).toStrictEqual({ieeeAddr: '0x129'}); expect(controller.getDeviceByNetworkAddress(129)).toBeUndefined(); + expect(Device.byNetworkAddress(129, true)).toBeInstanceOf(Device); // leaves another time when not in database await mockAdapterEvents['deviceLeave']({networkAddress: 129, ieeeAddr: null}); @@ -2089,16 +2174,13 @@ describe('Controller', () => { expect(databaseContents().includes('0x129')).toBeTruthy(); expect(databaseContents().includes('groupID')).toBeTruthy(); await controller.stop(); + mockAdapterStart.mockReturnValueOnce('reset'); await controller.start(); expect(controller.getDevices().length).toBe(1); expect(controller.getDevicesByType('Coordinator')[0].type).toBe('Coordinator'); expect(controller.getDeviceByIeeeAddr('0x129')).toBeUndefined(); expect(controller.getGroupByID(1)).toBeUndefined(); - // Items are marked as delete but still appear as lines in database, therefore we need to restart once - // database will then remove deleted items. - await controller.stop(); - await controller.start(); expect(databaseContents().includes('0x129')).toBeFalsy(); expect(databaseContents().includes('groupID')).toBeFalsy(); }); @@ -4077,7 +4159,7 @@ describe('Controller', () => { expect(error).toStrictEqual(new Error("Device '0x129' already has an endpoint '1'")); }); - it('Throw error when device with ieeeAddr already exists', async () => { + it('Throw error when device with IEEE address already exists', async () => { await controller.start(); await mockAdapterEvents['deviceJoined']({networkAddress: 140, ieeeAddr: '0x129'}); let error; @@ -4086,7 +4168,7 @@ describe('Controller', () => { } catch (e) { error = e; } - expect(error).toStrictEqual(new Error("Device with ieeeAddr '0x129' already exists")); + expect(error).toStrictEqual(new Error("Device with IEEE address '0x129' already exists")); }); it('Should allow to set type', async () => { @@ -8310,7 +8392,6 @@ describe('Controller', () => { _modelID: 'GreenPower_2', _networkAddress: 0x71f8, _type: 'GreenPower', - _deleted: true, meta: {}, }); @@ -8359,7 +8440,6 @@ describe('Controller', () => { _modelID: 'GreenPower_2', _networkAddress: 0x71f8, _type: 'GreenPower', - _deleted: false, meta: {}, }); });