"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.ZBOSSAdapter = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const __1 = require("../..");
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 const_1 = require("../../const");
const driver_1 = require("../driver");
const enums_1 = require("../enums");
const frame_1 = require("../frame");
const NS = 'zh:zboss';
class ZBOSSAdapter extends __1.Adapter {
    queue;
    driver;
    waitress;
    currentManufacturerCode;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = false;
        this.manufacturerID = Zcl.ManufacturerCode.NORDIC_SEMICONDUCTOR_ASA;
        this.currentManufacturerCode = Zcl.ManufacturerCode.NORDIC_SEMICONDUCTOR_ASA;
        const concurrent = adapterOptions && adapterOptions.concurrent ? adapterOptions.concurrent : 8;
        logger_1.logger.debug(`Adapter concurrent: ${concurrent}`, NS);
        this.queue = new utils_1.Queue(concurrent);
        this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter);
        this.driver = new driver_1.ZBOSSDriver(serialPortOptions, networkOptions);
        this.driver.on('frame', this.processMessage.bind(this));
    }
    async processMessage(frame) {
        logger_1.logger.debug(() => `processMessage: ${JSON.stringify(frame)}`, NS);
        if (frame.payload.zdoClusterId !== undefined) {
            this.emit('zdoResponse', frame.payload.zdoClusterId, frame.payload.zdo);
        }
        else if (frame.type == frame_1.FrameType.INDICATION) {
            switch (frame.commandId) {
                case enums_1.CommandId.ZDO_DEV_UPDATE_IND: {
                    logger_1.logger.debug(`Device ${frame.payload.ieee}:${frame.payload.nwk} ${enums_1.DeviceUpdateStatus[frame.payload.status]}.`, NS);
                    if (frame.payload.status === enums_1.DeviceUpdateStatus.LEFT) {
                        this.emit('deviceLeave', {
                            networkAddress: frame.payload.nwk,
                            ieeeAddr: frame.payload.ieee,
                        });
                    }
                    else {
                        // set workaround manuf code if necessary, or revert to default if previous joined device required workaround and new one does not
                        const joinManufCode = const_1.WORKAROUND_JOIN_MANUF_IEEE_PREFIX_TO_CODE[frame.payload.ieee.substring(0, 8)] ?? this.manufacturerID;
                        if (this.currentManufacturerCode !== joinManufCode) {
                            logger_1.logger.debug(`[WORKAROUND] Setting coordinator manufacturer code to ${Zcl.ManufacturerCode[joinManufCode]}.`, NS);
                            await this.driver.execCommand(enums_1.CommandId.ZDO_SET_NODE_DESC_MANUF_CODE, { manufacturerCode: joinManufCode });
                            this.currentManufacturerCode = joinManufCode;
                        }
                        // SECURE_REJOIN, UNSECURE_JOIN, TC_REJOIN
                        this.emit('deviceJoined', {
                            networkAddress: frame.payload.nwk,
                            ieeeAddr: frame.payload.ieee,
                        });
                    }
                    break;
                }
                case enums_1.CommandId.NWK_LEAVE_IND: {
                    this.emit('deviceLeave', {
                        networkAddress: frame.payload.nwk,
                        ieeeAddr: frame.payload.ieee,
                    });
                    break;
                }
                case enums_1.CommandId.APSDE_DATA_IND: {
                    const payload = {
                        clusterID: frame.payload.clusterID,
                        header: Zcl.Header.fromBuffer(frame.payload.data),
                        data: frame.payload.data,
                        address: frame.payload.srcNwk,
                        endpoint: frame.payload.srcEndpoint,
                        linkquality: frame.payload.lqi,
                        groupID: frame.payload.grpNwk,
                        wasBroadcast: false,
                        destinationEndpoint: frame.payload.dstEndpoint,
                    };
                    this.waitress.resolve(payload);
                    this.emit('zclPayload', payload);
                    break;
                }
            }
        }
    }
    async start() {
        logger_1.logger.info(`ZBOSS Adapter starting`, NS);
        await this.driver.connect();
        return await this.driver.startup(this.adapterOptions.transmitPower);
    }
    async stop() {
        await this.driver.stop();
        logger_1.logger.info(`ZBOSS Adapter stopped`, NS);
    }
    async getCoordinatorIEEE() {
        return this.driver.netInfo.ieeeAddr;
    }
    async getCoordinatorVersion() {
        return await this.queue.execute(async () => {
            const ver = await this.driver.execCommand(enums_1.CommandId.GET_MODULE_VERSION, {});
            const cver = await this.driver.execCommand(enums_1.CommandId.GET_COORDINATOR_VERSION, {});
            const ver2str = (version) => {
                const major = (version >> 24) & 0xff;
                const minor = (version >> 16) & 0xff;
                const revision = (version >> 8) & 0xff;
                const commit = version & 0xff;
                return `${major}.${minor}.${revision}.${commit}`;
            };
            return {
                type: `zboss`,
                meta: {
                    coordinator: cver.payload.version,
                    stack: ver2str(ver.payload.stackVersion),
                    protocol: ver2str(ver.payload.protocolVersion),
                    revision: ver2str(ver.payload.fwVersion),
                },
            };
        });
    }
    async reset(type) {
        throw new Error(`This adapter does not reset '${type}'`);
    }
    async supportsBackup() {
        return false;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async backup(ieeeAddressesInDatabase) {
        throw new Error('This adapter does not support backup');
    }
    async getNetworkParameters() {
        return await this.queue.execute(async () => {
            const channel = this.driver.netInfo.network.channel;
            const panID = this.driver.netInfo.network.panID;
            const extendedPanID = this.driver.netInfo.network.extendedPanID;
            return {
                panID,
                extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
                channel,
                nwkUpdateID: 0,
            };
        });
    }
    async addInstallCode(ieeeAddress, key, hashed) {
        logger_1.logger.error(`NOT SUPPORTED: sendZclFrameToGroup(${ieeeAddress},${key.toString('hex')},${hashed}`, NS);
        throw new Error(`Install code is not supported for 'zboss' yet`);
    }
    async permitJoin(seconds, networkAddress) {
        if (this.driver.isInitialized()) {
            if (this.currentManufacturerCode !== this.manufacturerID) {
                logger_1.logger.debug(`[WORKAROUND] Resetting coordinator manufacturer code to ${Zcl.ManufacturerCode[this.manufacturerID]}.`, NS);
                await this.driver.execCommand(enums_1.CommandId.ZDO_SET_NODE_DESC_MANUF_CODE, { manufacturerCode: this.manufacturerID });
                this.currentManufacturerCode = this.manufacturerID;
            }
            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) {
                // `device-only`
                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` (for `all` too)
                const result = await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.COORDINATOR_ADDRESS, clusterId, zdoPayload, false);
                if (!Zdo.Buffalo.checkStatus(result)) {
                    // TODO: will disappear once moved upstream
                    throw new Zdo.StatusError(result[0]);
                }
                if (networkAddress === undefined) {
                    // `all`: broadcast
                    await this.sendZdo(ZSpec.BLANK_EUI64, ZSpec.BroadcastAddress.DEFAULT, clusterId, zdoPayload, true);
                }
            }
        }
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        return await this.queue.execute(async () => {
            // stack-specific requirements
            switch (clusterId) {
                case Zdo.ClusterId.NETWORK_ADDRESS_REQUEST:
                case Zdo.ClusterId.IEEE_ADDRESS_REQUEST:
                case Zdo.ClusterId.BIND_REQUEST:
                case Zdo.ClusterId.UNBIND_REQUEST:
                case Zdo.ClusterId.LQI_TABLE_REQUEST:
                case Zdo.ClusterId.ROUTING_TABLE_REQUEST:
                case Zdo.ClusterId.BINDING_TABLE_REQUEST:
                case Zdo.ClusterId.LEAVE_REQUEST:
                case Zdo.ClusterId.PERMIT_JOINING_REQUEST: {
                    const prefixedPayload = Buffer.alloc(payload.length + 2);
                    prefixedPayload.writeUInt16LE(networkAddress, 0);
                    prefixedPayload.set(payload, 2);
                    payload = prefixedPayload;
                    break;
                }
            }
            switch (clusterId) {
                case Zdo.ClusterId.BIND_REQUEST:
                case Zdo.ClusterId.UNBIND_REQUEST: {
                    // use fixed size address
                    const addrType = payload.readUInt8(13); // address type
                    if (addrType == Zdo.MULTICAST_BINDING) {
                        payload = Buffer.concat([payload, Buffer.alloc(7)]);
                    }
                    break;
                }
            }
            const zdoResponseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
            const frame = await this.driver.requestZdo(clusterId, payload, disableResponse || zdoResponseClusterId === undefined);
            if (!disableResponse && zdoResponseClusterId !== undefined) {
                (0, node_assert_1.default)(frame, `ZDO ${Zdo.ClusterId[clusterId]} expected response ${Zdo.ClusterId[zdoResponseClusterId]}.`);
                return frame.payload.zdo;
            }
        }, networkAddress);
    }
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint) {
        return await this.queue.execute(async () => {
            return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint || 1, zclFrame, timeout, disableResponse, disableRecovery, 0, 0, false, false, false, null);
        }, networkAddress);
    }
    async sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore) {
        if (ieeeAddr == null) {
            ieeeAddr = this.driver.netInfo.ieeeAddr;
        }
        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 && 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);
        }
        try {
            const dataConfirmResult = await this.driver.request(ieeeAddr, 0x0104, zclFrame.cluster.ID, endpoint, sourceEndpoint || 0x01, 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, checkedNetworkAddress, discoveredRoute, assocRemove, assocRestore);
                    }
                    else {
                        throw error;
                    }
                }
            }
            else {
                return;
            }
        }
        catch (error) {
            if (response != null) {
                response.cancel();
            }
            throw error;
        }
    }
    async sendZclFrameToGroup(groupID, zclFrame, sourceEndpoint) {
        await this.driver.grequest(groupID, sourceEndpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID, zclFrame.cluster.ID, sourceEndpoint || 0x01, zclFrame.toBuffer());
    }
    async sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, destination) {
        await this.driver.brequest(destination, sourceEndpoint === ZSpec.GP_ENDPOINT && endpoint === ZSpec.GP_ENDPOINT ? ZSpec.GP_PROFILE_ID : ZSpec.HA_PROFILE_ID, zclFrame.cluster.ID, endpoint, sourceEndpoint || 0x01, zclFrame.toBuffer());
    }
    async setChannelInterPAN(channel) {
        logger_1.logger.error(`NOT SUPPORTED: setChannelInterPAN(${channel})`, NS);
        return;
    }
    async sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddress) {
        logger_1.logger.error(`NOT SUPPORTED: sendZclFrameInterPANToIeeeAddr(${JSON.stringify(zclFrame)},${ieeeAddress})`, NS);
        return;
    }
    async sendZclFrameInterPANBroadcast(zclFrame, timeout) {
        logger_1.logger.error(`NOT SUPPORTED: sendZclFrameInterPANBroadcast(${JSON.stringify(zclFrame)},${timeout})`, NS);
        throw new Error(`Is not supported for 'zboss' yet`);
    }
    async restoreChannelInterPAN() {
        return;
    }
    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 ((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) ||
            false);
    }
}
exports.ZBOSSAdapter = ZBOSSAdapter;
//# sourceMappingURL=zbossAdapter.js.map