"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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZoHAdapter = exports.DEFAULT_STACK_CONFIG = void 0;
const node_fs_1 = require("node:fs");
const node_net_1 = require("node:net");
const node_path_1 = require("node:path");
const zigbee_on_host_1 = require("zigbee-on-host");
const logger_1 = require("zigbee-on-host/dist/utils/logger");
const logger_2 = require("../../../utils/logger");
const queue_1 = require("../../../utils/queue");
const wait_1 = require("../../../utils/wait");
const waitress_1 = require("../../../utils/waitress");
const ZSpec = __importStar(require("../../../zspec"));
const Zcl = __importStar(require("../../../zspec/zcl"));
const Zdo = __importStar(require("../../../zspec/zdo"));
const adapter_1 = require("../../adapter");
const serialPort_1 = require("../../serialPort");
const utils_1 = require("../../utils");
const utils_2 = require("./utils");
const NS = "zh:zoh";
const DEFAULT_REQUEST_TIMEOUT = 15000;
exports.DEFAULT_STACK_CONFIG = {
    tiSerialSkipBootloader: false,
    eui64: Buffer.from([0x5a, 0x6f, 0x48, 0x6f, 0x6e, 0x5a, 0x32, 0x4d]).readBigUInt64LE(0),
    ccaBackoffAttempts: 1,
    ccaRetries: 4,
    enableCSMACA: true,
};
class ZoHAdapter extends adapter_1.Adapter {
    serialPort;
    socketPort;
    /** True when adapter is currently closing */
    // biome-ignore lint/correctness/noUnusedPrivateClassMembers: ignore
    closing;
    interpanLock;
    stackConfig;
    driver;
    queue;
    zclWaitress;
    zdoWaitress;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = true;
        this.manufacturerID = Zcl.ManufacturerCode.CONNECTIVITY_STANDARDS_ALLIANCE;
        this.closing = false;
        this.stackConfig = this.loadStackConfig();
        const channel = networkOptions.channelList[0];
        this.driver = new zigbee_on_host_1.OTRCPDriver({
            /* v8 ignore start */
            onFatalError: (message) => {
                logger_2.logger.error(message, NS);
            },
            onMACFrame: () => undefined, // unused
            /* v8 ignore stop */
            onFrame: this.onFrame.bind(this),
            onGPFrame: this.onGPFrame.bind(this),
            onDeviceJoined: this.onDeviceJoined.bind(this),
            onDeviceRejoined: this.onDeviceRejoined.bind(this),
            onDeviceLeft: this.onDeviceLeft.bind(this),
            onDeviceAuthorized: this.onDeviceAuthorized.bind(this),
        }, {
            txChannel: channel,
            ccaBackoffAttempts: this.stackConfig.ccaBackoffAttempts,
            ccaRetries: this.stackConfig.ccaRetries,
            enableCSMACA: this.stackConfig.enableCSMACA,
            headerUpdated: true,
            reTx: false,
            securityProcessed: true,
            txDelay: 0,
            txDelayBaseTime: 0,
            rxChannelAfterTxDone: channel,
        }, 
        // NOTE: this information is overwritten on `start` if a save exists
        {
            eui64: this.stackConfig.eui64,
            panId: this.networkOptions.panID,
            // biome-ignore lint/style/noNonNullAssertion: expected always valid at this point
            extendedPanId: Buffer.from(this.networkOptions.extendedPanID).readBigUInt64LE(0),
            channel,
            nwkUpdateId: 0,
            txPower: this.adapterOptions.transmitPower ?? /* v8 ignore next */ 5,
            // ZigBeeAlliance09
            tcKey: Buffer.from([0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39]),
            tcKeyFrameCounter: 0,
            // biome-ignore lint/style/noNonNullAssertion: expected always valid at this point
            networkKey: Buffer.from(this.networkOptions.networkKey),
            networkKeyFrameCounter: 0,
            networkKeySequenceNumber: 0,
        }, (0, node_path_1.dirname)(this.backupPath));
        this.queue = new queue_1.Queue(this.adapterOptions.concurrent || /* v8 ignore next */ 8); // ORed to avoid 0 (not checked in settings/queue constructor)
        this.zclWaitress = new waitress_1.Waitress(adapter_1.Adapter.zclWaitressValidator, adapter_1.Adapter.clusterWaitressTimeoutFormatter);
        this.zdoWaitress = new waitress_1.Waitress(this.zdoWaitressValidator, adapter_1.Adapter.clusterWaitressTimeoutFormatter);
        this.interpanLock = false;
    }
    loadStackConfig() {
        // store stack config in same dir as backup
        const configPath = (0, node_path_1.join)((0, node_path_1.dirname)(this.backupPath), "zoh_config.json");
        // use default as base (for all invalid/missing in JSON)
        const config = { ...exports.DEFAULT_STACK_CONFIG };
        try {
            const inRange = (value, min, max) => !(value == null || value < min || value > max);
            const customConfig = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf8"));
            if (customConfig.tiSerialSkipBootloader != null) {
                if (customConfig.tiSerialSkipBootloader === true || customConfig.tiSerialSkipBootloader === false) {
                    config.tiSerialSkipBootloader = customConfig.tiSerialSkipBootloader;
                }
                else {
                    logger_2.logger.error("[STACK CONFIG] Invalid tiSerialSkipBootloader, using default.", NS);
                }
            }
            if (customConfig.eui64 != null) {
                try {
                    config.eui64 = BigInt(customConfig.eui64);
                }
                catch (error) {
                    logger_2.logger.error(`[STACK CONFIG] Invalid eui64 (${error}), using default.`, NS);
                }
            }
            if (customConfig.ccaBackoffAttempts != null) {
                if (inRange(customConfig.ccaBackoffAttempts, 0, 255)) {
                    config.ccaBackoffAttempts = customConfig.ccaBackoffAttempts;
                }
                else {
                    logger_2.logger.error("[STACK CONFIG] Invalid ccaBackoffAttempts, using default.", NS);
                }
            }
            if (customConfig.ccaRetries != null) {
                if (inRange(customConfig.ccaRetries, 0, 255)) {
                    config.ccaRetries = customConfig.ccaRetries;
                }
                else {
                    logger_2.logger.error("[STACK CONFIG] Invalid ccaRetries, using default.", NS);
                }
            }
            if (customConfig.enableCSMACA != null) {
                if (customConfig.enableCSMACA === true || customConfig.enableCSMACA === false) {
                    config.enableCSMACA = customConfig.enableCSMACA;
                }
                else {
                    logger_2.logger.error("[STACK CONFIG] Invalid enableCSMACA, using default.", NS);
                }
            }
            logger_2.logger.info(`Using stack config ${JSON.stringify(config)}.`, NS);
        }
        catch {
            logger_2.logger.info("Using default stack config.", NS);
        }
        return config;
    }
    /**
     * Init the serial or socket port and hook parser/writer.
     */
    /* v8 ignore start */
    async initPort() {
        await this.closePort(); // will do nothing if nothing's open
        // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
        if ((0, utils_1.isTcpPath)(this.serialPortOptions.path)) {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            const pathUrl = new URL(this.serialPortOptions.path);
            const hostname = pathUrl.hostname;
            const port = Number.parseInt(pathUrl.port, 10);
            logger_2.logger.debug(`Opening TCP socket with ${hostname}:${port}`, NS);
            this.socketPort = new node_net_1.Socket();
            this.socketPort.setNoDelay(true);
            this.socketPort.setKeepAlive(true, 15000);
            this.driver.writer.pipe(this.socketPort);
            this.socketPort.pipe(this.driver.parser);
            this.driver.parser.on("data", this.driver.onFrame.bind(this.driver));
            return await new Promise((resolve, reject) => {
                const openError = async (err) => {
                    await this.stop();
                    reject(err);
                };
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                this.socketPort.on("connect", () => {
                    logger_2.logger.debug("Socket connected", NS);
                });
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                this.socketPort.on("ready", () => {
                    logger_2.logger.info("Socket ready", NS);
                    // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                    this.socketPort.removeListener("error", openError);
                    // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                    this.socketPort.once("close", this.onPortClose.bind(this));
                    // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                    this.socketPort.on("error", this.onPortError.bind(this));
                    resolve();
                });
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                this.socketPort.once("error", openError);
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                this.socketPort.connect(port, hostname);
            });
        }
        const serialOpts = {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            path: this.serialPortOptions.path,
            baudRate: typeof this.serialPortOptions.baudRate === "number" ? this.serialPortOptions.baudRate : 115200,
            rtscts: typeof this.serialPortOptions.rtscts === "boolean" ? this.serialPortOptions.rtscts : false,
            autoOpen: false,
            parity: "none",
            stopBits: 1,
            xon: false,
            xoff: false,
        };
        // enable software flow control if RTS/CTS not enabled in config
        if (!serialOpts.rtscts) {
            logger_2.logger.info("RTS/CTS config is off, enabling software flow control.", NS);
            serialOpts.xon = true;
            serialOpts.xoff = true;
        }
        logger_2.logger.debug(() => `Opening serial port with [path=${serialOpts.path} baudRate=${serialOpts.baudRate} rtscts=${serialOpts.rtscts}]`, NS);
        this.serialPort = new serialPort_1.SerialPort(serialOpts);
        this.driver.writer.pipe(this.serialPort);
        this.serialPort.pipe(this.driver.parser);
        this.driver.parser.on("data", this.driver.onFrame.bind(this.driver));
        try {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            await this.serialPort.asyncOpen();
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            await this.serialPort.asyncFlush();
            logger_2.logger.info("Serial port opened", NS);
            this.serialPort.once("close", this.onPortClose.bind(this));
            this.serialPort.on("error", this.onPortError.bind(this));
            if (this.stackConfig.tiSerialSkipBootloader) {
                // skipping bootloader is required for some CC2652/CC1352 devices (auto-entered)
                logger_2.logger.info("TI skip bootloader", NS);
                await this.serialPort.asyncSet({ dtr: false, rts: false });
                await (0, wait_1.wait)(150);
                await this.serialPort.asyncSet({ dtr: false, rts: true });
                await (0, wait_1.wait)(150);
                await this.serialPort.asyncSet({ dtr: false, rts: false });
                await (0, wait_1.wait)(150);
            }
        }
        catch (error) {
            await this.stop();
            throw error;
        }
    }
    /* v8 ignore stop */
    /**
     * Handle port closing
     * @param err A boolean for Socket, an Error for serialport
     */
    /* v8 ignore start */
    onPortClose(error) {
        logger_2.logger.info(`Port closed ${error}`, NS);
        this.emit("disconnected");
    }
    /* v8 ignore stop */
    /**
     * Handle port error
     * @param error
     */
    /* v8 ignore start */
    onPortError(error) {
        logger_2.logger.error(`Port ${error}`, NS);
    }
    /* v8 ignore stop */
    /* v8 ignore start */
    async closePort() {
        if (this.serialPort?.isOpen) {
            try {
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                await this.serialPort.asyncFlushAndClose();
            }
            catch (err) {
                logger_2.logger.error(`Failed to close serial port ${err}.`, NS);
            }
            this.serialPort.removeAllListeners();
            this.serialPort = undefined;
        }
        else if (this.socketPort != null && !this.socketPort.closed) {
            this.socketPort.destroy();
            this.socketPort.removeAllListeners();
            this.socketPort = undefined;
        }
    }
    /* v8 ignore stop */
    async start() {
        (0, logger_1.setLogger)(logger_2.logger); // pass the logger to ZoH
        await this.initPort();
        let result = "resumed";
        const currentNetParams = await this.driver.context.readNetworkState();
        if (currentNetParams) {
            // Note: channel change is handled by Controller
            if (
            // TODO: add eui64 whenever added as configurable
            this.networkOptions.panID !== currentNetParams.panId ||
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                Buffer.from(this.networkOptions.extendedPanID).readBigUInt64LE(0) !== currentNetParams.extendedPanId ||
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                !Buffer.from(this.networkOptions.networkKey).equals(currentNetParams.networkKey)) {
                await this.driver.resetNetwork();
                result = "reset";
            }
        }
        else {
            // no save detected, brand new network
            result = "reset";
        }
        await this.driver.start();
        await this.driver.formNetwork();
        return result;
    }
    async stop() {
        this.closing = true;
        this.queue.clear();
        this.zclWaitress.clear();
        this.zdoWaitress.clear();
        await this.driver.stop();
    }
    async getCoordinatorIEEE() {
        return await Promise.resolve(`0x${(0, utils_2.bigUInt64ToHexBE)(this.driver.context.netParams.eui64)}`);
    }
    async getCoordinatorVersion() {
        return await Promise.resolve({
            type: "Zigbee on Host",
            meta: {
                major: this.driver.protocolVersionMajor,
                minor: this.driver.protocolVersionMinor,
                version: this.driver.ncpVersion,
                apiVersion: this.driver.rcpAPIVersion,
                revision: `https://github.com/Nerivec/zigbee-on-host (using: ${this.driver.ncpVersion})`,
            },
        });
    }
    /* v8 ignore start */
    async reset(type) {
        await Promise.reject(new Error(`Reset ${type} not support`));
    }
    /* v8 ignore stop */
    /* v8 ignore start */
    async supportsBackup() {
        return await Promise.resolve(false);
    }
    /* v8 ignore stop */
    /* v8 ignore start */
    async backup(_ieeeAddressesInDatabase) {
        return await Promise.reject(new Error("Zigbee on Host handles backup internally"));
    }
    /* v8 ignore stop */
    async getNetworkParameters() {
        return await Promise.resolve({
            panID: this.driver.context.netParams.panId,
            extendedPanID: `0x${(0, utils_2.bigUInt64ToHexBE)(this.driver.context.netParams.extendedPanId)}`,
            channel: this.driver.context.netParams.channel,
            nwkUpdateID: this.driver.context.netParams.nwkUpdateId,
        });
    }
    async addInstallCode(ieeeAddress, key, hashed) {
        this.driver.context.addInstallCode(BigInt(ieeeAddress), key, hashed);
        return await Promise.resolve();
    }
    /* v8 ignore start */
    waitFor(networkAddress, endpoint, _frameType, _direction, transactionSequenceNumber, clusterId, commandId, timeout) {
        const waiter = this.zclWaitress.waitFor({ address: networkAddress, endpoint, clusterId, commandId, transactionSequenceNumber }, timeout);
        const cancel = () => this.zclWaitress.remove(waiter.ID);
        return { cancel, promise: waiter.start().promise };
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        if (networkAddress === ZSpec.COORDINATOR_ADDRESS) {
            // mock ZDO response using driver layer data for coordinator
            // seqNum doesn't matter since waitress bypassed, so don't bother doing any logic for it
            const response = this.driver.apsHandler.getCoordinatorZDOResponse(clusterId, payload);
            if (!response) {
                throw new Error(`Coordinator does not support ZDO cluster ${clusterId}`);
            }
            const respClusterId = clusterId | 0x8000;
            const result = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, respClusterId, response);
            this.emit("zdoResponse", respClusterId, result);
            return result;
        }
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_2.logger.debug(() => `~~~> [ZDO to=${ieeeAddress}:${networkAddress} clusterId=${clusterId} disableResponse=${disableResponse}]`, NS);
            const [, zdoSeqNum] = await this.driver.sendZDO(payload, networkAddress, // nwkDest16
            undefined, // nwkDest64 XXX: avoid passing EUI64 whenever not absolutely necessary
            clusterId);
            if (!disableResponse) {
                const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
                if (responseClusterId) {
                    const resp = await this.zdoWaitress
                        .waitFor({
                        address: responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress,
                        clusterId: responseClusterId,
                        transactionSequenceNumber: zdoSeqNum,
                    }, DEFAULT_REQUEST_TIMEOUT)
                        .start().promise;
                    return resp.response;
                }
            }
        }, networkAddress);
    }
    async permitJoin(seconds, networkAddress) {
        if (networkAddress === undefined) {
            // send ZDO BCAST
            this.driver.context.allowJoins(seconds, true);
            this.driver.nwkGPHandler.enterCommissioningMode(seconds);
            const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
            // `authentication`: TC significance always 1 (zb specs)
            const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []);
            await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true);
        }
        else if (networkAddress === ZSpec.COORDINATOR_ADDRESS) {
            this.driver.context.allowJoins(seconds, true);
            this.driver.nwkGPHandler.enterCommissioningMode(seconds);
        }
        else {
            // send ZDO to networkAddress
            this.driver.context.allowJoins(seconds, false);
            const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
            // `authentication`: TC significance always 1 (zb specs)
            const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []);
            const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false);
            /* v8 ignore start */
            if (!Zdo.Buffalo.checkStatus(result)) {
                throw new Zdo.StatusError(result[0]);
            }
            /* v8 ignore stop */
        }
    }
    // #endregion
    // #region ZCL
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint, profileId) {
        /* v8 ignore start */
        if (networkAddress === ZSpec.COORDINATOR_ADDRESS) {
            // TODO: handle e.g. GP permit join
            logger_2.logger.debug(() => `~x~> [ZCL clusterId=${zclFrame.cluster.ID} destEp=${endpoint} sourceEp=${sourceEndpoint}] Not sending to coordinator`, NS);
            return;
        }
        /* v8 ignore stop */
        let commandResponseId;
        if (zclFrame.command.response !== undefined && disableResponse === false) {
            commandResponseId = zclFrame.command.response;
        }
        else if (!zclFrame.header.frameControl.disableDefaultResponse) {
            commandResponseId = Zcl.Foundation.defaultRsp.ID;
        }
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_2.logger.debug(() => `~~~> [ZCL to=${ieeeAddr}:${networkAddress} clusterId=${zclFrame.cluster.ID} destEp=${endpoint} sourceEp=${sourceEndpoint}]`, NS);
            for (let i = 0; i < 2; i++) {
                try {
                    await this.driver.sendUnicast(zclFrame.toBuffer(), profileId ??
                        (sourceEndpoint === ZSpec.GP_ENDPOINT && endpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID), zclFrame.cluster.ID, networkAddress, // nwkDest16
                    undefined, // nwkDest64 XXX: avoid passing EUI64 whenever not absolutely necessary
                    endpoint, // destEp
                    sourceEndpoint ?? 1);
                    if (commandResponseId !== undefined) {
                        const resp = await this.zclWaitress
                            .waitFor({
                            address: networkAddress,
                            clusterId: zclFrame.cluster.ID,
                            endpoint,
                            commandId: commandResponseId,
                            transactionSequenceNumber: zclFrame.header.transactionSequenceNumber,
                        }, timeout)
                            .start().promise;
                        return resp;
                    }
                    return;
                }
                catch (error) {
                    if (disableRecovery || i === 1) {
                        throw error;
                    } // else retry
                }
                /* v8 ignore start */
            } // coverage detection failure
            /* v8 ignore stop */
        });
    }
    async sendZclFrameToGroup(groupID, zclFrame, sourceEndpoint, profileId) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_2.logger.debug(() => `~~~> [ZCL GROUP to=${groupID} clusterId=${zclFrame.cluster.ID} sourceEp=${sourceEndpoint}]`, NS);
            await this.driver.sendGroupcast(zclFrame.toBuffer(), profileId ?? ZSpec.HA_PROFILE_ID, zclFrame.cluster.ID, groupID, sourceEndpoint ?? 1);
            // settle
            await (0, wait_1.wait)(500);
        });
    }
    async sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, destination, profileId) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_2.logger.debug(() => `~~~> [ZCL BROADCAST to=${destination} destEp=${endpoint} sourceEp=${sourceEndpoint}]`, NS);
            await this.driver.sendBroadcast(zclFrame.toBuffer(), profileId ?? (sourceEndpoint === ZSpec.GP_ENDPOINT && endpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID), zclFrame.cluster.ID, destination, endpoint, sourceEndpoint);
            // settle
            await (0, wait_1.wait)(500);
        });
    }
    // #endregion
    // #region InterPAN
    /* v8 ignore start */
    async setChannelInterPAN(channel) {
        await Promise.reject(new Error(`not supported ${channel}`));
    }
    /* v8 ignore stop */
    /* v8 ignore start */
    async sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddress) {
        await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}, ${ieeeAddress}`));
    }
    async sendZclFrameInterPANBroadcast(zclFrame, timeout, disableResponse) {
        return await Promise.reject(new Error(`not supported ${JSON.stringify(zclFrame)}, ${timeout}, ${disableResponse}`));
    }
    /* v8 ignore stop */
    /* v8 ignore start */
    async restoreChannelInterPAN() {
        await Promise.reject(new Error("not supported"));
    }
    /* v8 ignore stop */
    // #endregion
    // #region Implementation-Specific
    /* v8 ignore start */
    checkInterpanLock() {
        if (this.interpanLock) {
            throw new Error("[INTERPAN MODE] Cannot execute non-InterPAN commands.");
        }
    }
    /* v8 ignore stop */
    /**
     * @param sender16 If undefined, sender64 is expected defined
     * @param sender64 If undefined, sender16 is expected defined
     * @param apsHeader
     * @param apsPayload
     */
    onFrame(sender16, sender64, apsHeader, apsPayload, rssi) {
        if (apsHeader.profileId === Zdo.ZDO_PROFILE_ID) {
            logger_2.logger.debug(() => `<~~~ APS ZDO[sender=${sender16}:${sender64} clusterId=${apsHeader.clusterId}]`, NS);
            try {
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                const result = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, apsHeader.clusterId, apsPayload);
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                if (apsHeader.clusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE) {
                    // special case to properly resolve a NETWORK_ADDRESS_RESPONSE following a NETWORK_ADDRESS_REQUEST (based on EUI64 from ZDO payload)
                    // NOTE: if response has invalid status (no EUI64 available), response waiter will eventually time out
                    if (Zdo.Buffalo.checkStatus(result)) {
                        this.zdoWaitress.resolve({ address: result[1].eui64, clusterId: apsHeader.clusterId, response: result, seqNum: apsPayload[0] });
                    }
                }
                else {
                    // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                    this.zdoWaitress.resolve({ address: sender16, clusterId: apsHeader.clusterId, response: result, seqNum: apsPayload[0] });
                }
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                this.emit("zdoResponse", apsHeader.clusterId, result);
                /* v8 ignore start */
            }
            catch (error) {
                logger_2.logger.error(`${error.message}`, NS);
            }
            /* v8 ignore stop */
        }
        else {
            logger_2.logger.debug(() => `<~~~ APS[sender=${sender16}:${sender64} profileId=${apsHeader.profileId} clusterId=${apsHeader.clusterId}]`, NS);
            const payload = {
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                clusterID: apsHeader.clusterId,
                header: Zcl.Header.fromBuffer(apsPayload),
                address: sender64 !== undefined
                    ? `0x${(0, utils_2.bigUInt64ToHexBE)(sender64)}`
                    : // biome-ignore lint/style/noNonNullAssertion: ignore
                        sender16,
                data: apsPayload,
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                endpoint: apsHeader.sourceEndpoint,
                linkquality: rssi, // TODO: convert RSSI to LQA
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                groupID: apsHeader.group,
                wasBroadcast: apsHeader.frameControl.deliveryMode === 2 /* BCAST */,
                // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                destinationEndpoint: apsHeader.destEndpoint,
            };
            if (payload.header !== undefined) {
                this.zclWaitress.resolve(payload);
            }
            this.emit("zclPayload", payload);
        }
    }
    onGPFrame(cmdId, payload, macHeader, nwkHeader, rssi) {
        // transform into a ZCL frame
        const data = Buffer.alloc((nwkHeader.frameControlExt?.appId === 0x02 /* ZGP */ ? /* v8 ignore next */ 20 : 15) + payload.byteLength);
        let offset = 0;
        offset = data.writeUInt8(0b00000001, offset); // frameControl: FrameType.SPECIFIC + Direction.CLIENT_TO_SERVER + disableDefaultResponse=false
        offset = data.writeUInt8(macHeader.sequenceNumber ?? /* v8 ignore next */ 0, offset);
        offset = data.writeUInt8(cmdId === 0xe0 ? 0x04 /* commissioning notification */ : 0x00 /* notification */, offset);
        if (nwkHeader.frameControlExt) {
            /* v8 ignore start */
            if (cmdId === 0xe0) {
                offset = data.writeUInt16LE((nwkHeader.frameControlExt.appId & 0x7) |
                    (((nwkHeader.frameControlExt.rxAfterTx ? 1 : 0) & 0x1) << 3) |
                    ((nwkHeader.frameControlExt.securityLevel & 0x3) << 4), offset);
                /* v8 ignore stop */
            }
            else {
                offset = data.writeUInt16LE((nwkHeader.frameControlExt.appId & 0x7) |
                    ((nwkHeader.frameControlExt.securityLevel & 0x3) << 6) |
                    /* v8 ignore next */ (((nwkHeader.frameControlExt.rxAfterTx ? 1 : 0) & 0x3) << 11), offset);
            }
        }
        else {
            offset = data.writeUInt16LE(0, offset); // options, only srcID present
        }
        /* v8 ignore start */
        if (nwkHeader.frameControlExt?.appId === 0x02 /* ZGP */) {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            offset = data.writeBigUInt64LE(macHeader.source64, offset);
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            offset = data.writeUInt8(nwkHeader.endpoint, offset);
            /* v8 ignore stop */
        }
        else {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            offset = data.writeUInt32LE(nwkHeader.sourceId, offset);
        }
        offset = data.writeUInt32LE(nwkHeader.securityFrameCounter ?? 0, offset);
        offset = data.writeUInt8(cmdId, offset);
        offset = data.writeUInt8(payload.byteLength, offset);
        payload.copy(data, offset);
        const zclPayload = {
            clusterID: 0x21 /* Green Power */,
            header: Zcl.Header.fromBuffer(data),
            address: macHeader.source64 !== undefined /* v8 ignore next */
                ? /* v8 ignore next */ `0x${(0, utils_2.bigUInt64ToHexBE)(macHeader.source64)}`
                : // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
                    nwkHeader.sourceId & 0xffff,
            data,
            endpoint: ZSpec.GP_ENDPOINT,
            linkquality: rssi, // TODO: convert RSSI to LQA
            groupID: ZSpec.GP_GROUP_ID,
            wasBroadcast: false,
            destinationEndpoint: ZSpec.GP_ENDPOINT,
        };
        if (zclPayload.header !== undefined) {
            this.zclWaitress.resolve(zclPayload);
        }
        this.emit("zclPayload", zclPayload);
    }
    onDeviceJoined(source16, source64, capabilities) {
        // XXX: don't delay if no cap? (joined through router)
        if (capabilities?.rxOnWhenIdle) {
            this.emit("deviceJoined", { networkAddress: source16, ieeeAddr: `0x${(0, utils_2.bigUInt64ToHexBE)(source64)}` });
        }
        else {
            // XXX: end devices can be finicky about finishing the key authorization, Z2M interview can create a bottleneck, so delay it
            setTimeout(() => {
                this.emit("deviceJoined", { networkAddress: source16, ieeeAddr: `0x${(0, utils_2.bigUInt64ToHexBE)(source64)}` });
            }, 5000);
        }
    }
    onDeviceRejoined(source16, source64, _capabilities) {
        this.emit("deviceJoined", { networkAddress: source16, ieeeAddr: `0x${(0, utils_2.bigUInt64ToHexBE)(source64)}` });
    }
    onDeviceLeft(source16, source64) {
        this.emit("deviceLeave", { networkAddress: source16, ieeeAddr: `0x${(0, utils_2.bigUInt64ToHexBE)(source64)}` });
    }
    /* v8 ignore start */
    onDeviceAuthorized(_source16, _source64) { }
    /* v8 ignore stop */
    zdoWaitressValidator(payload, matcher) {
        return matcher.address === payload.address && matcher.clusterId === payload.clusterId && matcher.transactionSequenceNumber === payload.seqNum;
    }
}
exports.ZoHAdapter = ZoHAdapter;
//# sourceMappingURL=zohAdapter.js.map