"use strict";
/* v8 ignore start */
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.EZSPAdapter = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
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 driver_1 = require("../driver");
const types_1 = require("../driver/types");
const NS = 'zh:ezsp';
class EZSPAdapter extends adapter_1.default {
    driver;
    waitress;
    interpanLock;
    queue;
    closing;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = true;
        this.manufacturerID = Zcl.ManufacturerCode.SILICON_LABORATORIES;
        this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter);
        this.interpanLock = false;
        this.closing = false;
        const concurrent = adapterOptions && adapterOptions.concurrent ? adapterOptions.concurrent : 8;
        logger_1.logger.debug(`Adapter concurrent: ${concurrent}`, NS);
        this.queue = new utils_1.Queue(concurrent);
        this.driver = new driver_1.Driver(this.serialPortOptions, this.networkOptions, backupPath);
        this.driver.on('close', this.onDriverClose.bind(this));
        this.driver.on('deviceJoined', this.handleDeviceJoin.bind(this));
        this.driver.on('deviceLeft', this.handleDeviceLeft.bind(this));
        this.driver.on('incomingMessage', this.processMessage.bind(this));
    }
    async processMessage(frame) {
        logger_1.logger.debug(() => `processMessage: ${JSON.stringify(frame)}`, NS);
        if (frame.apsFrame.profileId == Zdo.ZDO_PROFILE_ID) {
            if (frame.apsFrame.clusterId >= 0x8000 /* response only */) {
                this.emit('zdoResponse', frame.apsFrame.clusterId, frame.zdoResponse);
            }
        }
        else if (frame.apsFrame.profileId == ZSpec.HA_PROFILE_ID || frame.apsFrame.profileId == 0xffff) {
            const payload = {
                clusterID: frame.apsFrame.clusterId,
                header: Zcl.Header.fromBuffer(frame.message),
                data: frame.message,
                address: frame.sender,
                endpoint: frame.apsFrame.sourceEndpoint,
                linkquality: frame.lqi,
                groupID: frame.apsFrame.groupId ?? 0,
                wasBroadcast: false, // TODO
                destinationEndpoint: frame.apsFrame.destinationEndpoint,
            };
            this.waitress.resolve(payload);
            this.emit('zclPayload', payload);
        }
        else if (frame.apsFrame.profileId == ZSpec.TOUCHLINK_PROFILE_ID && frame.senderEui64) {
            // ZLL Frame
            const payload = {
                clusterID: frame.apsFrame.clusterId,
                header: Zcl.Header.fromBuffer(frame.message),
                data: frame.message,
                address: `0x${frame.senderEui64.toString()}`,
                endpoint: 0xfe,
                linkquality: frame.lqi,
                groupID: 0,
                wasBroadcast: false,
                destinationEndpoint: 1,
            };
            this.waitress.resolve(payload);
            this.emit('zclPayload', payload);
        }
        else if (frame.apsFrame.profileId == ZSpec.GP_PROFILE_ID) {
            // GP Frame
            // Only handle when clusterId == 33 (greenPower), some devices send messages with this profileId
            // while the cluster is not greenPower
            // https://github.com/Koenkk/zigbee2mqtt/issues/20838
            if (frame.apsFrame.clusterId === Zcl.Clusters.greenPower.ID) {
                const payload = {
                    header: Zcl.Header.fromBuffer(frame.message),
                    clusterID: frame.apsFrame.clusterId,
                    data: frame.message,
                    address: frame.sender,
                    endpoint: frame.apsFrame.sourceEndpoint,
                    linkquality: frame.lqi,
                    groupID: 0,
                    wasBroadcast: true,
                    destinationEndpoint: frame.apsFrame.sourceEndpoint,
                };
                this.waitress.resolve(payload);
                this.emit('zclPayload', payload);
            }
            else {
                logger_1.logger.debug(`Ignoring GP frame because clusterId is not greenPower`, NS);
            }
        }
    }
    async handleDeviceJoin(nwk, ieee) {
        logger_1.logger.debug(() => `Device join request received: ${nwk} ${ieee.toString()}`, NS);
        this.emit('deviceJoined', {
            networkAddress: nwk,
            ieeeAddr: `0x${ieee.toString()}`,
        });
    }
    handleDeviceLeft(nwk, ieee) {
        logger_1.logger.debug(() => `Device left network request received: ${nwk} ${ieee.toString()}`, NS);
        this.emit('deviceLeave', {
            networkAddress: nwk,
            ieeeAddr: `0x${ieee.toString()}`,
        });
    }
    /**
     * Adapter methods
     */
    async start() {
        logger_1.logger.warning(`'ezsp' driver is deprecated and will only remain to provide support for older firmware (pre 7.4.x). Migration to 'ember' is recommended. If using Zigbee2MQTT see https://github.com/Koenkk/zigbee2mqtt/discussions/21462`, NS);
        return await this.driver.startup(this.adapterOptions.transmitPower);
    }
    async stop() {
        this.closing = true;
        await this.driver.stop();
    }
    async onDriverClose() {
        logger_1.logger.debug(`onDriverClose()`, NS);
        if (!this.closing) {
            this.emit('disconnected');
        }
    }
    async getCoordinatorIEEE() {
        return `0x${this.driver.ieee.toString()}`;
    }
    async permitJoin(seconds, networkAddress) {
        if (!this.driver.ezsp.isInitialized()) {
            return;
        }
        const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
        if (networkAddress) {
            // specific device that is not `Coordinator`
            await this.queue.execute(async () => {
                this.checkInterpanLock();
                await this.driver.preJoining(seconds);
            });
            // `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);
            if (!Zdo.Buffalo.checkStatus(result)) {
                // TODO: will disappear once moved upstream
                throw new Zdo.StatusError(result[0]);
            }
        }
        else {
            // coordinator-only (0), or all
            await this.queue.execute(async () => {
                this.checkInterpanLock();
                await this.driver.preJoining(seconds);
            });
            const result = await this.driver.permitJoining(seconds);
            if (result.status !== types_1.EmberStatus.SUCCESS) {
                throw new Error(`[ZDO] Failed coordinator permit joining request with status=${result.status}.`);
            }
            logger_1.logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS);
            // broadcast permit joining ZDO
            if (networkAddress === undefined) {
                // `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);
            }
        }
    }
    async getCoordinatorVersion() {
        return { type: `EZSP v${this.driver.version.product}`, meta: this.driver.version };
    }
    async addInstallCode(ieeeAddress, key, hashed) {
        if ([8, 10, 14, 16, 18].indexOf(key.length) === -1) {
            throw new Error('Wrong install code length');
        }
        await this.driver.addInstallCode(ieeeAddress, key, hashed);
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async reset(type) {
        return await Promise.reject(new Error('Not supported'));
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            const clusterName = Zdo.ClusterId[clusterId];
            const frame = this.driver.makeApsFrame(clusterId, disableResponse);
            payload[0] = frame.sequence;
            let waiter;
            let responseClusterId;
            if (!disableResponse) {
                responseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
                if (responseClusterId) {
                    waiter = this.driver.waitFor(responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress, responseClusterId, frame.sequence);
                }
            }
            if (ZSpec.Utils.isBroadcastAddress(networkAddress)) {
                logger_1.logger.debug(() => `~~~> [ZDO ${clusterName} BROADCAST to=${networkAddress} payload=${payload.toString('hex')}]`, NS);
                const req = await this.driver.brequest(networkAddress, frame, payload);
                logger_1.logger.debug(`~~~> [SENT ZDO BROADCAST]`, NS);
                if (!req) {
                    waiter?.cancel();
                    throw new Error(`~x~> [ZDO ${clusterName} BROADCAST to=${networkAddress}] Failed to send request.`);
                }
            }
            else {
                logger_1.logger.debug(() => `~~~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} payload=${payload.toString('hex')}]`, NS);
                const req = await this.driver.request(networkAddress, frame, payload);
                logger_1.logger.debug(`~~~> [SENT ZDO UNICAST]`, NS);
                if (!req) {
                    waiter?.cancel();
                    throw new Error(`~x~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress}] Failed to send request.`);
                }
            }
            if (waiter && responseClusterId !== undefined) {
                const response = await waiter.start().promise;
                logger_1.logger.debug(() => `<~~ [ZDO ${Zdo.ClusterId[responseClusterId]} ${JSON.stringify(response.zdoResponse)}]`, NS);
                return response.zdoResponse;
            }
        }, networkAddress);
    }
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint || 1, zclFrame, timeout, disableResponse, disableRecovery, 0, 0);
        }, networkAddress);
    }
    async sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt) {
        if (ieeeAddr == null) {
            ieeeAddr = `0x${this.driver.ieee.toString()}`;
        }
        logger_1.logger.debug(`sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} ` +
            `(${responseAttempt},${dataRequestAttempt},${this.queue.count()}), timeout=${timeout}`, NS);
        let response = null;
        const command = zclFrame.command;
        if (command.response != undefined && disableResponse === false) {
            response = this.waitForInternal(networkAddress, endpoint, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, command.response, timeout);
        }
        else if (!zclFrame.header.frameControl.disableDefaultResponse) {
            response = this.waitForInternal(networkAddress, endpoint, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout);
        }
        const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, disableResponse || zclFrame.header.frameControl.disableDefaultResponse);
        frame.profileId = ZSpec.HA_PROFILE_ID;
        frame.sourceEndpoint = sourceEndpoint || 0x01;
        frame.destinationEndpoint = endpoint;
        frame.groupId = 0;
        this.driver.setNode(networkAddress, new types_1.EmberEUI64(ieeeAddr));
        const dataConfirmResult = await this.driver.request(networkAddress, frame, zclFrame.toBuffer());
        if (!dataConfirmResult) {
            if (response != null) {
                response.cancel();
            }
            throw Error('sendZclFrameToEndpointInternal error');
        }
        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) {
                    return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt + 1, dataRequestAttempt);
                }
                else {
                    throw error;
                }
            }
        }
    }
    async sendZclFrameToGroup(groupID, zclFrame) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false);
            frame.profileId = ZSpec.HA_PROFILE_ID;
            frame.sourceEndpoint = 0x01;
            frame.destinationEndpoint = 0x01;
            frame.groupId = groupID;
            await this.driver.mrequest(frame, zclFrame.toBuffer());
            /**
             * 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) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            const frame = this.driver.makeApsFrame(zclFrame.cluster.ID, false);
            frame.profileId = sourceEndpoint === ZSpec.GP_ENDPOINT && endpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID;
            frame.sourceEndpoint = sourceEndpoint;
            frame.destinationEndpoint = endpoint;
            frame.groupId = destination;
            // XXX: should be:
            // await this.driver.brequest(destination, frame, zclFrame.toBuffer())
            await this.driver.mrequest(frame, zclFrame.toBuffer());
            /**
             * 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 getNetworkParameters() {
        return {
            panID: this.driver.networkParams.panId,
            extendedPanID: ZSpec.Utils.eui64LEBufferToHex(this.driver.networkParams.extendedPanId),
            channel: this.driver.networkParams.radioChannel,
            nwkUpdateID: this.driver.networkParams.nwkUpdateId,
        };
    }
    async supportsBackup() {
        return true;
    }
    async backup() {
        (0, node_assert_1.default)(this.driver.ezsp.isInitialized(), 'Cannot make backup when ezsp is not initialized');
        return await this.driver.backupMan.createBackup();
    }
    async restoreChannelInterPAN() {
        return await this.queue.execute(async () => {
            const channel = (await this.getNetworkParameters()).channel;
            await this.driver.setChannel(channel);
            // Give adapter some time to restore, otherwise stuff crashes
            await (0, utils_1.wait)(3000);
            this.interpanLock = false;
        });
    }
    checkInterpanLock() {
        if (this.interpanLock) {
            throw new Error(`Cannot execute command, in Inter-PAN mode`);
        }
    }
    async sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddr) {
        return await this.queue.execute(async () => {
            logger_1.logger.debug(`sendZclFrameInterPANToIeeeAddr to ${ieeeAddr}`, NS);
            const frame = this.driver.makeEmberIeeeRawFrame();
            frame.ieeeFrameControl = 0xcc21;
            frame.destPanId = 0xffff;
            frame.destAddress = new types_1.EmberEUI64(ieeeAddr);
            frame.sourcePanId = this.driver.networkParams.panId;
            frame.sourceAddress = this.driver.ieee;
            frame.nwkFrameControl = 0x000b;
            frame.appFrameControl = 0x03;
            frame.clusterId = zclFrame.cluster.ID;
            frame.profileId = 0xc05e;
            await this.driver.ieeerawrequest(frame, zclFrame.toBuffer());
        });
    }
    async sendZclFrameInterPANBroadcast(zclFrame, timeout) {
        return await this.queue.execute(async () => {
            logger_1.logger.debug(`sendZclFrameInterPANBroadcast`, NS);
            const command = zclFrame.command;
            if (command.response == undefined) {
                throw new Error(`Command '${command.name}' has no response, cannot wait for response`);
            }
            const response = this.waitForInternal(undefined, 0xfe, undefined, zclFrame.cluster.ID, command.response, timeout);
            try {
                const frame = this.driver.makeEmberRawFrame();
                frame.ieeeFrameControl = 0xc801;
                frame.destPanId = 0xffff;
                frame.destNodeId = 0xffff;
                frame.sourcePanId = this.driver.networkParams.panId;
                frame.ieeeAddress = this.driver.ieee;
                frame.nwkFrameControl = 0x000b;
                frame.appFrameControl = 0x0b;
                frame.clusterId = zclFrame.cluster.ID;
                frame.profileId = 0xc05e;
                await this.driver.rawrequest(frame, zclFrame.toBuffer());
            }
            catch (error) {
                response.cancel();
                throw error;
            }
            return await response.start().promise;
        });
    }
    async setChannelInterPAN(channel) {
        return await this.queue.execute(async () => {
            this.interpanLock = true;
            await this.driver.setChannel(channel);
        });
    }
    waitForInternal(networkAddress, endpoint, transactionSequenceNumber, clusterID, commandIdentifier, timeout) {
        const waiter = this.waitress.waitFor({
            address: networkAddress,
            endpoint,
            clusterID,
            commandIdentifier,
            transactionSequenceNumber,
        }, 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, transactionSequenceNumber, clusterID, commandIdentifier, timeout);
        return { cancel: waiter.cancel, promise: waiter.start().promise };
    }
    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 || payload.header.transactionSequenceNumber === matcher.transactionSequenceNumber) &&
            payload.clusterID === matcher.clusterID &&
            matcher.commandIdentifier === payload.header.commandIdentifier);
    }
}
exports.EZSPAdapter = EZSPAdapter;
//# sourceMappingURL=ezspAdapter.js.map