diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index 54521be2..67b3f012 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -17,6 +17,7 @@ export default class Availability extends Extension { private retrieveStateDebouncers: {[s: string]: () => void} = {}; private pingQueue: Device[] = []; private pingQueueExecuting = false; + private stopped = false; private getTimeout(device: Device): number { if (typeof device.options.availability === 'object' && device.options.availability?.timeout != null) { @@ -92,6 +93,11 @@ export default class Availability extends Extension { } } + if (this.stopped) { + // Exit here to avoid triggering any follow-up activity (e.g., re-queuing another ping attempt). + return; + } + this.publishAvailability(device, !pingedSuccessfully); this.resetTimer(device); this.removeFromPingQueue(device); @@ -103,6 +109,10 @@ export default class Availability extends Extension { } override async start(): Promise { + if (this.stopped) { + throw new Error('This extension cannot be restarted.'); + } + this.eventBus.onEntityRenamed(this, (data) => { if (utils.isAvailabilityEnabledForEntity(data.entity, settings.get())) { this.mqtt.publish(`${data.from}/availability`, null, {retain: true, qos: 1}); @@ -183,8 +193,10 @@ export default class Availability extends Extension { } override async stop(): Promise { + this.stopped = true; + this.pingQueue = []; Object.values(this.timers).forEach((t) => clearTimeout(t)); - super.stop(); + await super.stop(); } private retrieveState(device: Device): void { diff --git a/test/availability.test.js b/test/availability.test.js index c8fa6f95..9103a858 100644 --- a/test/availability.test.js +++ b/test/availability.test.js @@ -6,6 +6,7 @@ import zigbeeHerdsman from './stub/zigbeeHerdsman'; import utils from '../lib/util/utils'; import * as settings from '../lib/util/settings'; import Controller from '../lib/controller'; +import Availability from '../lib/extension/availability'; import flushPromises from './lib/flushPromises'; import stringify from 'json-stable-stringify-without-jsonify'; @@ -344,4 +345,28 @@ describe('Availability', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', 'online', {retain: true, qos: 1}, expect.any(Function)); }); + + it('Should clear the ping queue on stop', async () => { + const availability = controller.extensions.find((extension) => extension instanceof Availability); + const publishAvailabilitySpy = jest.spyOn(availability, 'publishAvailability'); + + devices.bulb_color.zh = { ping: jest.fn().mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 1000)))}; + availability.addToPingQueue(devices.bulb_color); + availability.addToPingQueue(devices.bulb_color_2); + + await availability.stop(); + await advancedTime(utils.minutes(1)); + + expect(availability.pingQueue).toEqual([]); + // Validate the stop-interrupt implicitly by checking that it prevents further function invocations + expect(publishAvailabilitySpy).not.toHaveBeenCalled(); + }); + + it('Should prevent instance restart', async () => { + const availability = controller.extensions.find((extension) => extension instanceof Availability); + + await availability.stop(); + + await expect(() => availability.start()).rejects.toThrowError(); + }); });