"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZStackAdapter = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const debounce_1 = __importDefault(require("debounce"));
const utils_1 = require("../../../utils");
const logger_1 = require("../../../utils/logger");
const ZSpec = __importStar(require("../../../zspec"));
const Zcl = __importStar(require("../../../zspec/zcl"));
const Zdo = __importStar(require("../../../zspec/zdo"));
const adapter_1 = __importDefault(require("../../adapter"));
const Constants = __importStar(require("../constants"));
const unpi_1 = require("../unpi");
const znp_1 = require("../znp");
const definition_1 = __importDefault(require("../znp/definition"));
const utils_2 = require("../znp/utils");
const endpoints_1 = require("./endpoints");
const manager_1 = require("./manager");
const tstype_1 = require("./tstype");
const NS = "zh:zstack";
const Subsystem = unpi_1.Constants.Subsystem;
const Type = unpi_1.Constants.Type;
const { ZnpCommandStatus, AddressMode } = Constants.COMMON;
const DataConfirmTimeout = 9999; // Not an actual code
class DataConfirmError extends Error {
    code;
    constructor(code) {
        const error = code === DataConfirmTimeout ? "'TIMEOUT'" : `'${ZnpCommandStatus[code]}' (0x${code.toString(16)})`;
        const message = `Data request failed with error: ${error}`;
        super(message);
        this.code = code;
    }
}
class ZStackAdapter extends adapter_1.default {
    deviceAnnounceRouteDiscoveryDebouncers;
    znp;
    // @ts-expect-error initialized in `start`
    adapterManager;
    transactionID;
    // @ts-expect-error initialized in `start`
    version;
    closing;
    // @ts-expect-error initialized in `start`
    queue;
    supportsLED;
    interpanLock;
    interpanEndpointRegistered;
    waitress;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = false;
        this.manufacturerID = Zcl.ManufacturerCode.TEXAS_INSTRUMENTS;
        // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
        this.znp = new znp_1.Znp(this.serialPortOptions.path, this.serialPortOptions.baudRate, this.serialPortOptions.rtscts);
        this.transactionID = 0;
        this.deviceAnnounceRouteDiscoveryDebouncers = new Map();
        this.interpanLock = false;
        this.interpanEndpointRegistered = false;
        this.closing = false;
        this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter);
        this.znp.on("received", this.onZnpRecieved.bind(this));
        this.znp.on("close", this.onZnpClose.bind(this));
    }
    /**
     * Adapter methods
     */
    async start() {
        await this.znp.open();
        const attempts = 3;
        for (let i = 0; i < attempts; i++) {
            try {
                await this.znp.request(Subsystem.SYS, "ping", { capabilities: 1 });
                break;
            }
            catch (e) {
                if (attempts - 1 === i) {
                    throw new Error(`Failed to connect to the adapter (${e})`);
                }
            }
        }
        // Old firmware did not support version, assume it's Z-Stack 1.2 for now.
        try {
            this.version = (await this.znp.requestWithReply(Subsystem.SYS, "version", {})).payload;
        }
        catch {
            logger_1.logger.debug("Failed to get zStack version, assuming 1.2", NS);
            this.version = { transportrev: 2, product: 0, majorrel: 2, minorrel: 0, maintrel: 0, revision: "" };
        }
        const concurrent = this.adapterOptions?.concurrent ? this.adapterOptions.concurrent : this.version.product === tstype_1.ZnpVersion.ZStack3x0 ? 16 : 2;
        logger_1.logger.debug(`Adapter concurrent: ${concurrent}`, NS);
        this.queue = new utils_1.Queue(concurrent);
        logger_1.logger.debug(`Detected znp version '${tstype_1.ZnpVersion[this.version.product]}' (${JSON.stringify(this.version)})`, NS);
        this.adapterManager = new manager_1.ZnpAdapterManager(this, this.znp, {
            backupPath: this.backupPath,
            version: this.version.product,
            greenPowerGroup: ZSpec.GP_GROUP_ID,
            networkOptions: this.networkOptions,
            adapterOptions: this.adapterOptions,
        });
        const startResult = this.adapterManager.start();
        if (this.adapterOptions.disableLED) {
            // Wait a bit for adapter to startup, otherwise led doesn't disable (tested with CC2531)
            await (0, utils_1.wait)(200);
            await this.setLED("disable");
        }
        if (this.adapterOptions.transmitPower != null) {
            await this.znp.request(Subsystem.SYS, "stackTune", { operation: 0, value: this.adapterOptions.transmitPower });
        }
        return await startResult;
    }
    async stop() {
        this.closing = true;
        await this.znp.close();
    }
    async getCoordinatorIEEE() {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            const deviceInfo = await this.znp.requestWithReply(Subsystem.UTIL, "getDeviceInfo", {});
            return deviceInfo.payload.ieeeaddr;
        });
    }
    async getCoordinatorVersion() {
        return await Promise.resolve({ type: tstype_1.ZnpVersion[this.version.product], meta: this.version });
    }
    async permitJoin(seconds, networkAddress) {
        const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
        // `authentication`: TC significance always 1 (zb specs)
        const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []);
        if (networkAddress === undefined) {
            await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true);
        }
        else {
            // NOTE: `sendZdo` takes care of adjusting the payload as appropriate based on `networkAddress === 0` or not
            const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false);
            /* v8 ignore start */
            if (!Zdo.Buffalo.checkStatus(result)) {
                // TODO: will disappear once moved upstream
                throw new Zdo.StatusError(result[0]);
            }
            /* v8 ignore stop */
        }
        await this.queue.execute(async () => {
            this.checkInterpanLock();
            await this.setLED(seconds === 0 ? "off" : "on");
        });
    }
    async reset(type) {
        if (type === "soft") {
            await this.znp.request(Subsystem.SYS, "resetReq", { type: Constants.SYS.resetType.SOFT });
        }
        else {
            await this.znp.request(Subsystem.SYS, "resetReq", { type: Constants.SYS.resetType.HARD });
        }
    }
    async setLED(action) {
        if (this.supportsLED == null) {
            // Only zStack3x0 with 20210430 and greater support LED
            const zStack3x0 = this.version.product === tstype_1.ZnpVersion.ZStack3x0;
            this.supportsLED = !zStack3x0 || (zStack3x0 && Number.parseInt(this.version.revision, 10) >= 20210430);
        }
        if (!this.supportsLED || (this.adapterOptions.disableLED && action !== "disable")) {
            return;
        }
        // Firmwares build on and after 20211029 should handle LED themselves
        const firmwareControlsLed = Number.parseInt(this.version.revision, 10) >= 20211029;
        const lookup = {
            disable: firmwareControlsLed ? { ledid: 0xff, mode: 5 } : { ledid: 3, mode: 0 },
            on: firmwareControlsLed ? null : { ledid: 3, mode: 1 },
            off: firmwareControlsLed ? null : { ledid: 3, mode: 0 },
        };
        const payload = lookup[action];
        if (payload) {
            await this.znp.request(Subsystem.UTIL, "ledControl", payload, undefined, 500).catch(() => {
                // We cannot 100% correctly determine if an adapter supports LED. E.g. the zStack 1.2 20190608
                // fw supports led on the CC2531 but not on the CC2530. Therefore if a led request fails never thrown
                // an error but instead mark the led as unsupported.
                // https://github.com/Koenkk/zigbee-herdsman/issues/377
                // https://github.com/Koenkk/zigbee2mqtt/issues/7693
                this.supportsLED = false;
            });
        }
    }
    async requestNetworkAddress(ieeeAddr) {
        /**
         * NOTE: There are cases where multiple nwkAddrRsp are recevied with different network addresses,
         * this is currently not handled, the first nwkAddrRsp is taken.
         */
        logger_1.logger.debug(`Request network address of '${ieeeAddr}'`, NS);
        const clusterId = Zdo.ClusterId.NETWORK_ADDRESS_REQUEST;
        const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, ieeeAddr, false, 0);
        const result = await this.sendZdoInternal(ieeeAddr, ZSpec.NULL_NODE_ID, clusterId, zdoPayload, false, true);
        if (Zdo.Buffalo.checkStatus(result)) {
            return result[1].nwkAddress;
            /* v8 ignore start */
        }
        // TODO: will disappear once moved upstream
        throw new Zdo.StatusError(result[0]);
        /* v8 ignore stop */
    }
    supportsAssocRemove() {
        return this.version.product === tstype_1.ZnpVersion.ZStack3x0 && Number.parseInt(this.version.revision, 10) >= 20200805;
    }
    supportsAssocAdd() {
        return this.version.product === tstype_1.ZnpVersion.ZStack3x0 && Number.parseInt(this.version.revision, 10) >= 20201026;
    }
    async discoverRoute(networkAddress, waitSettled = true) {
        logger_1.logger.debug(`Discovering route to ${networkAddress}`, NS);
        const payload = { dstAddr: networkAddress, options: 0, radius: Constants.AF.DEFAULT_RADIUS };
        await this.znp.request(Subsystem.ZDO, "extRouteDisc", payload);
        if (waitSettled) {
            await (0, utils_1.wait)(3000);
        }
    }
    /**
     * Retrieve all data for AF_INCOMING_MSG_EXT with huge data byte count.
     *
     * @param timestamp
     * @param length full data length
     *
     * @returns Buffer containing the full data or undefined on error
     */
    async dataRetrieveAll(timestamp, length) {
        const buf = Buffer.alloc(length);
        const blockSize = 240;
        const freeExtMessage = async () => {
            // A length of zero is special and triggers the freeing of
            // the corresponding incoming message.
            await this.znp.requestWithReply(Subsystem.AF, "dataRetrieve", { timestamp: timestamp, index: 0, length: 0 });
        };
        for (let index = 0; index < length; index += blockSize) {
            const chunkSize = Math.min(blockSize, length - index);
            const rsp = await this.znp.requestWithReply(Subsystem.AF, "dataRetrieve", {
                timestamp: timestamp,
                index: index,
                length: chunkSize,
            }, undefined);
            // 0x00 = afStatus_SUCCESS
            if (rsp.payload.status !== 0x00) {
                logger_1.logger.error(`dataRetrieve [timestamp: ${timestamp}, index: ${index}, chunkSize: ${chunkSize}] error status: ${rsp.payload.status}`, NS);
                await freeExtMessage();
                return undefined;
            }
            if (rsp.payload.length !== chunkSize) {
                logger_1.logger.error(`dataRetrieve length mismatch [${chunkSize} requested, ${rsp.payload.length} returned`, NS);
                await freeExtMessage();
                return undefined;
            }
            rsp.payload.data.copy(buf, index);
        }
        await freeExtMessage();
        return buf;
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        return await this.sendZdoInternal(ieeeAddress, networkAddress, clusterId, payload, disableResponse, false);
    }
    async sendZdoInternal(ieeeAddress, networkAddress, clusterId, payload, disableResponse, skipQueue) {
        const func = async () => {
            this.checkInterpanLock();
            // stack-specific requirements
            switch (clusterId) {
                case Zdo.ClusterId.PERMIT_JOINING_REQUEST: {
                    const finalPayload = Buffer.alloc(payload.length + 3);
                    finalPayload.writeUInt8(ZSpec.BroadcastAddress[networkAddress] ? AddressMode.ADDR_BROADCAST : AddressMode.ADDR_16BIT, 0);
                    // zstack uses AddressMode.ADDR_16BIT + ZSpec.BroadcastAddress.DEFAULT to signal "coordinator-only"
                    finalPayload.writeUInt16LE(networkAddress === 0 ? ZSpec.BroadcastAddress.DEFAULT : networkAddress, 1);
                    finalPayload.set(payload, 3);
                    payload = finalPayload;
                    break;
                }
                case Zdo.ClusterId.NWK_UPDATE_REQUEST: {
                    // extra zeroes for empty nwkManagerAddr if necessary
                    const zeroes = 9 - payload.length - 1; /* zstack doesn't have nwkUpdateId */
                    const finalPayload = Buffer.alloc(payload.length + 3 + zeroes);
                    finalPayload.writeUInt16LE(networkAddress, 0);
                    finalPayload.writeUInt8(ZSpec.BroadcastAddress[networkAddress] ? AddressMode.ADDR_BROADCAST : AddressMode.ADDR_16BIT, 2);
                    finalPayload.set(payload, 3);
                    payload = finalPayload;
                    break;
                }
                case Zdo.ClusterId.BIND_REQUEST:
                case Zdo.ClusterId.UNBIND_REQUEST: {
                    // extra zeroes for uint16 (in place of ieee when MULTICAST) and endpoint (not used when MULTICAST)
                    const zeroes = 21 - payload.length;
                    const finalPayload = Buffer.alloc(payload.length + 2 + zeroes);
                    finalPayload.writeUInt16LE(networkAddress, 0);
                    finalPayload.set(payload, 2);
                    payload = finalPayload;
                    break;
                }
                case Zdo.ClusterId.NETWORK_ADDRESS_REQUEST:
                case Zdo.ClusterId.IEEE_ADDRESS_REQUEST: {
                    // no modification necessary
                    break;
                }
                default: {
                    const finalPayload = Buffer.alloc(payload.length + 2);
                    finalPayload.writeUInt16LE(networkAddress, 0);
                    finalPayload.set(payload, 2);
                    payload = finalPayload;
                    break;
                }
            }
            let waiter;
            if (!disableResponse) {
                const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
                if (responseClusterId) {
                    const cmd = definition_1.default[Subsystem.ZDO].find((c) => (0, utils_2.isMtCmdAreqZdo)(c) && c.zdoClusterId === responseClusterId);
                    (0, node_assert_1.default)(cmd, `Response for ZDO cluster ID '${responseClusterId}' not supported.`);
                    waiter = this.znp.waitFor(unpi_1.Constants.Type.AREQ, Subsystem.ZDO, cmd.name, responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress, undefined, undefined);
                }
            }
            try {
                await this.znp.requestZdo(clusterId, payload, waiter?.ID);
            }
            catch (error) {
                if (clusterId === Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST) {
                    // Discover route when node descriptor request fails
                    // https://github.com/Koenkk/zigbee2mqtt/issues/3276
                    logger_1.logger.debug(`Discover route to '${networkAddress}' because node descriptor request failed`, NS);
                    await this.discoverRoute(networkAddress);
                    await this.znp.requestZdo(clusterId, payload, /* v8 ignore next */ waiter?.ID);
                }
                else {
                    throw error;
                }
            }
            if (waiter) {
                const response = await waiter.start().promise;
                return response.payload.zdo;
            }
        };
        return skipQueue ? await func() : await this.queue.execute(func, networkAddress);
    }
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint, profileId) {
        const srcEndpoint = this.selectSourceEndpoint(sourceEndpoint, profileId);
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, srcEndpoint, zclFrame, timeout, disableResponse, disableRecovery, 0, 0, false, false, false, undefined);
        }, networkAddress);
    }
    async sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore) {
        logger_1.logger.debug(`sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} ` +
            `(${responseAttempt},${dataRequestAttempt},${this.queue.count()})`, NS);
        let response = null;
        const command = zclFrame.command;
        if (command.response !== undefined && disableResponse === false) {
            response = this.waitForInternal(networkAddress, endpoint, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, command.response, timeout);
        }
        else if (!zclFrame.header.frameControl.disableDefaultResponse) {
            response = this.waitForInternal(networkAddress, endpoint, Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout);
        }
        const dataConfirmResult = await this.dataRequest(networkAddress, endpoint, sourceEndpoint, zclFrame.cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(), timeout);
        if (dataConfirmResult !== ZnpCommandStatus.SUCCESS) {
            // In case dataConfirm timesout (= null) or gives an error, try to recover
            logger_1.logger.debug(`Data confirm error (${ieeeAddr}:${networkAddress},${dataConfirmResult},${dataRequestAttempt})`, NS);
            if (response !== null)
                response.cancel();
            /**
             * In case we did an assocRemove in the previous attempt and it still fails after this, assume that the
             * coordinator is still the parent of the device (but for some reason the device is not available now).
             * Re-add the device to the assoc table, otherwise we will never be able to reach it anymore.
             */
            if (assocRemove && assocRestore && this.supportsAssocAdd()) {
                logger_1.logger.debug(`assocAdd(${assocRestore.ieeeadr})`, NS);
                await this.znp.request(Subsystem.UTIL, "assocAdd", assocRestore);
                assocRestore = undefined;
            }
            const recoverableErrors = [
                ZnpCommandStatus.NWK_NO_ROUTE,
                ZnpCommandStatus.MAC_NO_ACK,
                ZnpCommandStatus.MAC_CHANNEL_ACCESS_FAILURE,
                ZnpCommandStatus.MAC_TRANSACTION_EXPIRED,
                ZnpCommandStatus.BUFFER_FULL,
                ZnpCommandStatus.MAC_NO_RESOURCES,
            ];
            if (dataRequestAttempt >= 4 || !recoverableErrors.includes(dataConfirmResult) || disableRecovery) {
                throw new DataConfirmError(dataConfirmResult);
            }
            if (dataConfirmResult === ZnpCommandStatus.MAC_CHANNEL_ACCESS_FAILURE ||
                dataConfirmResult === ZnpCommandStatus.BUFFER_FULL ||
                dataConfirmResult === ZnpCommandStatus.MAC_NO_RESOURCES) {
                /**
                 * MAC_CHANNEL_ACCESS_FAILURE: When many commands at once are executed we can end up in a MAC
                 * channel access failure error. This is because there is too much traffic on the network.
                 * Retry this command once after a cooling down period.
                 * BUFFER_FULL: When many commands are executed at once the buffer can get full, wait
                 * some time and retry.
                 * MAC_NO_RESOURCES: Operation could not be completed because no memory resources are available,
                 * wait some time and retry.
                 */
                await (0, utils_1.wait)(2000);
                return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt + 1, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore);
            }
            let doAssocRemove = false;
            if (!assocRemove &&
                dataConfirmResult === ZnpCommandStatus.MAC_TRANSACTION_EXPIRED &&
                dataRequestAttempt >= 1 &&
                this.supportsAssocRemove()) {
                const match = await this.znp.requestWithReply(Subsystem.UTIL, "assocGetWithAddress", {
                    extaddr: ieeeAddr,
                    nwkaddr: networkAddress,
                });
                if (match.payload.nwkaddr !== 0xfffe && match.payload.noderelation !== 255) {
                    doAssocRemove = true;
                    assocRestore = { ieeeadr: ieeeAddr, nwkaddr: networkAddress, noderelation: match.payload.noderelation };
                }
                assocRemove = true;
            }
            // NWK_NO_ROUTE: no network route => rediscover route
            // MAC_NO_ACK: route may be corrupted
            // MAC_TRANSACTION_EXPIRED: Mac layer is sleeping
            if (doAssocRemove) {
                /**
                 * Since child aging is disabled on the firmware, when a end device is directly connected
                 * to the coordinator and changes parent and the coordinator does not recevie this update,
                 * it still thinks it's directly connected.
                 * A discoverRoute() is not send out in this case, therefore remove it from the associated device
                 * list and try again.
                 * Note: assocRemove is a custom command, not available by default, only available on recent
                 * z-stack-firmware firmware version. In case it's not supported by the coordinator we will
                 * automatically timeout after 60000ms.
                 */
                logger_1.logger.debug(`assocRemove(${ieeeAddr})`, NS);
                await this.znp.request(Subsystem.UTIL, "assocRemove", { ieeeadr: ieeeAddr });
            }
            else if (!discoveredRoute && dataRequestAttempt >= 1) {
                discoveredRoute = true;
                await this.discoverRoute(networkAddress);
            }
            else if (!checkedNetworkAddress && dataRequestAttempt >= 1) {
                // Figure out once if the network address has been changed.
                try {
                    checkedNetworkAddress = true;
                    const actualNetworkAddress = await this.requestNetworkAddress(ieeeAddr);
                    if (networkAddress !== actualNetworkAddress) {
                        logger_1.logger.debug("Failed because request was done with wrong network address", NS);
                        discoveredRoute = true;
                        networkAddress = actualNetworkAddress;
                        await this.discoverRoute(actualNetworkAddress);
                    }
                    else {
                        logger_1.logger.debug("Network address did not change", NS);
                    }
                    /* v8 ignore start */
                }
                catch {
                    /* empty */
                }
                /* v8 ignore stop */
            }
            else {
                logger_1.logger.debug("Wait 2000ms", NS);
                await (0, utils_1.wait)(2000);
            }
            return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt + 1, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore);
        }
        if (response !== null) {
            try {
                const result = await response.start().promise;
                return result;
            }
            catch (error) {
                logger_1.logger.debug(`Response timeout (${ieeeAddr}:${networkAddress},${responseAttempt})`, NS);
                if (responseAttempt < 1 && !disableRecovery) {
                    // No response could be because the radio of the end device is turned off:
                    // Sometimes the coordinator does not properly set the PENDING flag.
                    // Try to rewrite the device entry in the association table, this fixes it sometimes.
                    const match = await this.znp.requestWithReply(Subsystem.UTIL, "assocGetWithAddress", {
                        extaddr: ieeeAddr,
                        nwkaddr: networkAddress,
                    });
                    logger_1.logger.debug(`Response timeout recovery: Node relation ${match.payload.noderelation} (${ieeeAddr} / ${match.payload.nwkaddr})`, NS);
                    if (this.supportsAssocAdd() &&
                        this.supportsAssocRemove() &&
                        match.payload.nwkaddr !== 0xfffe &&
                        match.payload.noderelation === 1) {
                        logger_1.logger.debug(`Response timeout recovery: Rewrite association table entry (${ieeeAddr})`, NS);
                        await this.znp.request(Subsystem.UTIL, "assocRemove", { ieeeadr: ieeeAddr });
                        await this.znp.request(Subsystem.UTIL, "assocAdd", {
                            ieeeadr: ieeeAddr,
                            nwkaddr: networkAddress,
                            noderelation: match.payload.noderelation,
                        });
                    }
                    // No response could be of invalid route, e.g. when message is send to wrong parent of end device.
                    await this.discoverRoute(networkAddress);
                    return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt + 1, dataRequestAttempt, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore);
                }
                throw error;
            }
        }
    }
    async sendZclFrameToGroup(groupID, zclFrame, sourceEndpoint, profileId) {
        const srcEndpoint = this.selectSourceEndpoint(sourceEndpoint, profileId);
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            await this.dataRequestExtended(AddressMode.ADDR_GROUP, groupID, 0xff, 0, srcEndpoint, zclFrame.cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(), 3000, true);
            /**
             * As a group command is not confirmed and thus immidiately returns
             * (contrary to network address requests) we will give the
             * command some time to 'settle' in the network.
             */
            await (0, utils_1.wait)(200);
        });
    }
    async sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, destination, profileId) {
        const srcEndpoint = this.selectSourceEndpoint(sourceEndpoint, profileId);
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            await this.dataRequestExtended(AddressMode.ADDR_16BIT, destination, endpoint, 0, srcEndpoint, zclFrame.cluster.ID, Constants.AF.DEFAULT_RADIUS, zclFrame.toBuffer(), 3000, false, 0);
            /**
             * As a broadcast command is not confirmed and thus immidiately returns
             * (contrary to network address requests) we will give the
             * command some time to 'settle' in the network.
             */
            await (0, utils_1.wait)(200);
        });
    }
    async addInstallCode(ieeeAddress, key, hashed) {
        (0, node_assert_1.default)(this.version.product !== tstype_1.ZnpVersion.ZStack12, "Install code is not supported for ZStack 1.2 adapter");
        // TODO: always use 0x2? => const hashedKey = hashed ? key : ZSpec.Utils.aes128MmoHash(key);
        const payload = { installCodeFormat: hashed ? 0x2 : 0x1, ieeeaddr: ieeeAddress, installCode: key };
        await this.znp.request(Subsystem.APP_CNF, "bdbAddInstallCode", payload);
    }
    /**
     * Event handlers
     */
    onZnpClose() {
        if (!this.closing) {
            this.emit("disconnected");
        }
    }
    onZnpRecieved(object) {
        if (object.type !== unpi_1.Constants.Type.AREQ) {
            return;
        }
        if (object.subsystem === Subsystem.ZDO) {
            if ((0, utils_2.isMtCmdAreqZdo)(object.command)) {
                this.emit("zdoResponse", object.command.zdoClusterId, object.payload.zdo);
            }
            if (object.command.name === "tcDeviceInd") {
                const payload = {
                    networkAddress: object.payload.nwkaddr,
                    ieeeAddr: object.payload.extaddr,
                };
                this.emit("deviceJoined", payload);
            }
            else if (object.command.name === "endDeviceAnnceInd") {
                // TODO: better way???
                if (Zdo.Buffalo.checkStatus(object.payload.zdo)) {
                    const zdoPayload = object.payload.zdo[1];
                    // Only discover routes to end devices, if bit 1 of capabilities === 0 it's an end device.
                    const isEndDevice = zdoPayload.capabilities.deviceType === 0;
                    if (isEndDevice) {
                        if (!this.deviceAnnounceRouteDiscoveryDebouncers.has(zdoPayload.nwkAddress)) {
                            // If a device announces multiple times in a very short time, it makes no sense
                            // to rediscover the route every time.
                            const debouncer = (0, debounce_1.default)(() => {
                                this.queue
                                    .execute(async () => {
                                    await this.discoverRoute(zdoPayload.nwkAddress, false).catch(() => { });
                                }, zdoPayload.nwkAddress)
                                    .catch(() => { });
                            }, 60 * 1000, { immediate: true });
                            this.deviceAnnounceRouteDiscoveryDebouncers.set(zdoPayload.nwkAddress, debouncer);
                        }
                        const debouncer = this.deviceAnnounceRouteDiscoveryDebouncers.get(zdoPayload.nwkAddress);
                        (0, node_assert_1.default)(debouncer);
                        debouncer();
                    }
                }
            }
            else if (object.command.name === "concentratorIndCb") {
                // Some routers may change short addresses and the announcement
                // is missed by the coordinator. This can happen when there are
                // power outages or other interruptions in service. They may
                // not send additional announcements, causing the device to go
                // offline. However, those devices may instead send
                // Concentrator Indicator Callback commands, which contain both
                // the short and the long address allowing us to update our own
                // mappings.
                // https://e2e.ti.com/cfs-file/__key/communityserver-discussions-components-files/158/4403.zstacktask.c
                // https://github.com/Koenkk/zigbee-herdsman/issues/74
                this.emit("zdoResponse", Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE, [
                    Zdo.Status.SUCCESS,
                    {
                        eui64: object.payload.extaddr,
                        nwkAddress: object.payload.srcaddr,
                        startIndex: 0,
                        assocDevList: [],
                    },
                ]);
            }
            else {
                if (object.command.name === "leaveInd") {
                    if (object.payload.rejoin) {
                        logger_1.logger.debug("Device leave: Got leave indication with rejoin=true, nothing to do", NS);
                    }
                    else {
                        const payload = {
                            networkAddress: object.payload.srcaddr,
                            ieeeAddr: object.payload.extaddr,
                        };
                        this.emit("deviceLeave", payload);
                    }
                }
            }
        }
        else {
            if (object.subsystem === Subsystem.AF) {
                if (object.command.name === "incomingMsg" || object.command.name === "incomingMsgExt") {
                    // TI ZNP API docs
                    // ---------------
                    // AF_INCOMING_MSG_EXT - SrcAddrMode
                    // A value of 3 (i.e. the enumeration value for ‘afAddr64Bit’) indicates 8-
                    // byte/64-bit address mode; otherwise, only the 2 LSB’s of the 8 bytes are
                    // used to form a 2-byte short address.
                    // Addr is currently parsed by Buffalo as Eui64 (`0x${string}`), but it's
                    // possible future changes could read srcaddrmode and parse accordingly.
                    // Prior tests also passed an integer (e.g. 2), so best to handle all cases.
                    // If type is not a string, we assume a 16-bit address.
                    const isEui64Addr = typeof object.payload.srcaddr === "string";
                    const srcaddr = object.command.name === "incomingMsgExt" && object.payload.srcaddrmode !== 0x03
                        ? isEui64Addr
                            ? Number.parseInt(object.payload.srcaddr.slice(-4), 16)
                            : object.payload.srcaddr
                        : object.payload.srcaddr;
                    // TI ZNP API docs suggest that payload.data should be zero length for messages
                    // with huge data, but testing shows it is 3-bytes, the first two of which are
                    // the 16-bit srcAddr.
                    // Possibly zh is not parsing incomingMsgExt correctly. Just compare len with
                    // payload length for now.
                    //if (object.command.name === "incomingMsgExt" && object.payload.len > 0 && object.payload.data.length === 0) {
                    if (object.command.name === "incomingMsgExt" && object.payload.data.length < object.payload.len) {
                        // The ZNP will send incomingMsgExt (AF_INCOMING_MSG_EXT)
                        // when data length is > about 223 bytes (or if INTER_PAN network
                        // is used).
                        //
                        // In the first case, the data is not included in the payload, but
                        // must be retrieved block by block from the ZNP buffer using the
                        // AF_DATA_RETRIEVE message.
                        this.queue
                            .execute(async () => {
                            const data = await this.dataRetrieveAll(object.payload.timestamp, object.payload.len);
                            if (data === undefined) {
                                logger_1.logger.error("Failed to retrieve chunked payload for incomingMsgExt", NS);
                            }
                            else {
                                logger_1.logger.debug(`Retrieved ${data.length} bytes from huge data buffer for msg with timestamp ${object.payload.timestamp}`, NS);
                                const payload = {
                                    clusterID: object.payload.clusterid,
                                    data: data,
                                    header: Zcl.Header.fromBuffer(data),
                                    address: srcaddr,
                                    endpoint: object.payload.srcendpoint,
                                    linkquality: object.payload.linkquality,
                                    groupID: object.payload.groupid,
                                    wasBroadcast: object.payload.wasbroadcast === 1,
                                    destinationEndpoint: object.payload.dstendpoint,
                                };
                                this.waitress.resolve(payload);
                                this.emit("zclPayload", payload);
                            }
                        })
                            .catch(() => { });
                    }
                    else {
                        // incomingMsg OR incomingMsgExt with data
                        // in the payload (i.e. INTER_PAN network)
                        const payload = {
                            clusterID: object.payload.clusterid,
                            data: object.payload.data,
                            header: Zcl.Header.fromBuffer(object.payload.data),
                            address: srcaddr,
                            endpoint: object.payload.srcendpoint,
                            linkquality: object.payload.linkquality,
                            groupID: object.payload.groupid,
                            wasBroadcast: object.payload.wasbroadcast === 1,
                            destinationEndpoint: object.payload.dstendpoint,
                        };
                        this.waitress.resolve(payload);
                        this.emit("zclPayload", payload);
                    }
                }
            }
        }
    }
    async getNetworkParameters() {
        const result = await this.znp.requestWithReply(Subsystem.ZDO, "extNwkInfo", {});
        return {
            panID: result.payload.panid,
            extendedPanID: result.payload.extendedpanid, // read as IEEEADDR, so `0x${string}`
            channel: result.payload.channel,
            /**
             * Return a dummy nwkUpdateId of 0, the nwkUpdateId is used when changing channels however the
             * zstack API does not allow to set this value. Instead it automatically increments the nwkUpdateId
             * based on the value in the NIB.
             * https://github.com/Koenkk/zigbee-herdsman/pull/1280#discussion_r1947815987
             */
            nwkUpdateID: 0,
        };
    }
    async supportsBackup() {
        return await Promise.resolve(true);
    }
    async backup(ieeeAddressesInDatabase) {
        return await this.adapterManager.backup.createBackup(ieeeAddressesInDatabase);
    }
    async setChannelInterPAN(channel) {
        return await this.queue.execute(async () => {
            this.interpanLock = true;
            await this.znp.request(Subsystem.AF, "interPanCtl", { cmd: 1, data: [channel] });
            if (!this.interpanEndpointRegistered) {
                // Make sure that endpoint 12 is registered to proxy the InterPAN messages.
                await this.znp.request(Subsystem.AF, "interPanCtl", { cmd: 2, data: [12] });
                this.interpanEndpointRegistered = true;
            }
        });
    }
    async sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddr) {
        return await this.queue.execute(async () => {
            await this.dataRequestExtended(AddressMode.ADDR_64BIT, ieeeAddr, 0xfe, 0xffff, 12, zclFrame.cluster.ID, 30, zclFrame.toBuffer(), 10000, false);
        });
    }
    async sendZclFrameInterPANBroadcast(zclFrame, timeout, disableResponse) {
        return await this.queue.execute(async () => {
            const command = zclFrame.command;
            if (!disableResponse && command.response === undefined) {
                throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
            }
            let response;
            if (!disableResponse && command.response !== undefined) {
                response = this.waitForInternal(undefined, 0xfe, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, undefined, zclFrame.cluster.ID, command.response, timeout);
            }
            try {
                await this.dataRequestExtended(AddressMode.ADDR_16BIT, 0xffff, 0xfe, 0xffff, 12, zclFrame.cluster.ID, 30, zclFrame.toBuffer(), 10000, false);
            }
            catch (error) {
                response?.cancel();
                throw error;
            }
            if (response) {
                return await response.start().promise;
            }
        });
    }
    async restoreChannelInterPAN() {
        return await this.queue.execute(async () => {
            await this.znp.request(Subsystem.AF, "interPanCtl", { cmd: 0, data: [] });
            // Give adapter some time to restore, otherwise stuff crashes
            await (0, utils_1.wait)(3000);
            this.interpanLock = false;
        });
    }
    waitForInternal(networkAddress, endpoint, frameType, direction, transactionSequenceNumber, clusterID, commandIdentifier, timeout) {
        const payload = {
            address: networkAddress,
            endpoint,
            clusterID,
            commandIdentifier,
            frameType,
            direction,
            transactionSequenceNumber,
        };
        const waiter = this.waitress.waitFor(payload, timeout);
        const cancel = () => this.waitress.remove(waiter.ID);
        return { start: waiter.start, cancel };
    }
    waitFor(networkAddress, endpoint, frameType, direction, transactionSequenceNumber, clusterID, commandIdentifier, timeout) {
        const waiter = this.waitForInternal(networkAddress, endpoint, frameType, direction, transactionSequenceNumber, clusterID, commandIdentifier, timeout);
        return { cancel: waiter.cancel, promise: waiter.start().promise };
    }
    /**
     * Private methods
     */
    async dataRequest(destinationAddress, destinationEndpoint, sourceEndpoint, clusterID, radius, data, timeout) {
        const transactionID = this.nextTransactionID();
        const response = this.znp.waitFor(Type.AREQ, Subsystem.AF, "dataConfirm", undefined, transactionID, undefined, timeout);
        await this.znp.request(Subsystem.AF, "dataRequest", {
            dstaddr: destinationAddress,
            destendpoint: destinationEndpoint,
            srcendpoint: sourceEndpoint,
            clusterid: clusterID,
            transid: transactionID,
            options: 0,
            radius: radius,
            len: data.length,
            data: data,
        }, response.ID);
        let result = null;
        try {
            const dataConfirm = await response.start().promise;
            result = dataConfirm.payload.status;
        }
        catch {
            result = DataConfirmTimeout;
        }
        return result;
    }
    async dataRequestExtended(addressMode, destinationAddressOrGroupID, destinationEndpoint, panID, sourceEndpoint, clusterID, radius, data, timeout, confirmation, attemptsLeft = 5) {
        const transactionID = this.nextTransactionID();
        const response = confirmation
            ? this.znp.waitFor(Type.AREQ, Subsystem.AF, "dataConfirm", undefined, transactionID, undefined, timeout)
            : undefined;
        await this.znp.request(Subsystem.AF, "dataRequestExt", {
            dstaddrmode: addressMode,
            dstaddr: this.toAddressString(destinationAddressOrGroupID),
            destendpoint: destinationEndpoint,
            dstpanid: panID,
            srcendpoint: sourceEndpoint,
            clusterid: clusterID,
            transid: transactionID,
            options: 0, // TODO: why was this here? Constants.AF.options.DISCV_ROUTE,
            radius,
            len: data.length,
            data: data,
        }, response?.ID);
        if (confirmation && response) {
            const dataConfirm = await response.start().promise;
            if (dataConfirm.payload.status !== ZnpCommandStatus.SUCCESS) {
                if (attemptsLeft > 0 &&
                    (dataConfirm.payload.status === ZnpCommandStatus.MAC_CHANNEL_ACCESS_FAILURE ||
                        dataConfirm.payload.status === ZnpCommandStatus.BUFFER_FULL)) {
                    /**
                     * 225: When many commands at once are executed we can end up in a MAC channel access failure
                     * error. This is because there is too much traffic on the network.
                     * Retry this command once after a cooling down period.
                     */
                    await (0, utils_1.wait)(2000);
                    return await this.dataRequestExtended(addressMode, destinationAddressOrGroupID, destinationEndpoint, panID, sourceEndpoint, clusterID, radius, data, timeout, confirmation, attemptsLeft - 1);
                }
                throw new DataConfirmError(dataConfirm.payload.status);
            }
            return dataConfirm;
        }
    }
    nextTransactionID() {
        this.transactionID++;
        if (this.transactionID > 255) {
            this.transactionID = 1;
        }
        return this.transactionID;
    }
    toAddressString(address) {
        return typeof address === "number" ? `0x${address.toString(16).padStart(16, "0")}` : address.toString();
    }
    waitressTimeoutFormatter(matcher, timeout) {
        return (`Timeout - ${matcher.address} - ${matcher.endpoint}` +
            ` - ${matcher.transactionSequenceNumber} - ${matcher.clusterID}` +
            ` - ${matcher.commandIdentifier} after ${timeout}ms`);
    }
    waitressValidator(payload, matcher) {
        return Boolean(payload.header &&
            (!matcher.address || payload.address === matcher.address) &&
            payload.endpoint === matcher.endpoint &&
            (matcher.transactionSequenceNumber === undefined || payload.header.transactionSequenceNumber === matcher.transactionSequenceNumber) &&
            payload.clusterID === matcher.clusterID &&
            matcher.frameType === payload.header.frameControl.frameType &&
            matcher.commandIdentifier === payload.header.commandIdentifier &&
            matcher.direction === payload.header.frameControl.direction);
    }
    checkInterpanLock() {
        if (this.interpanLock) {
            throw new Error("Cannot execute command, in Inter-PAN mode");
        }
    }
    selectSourceEndpoint(sourceEndpoint, profileId) {
        // Use provided sourceEndpoint as the highest priority
        let srcEndpoint = sourceEndpoint;
        // If sourceEndpoint is not provided, try to select the source endpoint based on the profileId.
        if (srcEndpoint === undefined && profileId !== undefined) {
            srcEndpoint = endpoints_1.Endpoints.find((e) => e.appprofid === profileId)?.endpoint;
        }
        //If no profileId is provided, or if no corresponding endpoint exists, use endpoint 1.
        if (srcEndpoint === undefined) {
            srcEndpoint = 1;
        }
        // Validate that the requested profileId can be satisfied by the adapter.
        if (profileId !== undefined && endpoints_1.Endpoints.find((e) => e.endpoint === srcEndpoint)?.appprofid !== profileId) {
            throw new Error(`Profile ID ${profileId} is not supported by this adapter.`);
        }
        return srcEndpoint;
    }
}
exports.ZStackAdapter = ZStackAdapter;
//# sourceMappingURL=zStackAdapter.js.map