"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.EmberAdapter = exports.DEFAULT_APS_OPTIONS = exports.DEFAULT_STACK_CONFIG = void 0;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const node_path_1 = __importDefault(require("node:path"));
const es6_1 = __importDefault(require("fast-deep-equal/es6"));
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 consts_1 = require("../consts");
const enums_1 = require("../enums");
const buffalo_1 = require("../ezsp/buffalo");
const consts_2 = require("../ezsp/consts");
const enums_2 = require("../ezsp/enums");
const ezsp_1 = require("../ezsp/ezsp");
const ezspError_1 = require("../ezspError");
const initters_1 = require("../utils/initters");
const math_1 = require("../utils/math");
const endpoints_1 = require("./endpoints");
const oneWaitress_1 = require("./oneWaitress");
const NS = 'zh:ember';
var NetworkInitAction;
(function (NetworkInitAction) {
    /** Ain't that nice! */
    NetworkInitAction[NetworkInitAction["DONE"] = 0] = "DONE";
    /** Config mismatch, must leave network. */
    NetworkInitAction[NetworkInitAction["LEAVE"] = 1] = "LEAVE";
    /** Config mismatched, left network. Will evaluate forming from backup or config next. */
    NetworkInitAction[NetworkInitAction["LEFT"] = 2] = "LEFT";
    /** Form the network using config. No backup, or backup mismatch. */
    NetworkInitAction[NetworkInitAction["FORM_CONFIG"] = 3] = "FORM_CONFIG";
    /** Re-form the network using full backed-up data. */
    NetworkInitAction[NetworkInitAction["FORM_BACKUP"] = 4] = "FORM_BACKUP";
})(NetworkInitAction || (NetworkInitAction = {}));
/**
 * Application generated ZDO messages use sequence numbers 0-127, and the stack
 * uses sequence numbers 128-255.  This simplifies life by eliminating the need
 * for coordination between the two entities, and allows both to send ZDO
 * messages with non-conflicting sequence numbers.
 */
const APPLICATION_ZDO_SEQUENCE_MASK = 0x7f;
/* Default radius used for broadcast ZDO requests. uint8_t */
const ZDO_REQUEST_RADIUS = 0xff;
/** Oldest supported EZSP version for backups. Don't take the risk to restore a broken network until older backup versions can be investigated. */
const BACKUP_OLDEST_SUPPORTED_EZSP_VERSION = 12;
/**
 * 9sec is minimum recommended for `ezspBroadcastNextNetworkKey` to have propagated throughout network.
 * NOTE: This is blocking the request queue, so we shouldn't go crazy high.
 */
const BROADCAST_NETWORK_KEY_SWITCH_WAIT_TIME = 15000;
const QUEUE_MAX_SEND_ATTEMPTS = 3;
const QUEUE_BUSY_DEFER_MSEC = 500;
const QUEUE_NETWORK_DOWN_DEFER_MSEC = 1500;
/**
 * Default stack configuration values.
 * @see https://www.silabs.com/documents/public/user-guides/ug100-ezsp-reference-guide.pdf 2.3.1 for descriptions/RAM costs
 *
 * https://github.com/darkxst/silabs-firmware-builder/tree/main/manifests
 * https://github.com/NabuCasa/silabs-firmware/wiki/Zigbee-EmberZNet-NCP-firmware-configuration#skyconnect
 * https://github.com/SiliconLabs/UnifySDK/blob/main/applications/zigbeed/project_files/zigbeed.slcp
 */
exports.DEFAULT_STACK_CONFIG = {
    CONCENTRATOR_RAM_TYPE: 'high',
    CONCENTRATOR_MIN_TIME: 5, // zigpc: 10
    CONCENTRATOR_MAX_TIME: 60, // zigpc: 60
    CONCENTRATOR_ROUTE_ERROR_THRESHOLD: 3, // zigpc: 3
    CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD: 1, // zigpc: 1, ZigbeeMinimalHost: 3
    CONCENTRATOR_MAX_HOPS: 0, // zigpc: 0
    MAX_END_DEVICE_CHILDREN: 32, // zigpc: 6, nabucasa: 32, Dongle-E (Sonoff firmware): 32
    TRANSIENT_DEVICE_TIMEOUT: 10000,
    END_DEVICE_POLL_TIMEOUT: 8, // zigpc: 8
    TRANSIENT_KEY_TIMEOUT_S: 300, // zigpc: 65535
    CCA_MODE: undefined, // not set by default
};
/** Default behavior is to disable app key requests */
const ALLOW_APP_KEY_REQUESTS = false;
/** @see EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE */
const TRUST_CENTER_ADDRESS_CACHE_SIZE = 2;
/**
 * NOTE: This from SDK is currently ignored here because of issues in below links:
 * - BUGZID 12261: Concentrators use MTORRs for route discovery and should not enable route discovery in the APS options.
 * - https://community.silabs.com/s/question/0D58Y00008DRfDCSA1/coordinator-cant-send-unicast-to-sleepy-node-after-reboot
 * - https://community.silabs.com/s/question/0D58Y0000B4nTb7SQE/largedense-network-communication-problem-source-route-table-not-big-enough
 *
 * Removing `ENABLE_ROUTE_DISCOVERY` leads to devices that won't reconnect/go offline, and various other issues. Keeping it for now.
 */
exports.DEFAULT_APS_OPTIONS = enums_1.EmberApsOption.RETRY | enums_1.EmberApsOption.ENABLE_ROUTE_DISCOVERY | enums_1.EmberApsOption.ENABLE_ADDRESS_DISCOVERY;
/** Time for a request to get a callback response. ASH is 2400*6 for ACK timeout. */
const DEFAULT_REQUEST_TIMEOUT = 15000; // msec
/** Time for a network-related request to get a response (usually via event). */
const DEFAULT_NETWORK_REQUEST_TIMEOUT = 10000; // nothing on the network to bother requests, should be much faster than this
/** Time between watchdog counters reading/clearing */
const WATCHDOG_COUNTERS_FEED_INTERVAL = 3600000; // every hour...
/** Default manufacturer code reported by coordinator. */
const DEFAULT_MANUFACTURER_CODE = Zcl.ManufacturerCode.SILICON_LABORATORIES;
/**
 * Relay calls between Z2M and EZSP-layer and handle any error that might occur via queue & waitress.
 *
 * Anything post `start` that requests anything from the EZSP layer must run through the request queue for proper execution flow.
 */
class EmberAdapter extends __1.Adapter {
    /** Current manufacturer code assigned to the coordinator. Used for join workarounds... */
    manufacturerCode;
    stackConfig;
    ezsp;
    version;
    queue;
    oneWaitress;
    /** Periodically retrieve counters then clear them. */
    watchdogCountersHandle;
    /** Sequence number used for ZDO requests. static uint8_t  */
    zdoRequestSequence;
    interpanLock;
    /**
     * Cached network params to avoid NCP calls. Prevents frequent EZSP transactions.
     * NOTE: Do not use directly, use getter functions for it that check if valid or need retrieval from NCP.
     */
    networkCache;
    multicastTable;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = true;
        this.manufacturerID = Zcl.ManufacturerCode.SILICON_LABORATORIES;
        this.version = {
            ezsp: 0,
            revision: 'unknown',
            build: 0,
            major: 0,
            minor: 0,
            patch: 0,
            special: 0,
            type: enums_1.EmberVersionType.GA,
        };
        this.zdoRequestSequence = 0; // start at 1
        this.interpanLock = false;
        this.networkCache = (0, initters_1.initNetworkCache)();
        this.manufacturerCode = DEFAULT_MANUFACTURER_CODE; // will be set in NCP in initEzsp
        this.multicastTable = [];
        this.stackConfig = this.loadStackConfig();
        this.queue = new utils_1.Queue(this.adapterOptions.concurrent || 16); // ORed to avoid 0 (not checked in settings/queue constructor)
        this.oneWaitress = new oneWaitress_1.EmberOneWaitress();
        this.ezsp = new ezsp_1.Ezsp(serialPortOptions);
        this.ezsp.on('zdoResponse', this.onZDOResponse.bind(this));
        this.ezsp.on('incomingMessage', this.onIncomingMessage.bind(this));
        this.ezsp.on('touchlinkMessage', this.onTouchlinkMessage.bind(this));
        this.ezsp.on('stackStatus', this.onStackStatus.bind(this));
        this.ezsp.on('trustCenterJoin', this.onTrustCenterJoin.bind(this));
        this.ezsp.on('messageSent', this.onMessageSent.bind(this));
        this.ezsp.once('ncpNeedsResetAndInit', this.onNcpNeedsResetAndInit.bind(this));
    }
    loadStackConfig() {
        // store stack config in same dir as backup
        const configPath = node_path_1.default.join(node_path_1.default.dirname(this.backupPath), 'stack_config.json');
        try {
            const customConfig = JSON.parse((0, node_fs_1.readFileSync)(configPath, 'utf8'));
            // set any undefined config to default
            const config = { ...exports.DEFAULT_STACK_CONFIG, ...customConfig };
            const inRange = (value, min, max) => (value == undefined || value < min || value > max ? false : true);
            if (!['high', 'low'].includes(config.CONCENTRATOR_RAM_TYPE)) {
                config.CONCENTRATOR_RAM_TYPE = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_RAM_TYPE;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_RAM_TYPE, using default.`, NS);
            }
            if (!inRange(config.CONCENTRATOR_MIN_TIME, 1, 60) || config.CONCENTRATOR_MIN_TIME >= config.CONCENTRATOR_MAX_TIME) {
                config.CONCENTRATOR_MIN_TIME = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_MIN_TIME;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_MIN_TIME, using default.`, NS);
            }
            if (!inRange(config.CONCENTRATOR_MAX_TIME, 30, 300) || config.CONCENTRATOR_MAX_TIME <= config.CONCENTRATOR_MIN_TIME) {
                config.CONCENTRATOR_MAX_TIME = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_TIME;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_MAX_TIME, using default.`, NS);
            }
            if (!inRange(config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, 1, 100)) {
                config.CONCENTRATOR_ROUTE_ERROR_THRESHOLD = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_ROUTE_ERROR_THRESHOLD;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_ROUTE_ERROR_THRESHOLD, using default.`, NS);
            }
            if (!inRange(config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, 1, 100)) {
                config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, using default.`, NS);
            }
            if (!inRange(config.CONCENTRATOR_MAX_HOPS, 0, 30)) {
                config.CONCENTRATOR_MAX_HOPS = exports.DEFAULT_STACK_CONFIG.CONCENTRATOR_MAX_HOPS;
                logger_1.logger.error(`[STACK CONFIG] Invalid CONCENTRATOR_MAX_HOPS, using default.`, NS);
            }
            if (!inRange(config.MAX_END_DEVICE_CHILDREN, 6, 64)) {
                config.MAX_END_DEVICE_CHILDREN = exports.DEFAULT_STACK_CONFIG.MAX_END_DEVICE_CHILDREN;
                logger_1.logger.error(`[STACK CONFIG] Invalid MAX_END_DEVICE_CHILDREN, using default.`, NS);
            }
            if (!inRange(config.TRANSIENT_DEVICE_TIMEOUT, 0, 65535)) {
                config.TRANSIENT_DEVICE_TIMEOUT = exports.DEFAULT_STACK_CONFIG.TRANSIENT_DEVICE_TIMEOUT;
                logger_1.logger.error(`[STACK CONFIG] Invalid TRANSIENT_DEVICE_TIMEOUT, using default.`, NS);
            }
            if (!inRange(config.END_DEVICE_POLL_TIMEOUT, 0, 14)) {
                config.END_DEVICE_POLL_TIMEOUT = exports.DEFAULT_STACK_CONFIG.END_DEVICE_POLL_TIMEOUT;
                logger_1.logger.error(`[STACK CONFIG] Invalid END_DEVICE_POLL_TIMEOUT, using default.`, NS);
            }
            if (!inRange(config.TRANSIENT_KEY_TIMEOUT_S, 0, 65535)) {
                config.TRANSIENT_KEY_TIMEOUT_S = exports.DEFAULT_STACK_CONFIG.TRANSIENT_KEY_TIMEOUT_S;
                logger_1.logger.error(`[STACK CONFIG] Invalid TRANSIENT_KEY_TIMEOUT_S, using default.`, NS);
            }
            config.CCA_MODE = config.CCA_MODE ?? undefined; // always default to undefined
            if (config.CCA_MODE && enums_1.IEEE802154CcaMode[config.CCA_MODE] === undefined) {
                config.CCA_MODE = undefined;
                logger_1.logger.error(`[STACK CONFIG] Invalid CCA_MODE, ignoring.`, NS);
            }
            logger_1.logger.info(`Using stack config ${JSON.stringify(config)}.`, NS);
            return config;
        }
        catch {
            /* empty */
        }
        logger_1.logger.info(`Using default stack config.`, NS);
        return exports.DEFAULT_STACK_CONFIG;
    }
    /**
     * Emitted from @see Ezsp.ezspStackStatusHandler
     * @param status
     */
    async onStackStatus(status) {
        // to be extra careful, should clear network cache upon receiving this.
        this.clearNetworkCache();
        switch (status) {
            case enums_1.SLStatus.NETWORK_UP: {
                this.oneWaitress.resolveEvent(oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_UP);
                logger_1.logger.info(`[STACK STATUS] Network up.`, NS);
                break;
            }
            case enums_1.SLStatus.NETWORK_DOWN: {
                this.oneWaitress.resolveEvent(oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_DOWN);
                logger_1.logger.info(`[STACK STATUS] Network down.`, NS);
                break;
            }
            case enums_1.SLStatus.ZIGBEE_NETWORK_OPENED: {
                this.oneWaitress.resolveEvent(oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_OPENED);
                logger_1.logger.info(`[STACK STATUS] Network opened.`, NS);
                break;
            }
            case enums_1.SLStatus.ZIGBEE_NETWORK_CLOSED: {
                this.oneWaitress.resolveEvent(oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_CLOSED);
                logger_1.logger.info(`[STACK STATUS] Network closed.`, NS);
                if (this.manufacturerCode !== DEFAULT_MANUFACTURER_CODE) {
                    await this.queue.execute(async () => {
                        logger_1.logger.debug(`[WORKAROUND] Reverting coordinator manufacturer code to default.`, NS);
                        await this.ezsp.ezspSetManufacturerCode(DEFAULT_MANUFACTURER_CODE);
                        this.manufacturerCode = DEFAULT_MANUFACTURER_CODE;
                    });
                }
                break;
            }
            case enums_1.SLStatus.ZIGBEE_CHANNEL_CHANGED: {
                // invalidate cache
                this.networkCache.parameters.radioChannel = consts_1.INVALID_RADIO_CHANNEL;
                logger_1.logger.info(`[STACK STATUS] Channel changed.`, NS);
                break;
            }
            default: {
                logger_1.logger.debug(`[STACK STATUS] ${enums_1.SLStatus[status]}.`, NS);
                break;
            }
        }
    }
    /**
     * Emitted from @see Ezsp.ezspMessageSentHandler
     * WARNING: Cannot rely on `ezspMessageSentHandler` > `ezspIncomingMessageHandler` order, some devices mix it up!
     *
     * @param type
     * @param indexOrDestination
     * @param apsFrame
     * @param messageTag
     * @param status
     */
    async onMessageSent(status, type, indexOrDestination, apsFrame, messageTag) {
        switch (status) {
            case enums_1.SLStatus.ZIGBEE_DELIVERY_FAILED: {
                logger_1.logger.debug(() => `~x~> DELIVERY_FAILED [indexOrDestination=${indexOrDestination} apsFrame=${JSON.stringify(apsFrame)} messageTag=${messageTag}]`, NS);
                // no ACK was received from the destination
                switch (type) {
                    case enums_1.EmberOutgoingMessageType.BROADCAST:
                    case enums_1.EmberOutgoingMessageType.BROADCAST_WITH_ALIAS:
                    case enums_1.EmberOutgoingMessageType.MULTICAST:
                    case enums_1.EmberOutgoingMessageType.MULTICAST_WITH_ALIAS: {
                        // BC/MC not checking for message sent, avoid unnecessary waitress lookups
                        logger_1.logger.error(`Delivery of ${enums_1.EmberOutgoingMessageType[type]} failed for '${indexOrDestination}'.`, NS);
                        break;
                    }
                    default: {
                        // reject any waitress early (don't wait for timeout if we know we're gonna get there eventually)
                        this.oneWaitress.deliveryFailedFor(indexOrDestination, apsFrame);
                        break;
                    }
                }
                break;
            }
            case enums_1.SLStatus.OK: {
                if (type === enums_1.EmberOutgoingMessageType.MULTICAST &&
                    apsFrame.destinationEndpoint === 0xff &&
                    apsFrame.groupId < consts_1.EMBER_MIN_BROADCAST_ADDRESS &&
                    !this.multicastTable.includes(apsFrame.groupId)) {
                    // workaround for devices using multicast for state update (coordinator passthrough)
                    const tableIdx = this.multicastTable.length;
                    const multicastEntry = {
                        multicastId: apsFrame.groupId,
                        endpoint: endpoints_1.FIXED_ENDPOINTS[0].endpoint,
                        networkIndex: endpoints_1.FIXED_ENDPOINTS[0].networkIndex,
                    };
                    // set immediately to avoid potential race
                    this.multicastTable.push(multicastEntry.multicastId);
                    try {
                        await this.queue.execute(async () => {
                            const status = await this.ezsp.ezspSetMulticastTableEntry(tableIdx, multicastEntry);
                            if (status !== enums_1.SLStatus.OK) {
                                throw new Error(`Failed to register group '${multicastEntry.multicastId}' in multicast table with status=${enums_1.SLStatus[status]}.`);
                            }
                            logger_1.logger.debug(() => `Registered multicast table entry (${tableIdx}): ${JSON.stringify(multicastEntry)}.`, NS);
                        });
                    }
                    catch (error) {
                        // remove to allow retry on next occurrence
                        this.multicastTable.splice(tableIdx, 1);
                        logger_1.logger.error(error.message, NS);
                    }
                }
                break;
            }
        }
        // shouldn't be any other status
    }
    /**
     * Emitted from @see Ezsp.ezspIncomingMessageHandler
     *
     * @param apsFrame The APS frame associated with the response.
     * @param sender The sender of the response. Should match `payload.nodeId` in many responses.
     * @param messageContents The content of the response.
     */
    async onZDOResponse(apsFrame, sender, messageContents) {
        const result = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, apsFrame.clusterId, messageContents);
        if (apsFrame.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.oneWaitress.resolveZDO(result[1].eui64, apsFrame, result);
            }
        }
        else {
            this.oneWaitress.resolveZDO(sender, apsFrame, result);
        }
        this.emit('zdoResponse', apsFrame.clusterId, result);
    }
    /**
     * Emitted from @see Ezsp.ezspIncomingMessageHandler @see Ezsp.ezspGpepIncomingMessageHandler
     *
     * @param type
     * @param apsFrame
     * @param lastHopLqi
     * @param sender
     * @param messageContents
     */
    async onIncomingMessage(type, apsFrame, lastHopLqi, sender, messageContents) {
        const payload = {
            clusterID: apsFrame.clusterId,
            header: Zcl.Header.fromBuffer(messageContents),
            address: sender,
            data: messageContents,
            endpoint: apsFrame.sourceEndpoint,
            linkquality: lastHopLqi,
            groupID: apsFrame.groupId,
            wasBroadcast: type === enums_1.EmberIncomingMessageType.BROADCAST || type === enums_1.EmberIncomingMessageType.BROADCAST_LOOPBACK,
            destinationEndpoint: apsFrame.destinationEndpoint,
        };
        this.oneWaitress.resolveZCL(payload);
        this.emit('zclPayload', payload);
    }
    /**
     * Emitted from @see Ezsp.ezspMacFilterMatchMessageHandler when the message is a valid InterPAN touchlink message.
     *
     * @param sourcePanId
     * @param sourceAddress
     * @param groupId
     * @param lastHopLqi
     * @param messageContents
     */
    async onTouchlinkMessage(sourcePanId, sourceAddress, groupId, lastHopLqi, messageContents) {
        const endpoint = endpoints_1.FIXED_ENDPOINTS[0].endpoint;
        const payload = {
            clusterID: Zcl.Clusters.touchlink.ID,
            data: messageContents,
            header: Zcl.Header.fromBuffer(messageContents),
            address: sourceAddress,
            endpoint: endpoint, // arbitrary since not sent over-the-air
            linkquality: lastHopLqi,
            groupID: groupId,
            wasBroadcast: true, // XXX: since always sent broadcast atm...
            destinationEndpoint: endpoint,
        };
        this.oneWaitress.resolveZCL(payload);
        this.emit('zclPayload', payload);
    }
    /**
     * Emitted from @see Ezsp.ezspTrustCenterJoinHandler
     * Also from @see Ezsp.ezspIdConflictHandler as a DEVICE_LEFT
     *
     * @param newNodeId
     * @param newNodeEui64
     * @param status
     * @param policyDecision
     * @param parentOfNewNodeId
     */
    async onTrustCenterJoin(newNodeId, newNodeEui64, status, policyDecision, parentOfNewNodeId) {
        if (status === enums_1.EmberDeviceUpdate.DEVICE_LEFT) {
            const payload = {
                networkAddress: newNodeId,
                ieeeAddr: newNodeEui64,
            };
            this.emit('deviceLeave', payload);
        }
        else {
            if (policyDecision !== enums_1.EmberJoinDecision.DENY_JOIN) {
                const payload = {
                    networkAddress: newNodeId,
                    ieeeAddr: newNodeEui64,
                };
                // 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[newNodeEui64.substring(0, 8)] ?? DEFAULT_MANUFACTURER_CODE;
                if (this.manufacturerCode !== joinManufCode) {
                    await this.queue.execute(async () => {
                        logger_1.logger.debug(`[WORKAROUND] Setting coordinator manufacturer code to ${Zcl.ManufacturerCode[joinManufCode]}.`, NS);
                        await this.ezsp.ezspSetManufacturerCode(joinManufCode);
                        this.manufacturerCode = joinManufCode;
                        this.emit('deviceJoined', payload);
                    });
                }
                else {
                    this.emit('deviceJoined', payload);
                }
            }
            else {
                logger_1.logger.warning(`[TRUST CENTER] Device ${newNodeId}:${newNodeEui64} was denied joining via ${parentOfNewNodeId}.`, NS);
            }
        }
    }
    async watchdogCounters() {
        await this.queue.execute(async () => {
            // listed as per EmberCounterType
            const ncpCounters = await this.ezsp.ezspReadAndClearCounters();
            logger_1.logger.info(`[NCP COUNTERS] ${ncpCounters.join(',')}`, NS);
            const ashCounters = this.ezsp.ash.readAndClearCounters();
            logger_1.logger.info(`[ASH COUNTERS] ${ashCounters.join(',')}`, NS);
        });
    }
    /**
     * Proceed to execute the long list of commands required to setup comms between Host<>NCP.
     * This is called by start and on internal reset.
     */
    async initEzsp() {
        let result = 'resumed';
        // NOTE: something deep in this call can throw too
        const startResult = await this.ezsp.start();
        if (startResult !== enums_1.EzspStatus.SUCCESS) {
            throw new Error(`Failed to start EZSP layer with status=${enums_1.EzspStatus[startResult]}.`);
        }
        // call before any other command, else fails
        await this.emberVersion();
        /** The address cache needs to be initialized and used with the source routing code for the trust center to operate properly. */
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.TRUST_CENTER_ADDRESS_CACHE_SIZE, TRUST_CENTER_ADDRESS_CACHE_SIZE);
        /** MAC indirect timeout should be 7.68 secs (STACK_PROFILE_ZIGBEE_PRO) */
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.INDIRECT_TRANSMISSION_TIMEOUT, 7680);
        /** Max hops should be 2 * nwkMaxDepth, where nwkMaxDepth is 15 (STACK_PROFILE_ZIGBEE_PRO) */
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.MAX_HOPS, 30);
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.SUPPORTED_NETWORKS, 1);
        // allow other devices to modify the binding table
        await this.emberSetEzspPolicy(enums_2.EzspPolicyId.BINDING_MODIFICATION_POLICY, enums_2.EzspDecisionId.CHECK_BINDING_MODIFICATIONS_ARE_VALID_ENDPOINT_CLUSTERS);
        // return message tag only in ezspMessageSentHandler()
        await this.emberSetEzspPolicy(enums_2.EzspPolicyId.MESSAGE_CONTENTS_IN_CALLBACK_POLICY, enums_2.EzspDecisionId.MESSAGE_TAG_ONLY_IN_CALLBACK);
        await this.emberSetEzspValue(enums_2.EzspValueId.TRANSIENT_DEVICE_TIMEOUT, 2, (0, math_1.lowHighBytes)(this.stackConfig.TRANSIENT_DEVICE_TIMEOUT));
        await this.ezsp.ezspSetManufacturerCode(this.manufacturerCode);
        // network security init
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.STACK_PROFILE, consts_1.STACK_PROFILE_ZIGBEE_PRO);
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.SECURITY_LEVEL, consts_1.SECURITY_LEVEL_Z3);
        // common configs
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.MAX_END_DEVICE_CHILDREN, this.stackConfig.MAX_END_DEVICE_CHILDREN);
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.END_DEVICE_POLL_TIMEOUT, this.stackConfig.END_DEVICE_POLL_TIMEOUT);
        await this.emberSetEzspConfigValue(enums_2.EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, this.stackConfig.TRANSIENT_KEY_TIMEOUT_S);
        // XXX: temp-fix: forces a side-effect in the firmware that prevents broadcast issues in environments with unusual interferences
        await this.emberSetEzspValue(enums_2.EzspValueId.CCA_THRESHOLD, 1, [0]);
        if (this.stackConfig.CCA_MODE) {
            // validated in `loadStackConfig`
            await this.ezsp.ezspSetRadioIeee802154CcaMode(enums_1.IEEE802154CcaMode[this.stackConfig.CCA_MODE]);
        }
        // WARNING: From here on EZSP commands that affect memory allocation on the NCP should no longer be called (like resizing tables)
        await this.registerFixedEndpoints();
        this.clearNetworkCache();
        result = await this.initTrustCenter();
        // after network UP, as per SDK, ensures clean slate
        await this.initNCPConcentrator();
        // populate network cache info
        const [status, , parameters] = await this.ezsp.ezspGetNetworkParameters();
        if (status !== enums_1.SLStatus.OK) {
            throw new Error(`Failed to get network parameters with status=${enums_1.SLStatus[status]}.`);
        }
        if (this.adapterOptions.transmitPower != undefined && parameters.radioTxPower !== this.adapterOptions.transmitPower) {
            const status = await this.ezsp.ezspSetRadioPower(this.adapterOptions.transmitPower);
            if (status !== enums_1.SLStatus.OK) {
                // soft-fail, don't prevent start
                logger_1.logger.error(`Failed to set transmit power to ${this.adapterOptions.transmitPower} status=${enums_1.SLStatus[status]}.`, NS);
            }
        }
        this.networkCache.parameters = parameters;
        this.networkCache.eui64 = await this.ezsp.ezspGetEui64();
        logger_1.logger.debug(() => `[INIT] Network Ready! ${JSON.stringify(this.networkCache)}`, NS);
        this.watchdogCountersHandle = setInterval(this.watchdogCounters.bind(this), WATCHDOG_COUNTERS_FEED_INTERVAL);
        return result;
    }
    /**
     * NCP concentrator init. Also enables source route discovery mode with RESCHEDULE.
     *
     * From AN1233:
     * To function correctly in a Zigbee PRO network, a trust center also requires that:
     *
     * 1. The trust center application must act as a concentrator (either high or low RAM).
     * 2. The trust center application must have support for source routing.
     *    It must record the source routes and properly handle requests by the stack for a particular source route.
     * 3. The trust center application must use an address cache for security, in order to maintain a mapping of IEEE address to short ID.
     *
     * Failure to satisfy all of the above requirements may result in failures when joining/rejoining devices to the network across multiple hops
     * (through a target node that is neither the trust center nor one of its neighboring routers.)
     */
    async initNCPConcentrator() {
        const status = await this.ezsp.ezspSetConcentrator(true, this.stackConfig.CONCENTRATOR_RAM_TYPE === 'low' ? consts_1.EMBER_LOW_RAM_CONCENTRATOR : consts_1.EMBER_HIGH_RAM_CONCENTRATOR, this.stackConfig.CONCENTRATOR_MIN_TIME, this.stackConfig.CONCENTRATOR_MAX_TIME, this.stackConfig.CONCENTRATOR_ROUTE_ERROR_THRESHOLD, this.stackConfig.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, this.stackConfig.CONCENTRATOR_MAX_HOPS);
        if (status !== enums_1.SLStatus.OK) {
            throw new Error(`[CONCENTRATOR] Failed to set concentrator with status=${enums_1.SLStatus[status]}.`);
        }
        const remainTilMTORR = await this.ezsp.ezspSetSourceRouteDiscoveryMode(enums_1.EmberSourceRouteDiscoveryMode.RESCHEDULE);
        logger_1.logger.info(`[CONCENTRATOR] Started source route discovery. ${remainTilMTORR}ms until next broadcast.`, NS);
    }
    /**
     * Register fixed endpoints and set any related multicast entries that need to be.
     */
    async registerFixedEndpoints() {
        for (const ep of endpoints_1.FIXED_ENDPOINTS) {
            const [epStatus] = await this.ezsp.ezspGetEndpointFlags(ep.endpoint);
            // endpoint not already registered
            if (epStatus !== enums_1.SLStatus.OK) {
                // check to see if ezspAddEndpoint needs to be called
                // if ezspInit is called without NCP reset, ezspAddEndpoint is not necessary and will return an error
                const status = await this.ezsp.ezspAddEndpoint(ep.endpoint, ep.profileId, ep.deviceId, ep.deviceVersion, ep.inClusterList.slice(), // copy
                ep.outClusterList.slice());
                if (status === enums_1.SLStatus.OK) {
                    logger_1.logger.debug(`Registered endpoint '${ep.endpoint}'.`, NS);
                }
                else {
                    throw new Error(`Failed to register endpoint '${ep.endpoint}' with status=${enums_1.SLStatus[status]}.`);
                }
            }
            else {
                logger_1.logger.debug(`Endpoint '${ep.endpoint}' already registered.`, NS);
            }
            for (const multicastId of ep.multicastIds) {
                const multicastEntry = {
                    multicastId,
                    endpoint: ep.endpoint,
                    networkIndex: ep.networkIndex,
                };
                const status = await this.ezsp.ezspSetMulticastTableEntry(this.multicastTable.length, multicastEntry);
                if (status !== enums_1.SLStatus.OK) {
                    throw new Error(`Failed to register group '${multicastId}' in multicast table with status=${enums_1.SLStatus[status]}.`);
                }
                logger_1.logger.debug(() => `Registered multicast table entry (${this.multicastTable.length}): ${JSON.stringify(multicastEntry)}.`, NS);
                this.multicastTable.push(multicastEntry.multicastId);
            }
        }
    }
    /**
     *
     * @returns True if the network needed to be formed.
     */
    async initTrustCenter() {
        // init TC policies
        {
            let status = await this.emberSetEzspPolicy(enums_2.EzspPolicyId.TC_KEY_REQUEST_POLICY, enums_2.EzspDecisionId.ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[INIT TC] Failed to set EzspPolicyId TC_KEY_REQUEST_POLICY to ALLOW_TC_KEY_REQUESTS_AND_SEND_CURRENT_KEY with status=${enums_1.SLStatus[status]}.`);
            }
            /* v8 ignore next */
            const appKeyRequestsPolicy = ALLOW_APP_KEY_REQUESTS ? enums_2.EzspDecisionId.ALLOW_APP_KEY_REQUESTS : enums_2.EzspDecisionId.DENY_APP_KEY_REQUESTS;
            status = await this.emberSetEzspPolicy(enums_2.EzspPolicyId.APP_KEY_REQUEST_POLICY, appKeyRequestsPolicy);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[INIT TC] Failed to set EzspPolicyId APP_KEY_REQUEST_POLICY to ${enums_2.EzspDecisionId[appKeyRequestsPolicy]} with status=${enums_1.SLStatus[status]}.`);
            }
            status = await this.emberSetJoinPolicy(enums_1.EmberJoinDecision.USE_PRECONFIGURED_KEY);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[INIT TC] Failed to set join policy to USE_PRECONFIGURED_KEY with status=${enums_1.SLStatus[status]}.`);
            }
        }
        const configNetworkKey = Buffer.from(this.networkOptions.networkKey);
        const networkInitStruct = {
            bitmask: enums_1.EmberNetworkInitBitmask.PARENT_INFO_IN_TOKEN | enums_1.EmberNetworkInitBitmask.END_DEVICE_REJOIN_ON_REBOOT,
        };
        const initStatus = await this.ezsp.ezspNetworkInit(networkInitStruct);
        logger_1.logger.debug(`[INIT TC] Network init status=${enums_1.SLStatus[initStatus]}.`, NS);
        if (initStatus !== enums_1.SLStatus.OK && initStatus !== enums_1.SLStatus.NOT_JOINED) {
            throw new Error(`[INIT TC] Failed network init request with status=${enums_1.SLStatus[initStatus]}.`);
        }
        let action = NetworkInitAction.DONE;
        if (initStatus === enums_1.SLStatus.OK) {
            // network
            await this.oneWaitress.startWaitingForEvent({ eventName: oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_UP }, DEFAULT_NETWORK_REQUEST_TIMEOUT, '[INIT TC] Network init');
            const [npStatus, nodeType, netParams] = await this.ezsp.ezspGetNetworkParameters();
            logger_1.logger.debug(() => `[INIT TC] Current adapter network: nodeType=${enums_1.EmberNodeType[nodeType]} params=${JSON.stringify(netParams)}`, NS);
            if (npStatus === enums_1.SLStatus.OK &&
                nodeType === enums_1.EmberNodeType.COORDINATOR &&
                this.networkOptions.panID === netParams.panId &&
                (0, es6_1.default)(this.networkOptions.extendedPanID, netParams.extendedPanId)) {
                // config matches adapter so far, no error, we can check the network key
                const context = (0, initters_1.initSecurityManagerContext)();
                context.coreKeyType = enums_1.SecManKeyType.NETWORK;
                context.keyIndex = 0;
                const [nkStatus, networkKey] = await this.ezsp.ezspExportKey(context);
                if (nkStatus !== enums_1.SLStatus.OK) {
                    throw new Error(`[INIT TC] Failed to export Network Key with status=${enums_1.SLStatus[nkStatus]}.`);
                }
                // config doesn't match adapter anymore
                if (!networkKey.contents.equals(configNetworkKey)) {
                    action = NetworkInitAction.LEAVE;
                }
            }
            else {
                // config doesn't match adapter
                action = NetworkInitAction.LEAVE;
            }
            if (action === NetworkInitAction.LEAVE) {
                logger_1.logger.info(`[INIT TC] Adapter network does not match config. Leaving network...`, NS);
                const leaveStatus = await this.ezsp.ezspLeaveNetwork();
                if (leaveStatus !== enums_1.SLStatus.OK) {
                    throw new Error(`[INIT TC] Failed leave network request with status=${enums_1.SLStatus[leaveStatus]}.`);
                }
                await this.oneWaitress.startWaitingForEvent({ eventName: oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_DOWN }, DEFAULT_NETWORK_REQUEST_TIMEOUT, '[INIT TC] Leave network');
                await (0, utils_1.wait)(200); // settle down
                action = NetworkInitAction.LEFT;
            }
        }
        const backup = this.getStoredBackup();
        if (initStatus === enums_1.SLStatus.NOT_JOINED || action === NetworkInitAction.LEFT) {
            // no network
            if (backup != undefined) {
                if (this.networkOptions.panID === backup.networkOptions.panId &&
                    Buffer.from(this.networkOptions.extendedPanID).equals(backup.networkOptions.extendedPanId) &&
                    this.networkOptions.channelList.includes(backup.logicalChannel) &&
                    configNetworkKey.equals(backup.networkOptions.networkKey)) {
                    // config matches backup
                    action = NetworkInitAction.FORM_BACKUP;
                }
                else {
                    // config doesn't match backup
                    logger_1.logger.info(`[INIT TC] Config does not match backup.`, NS);
                    action = NetworkInitAction.FORM_CONFIG;
                }
            }
            else {
                // no backup
                logger_1.logger.info(`[INIT TC] No valid backup found.`, NS);
                action = NetworkInitAction.FORM_CONFIG;
            }
        }
        //---- from here on, we assume everything is in place for whatever decision was taken above
        let result = 'resumed';
        switch (action) {
            case NetworkInitAction.FORM_BACKUP: {
                logger_1.logger.info(`[INIT TC] Forming from backup.`, NS);
                // `backup` valid in this `action` path (not detected by TS)
                /* v8 ignore start */
                const keyList = backup.devices.map((device) => ({
                    deviceEui64: ZSpec.Utils.eui64BEBufferToHex(device.ieeeAddress),
                    key: { contents: device.linkKey.key },
                    outgoingFrameCounter: device.linkKey.txCounter,
                    incomingFrameCounter: device.linkKey.rxCounter,
                }));
                /* v8 ignore stop */
                // before forming
                await this.importLinkKeys(keyList);
                await this.formNetwork(true /*from backup*/, backup.networkOptions.networkKey, backup.networkKeyInfo.sequenceNumber, backup.networkKeyInfo.frameCounter, backup.networkOptions.panId, Array.from(backup.networkOptions.extendedPanId), backup.logicalChannel, backup.ezsp.hashed_tclk, // valid from getStoredBackup
                backup.networkUpdateId);
                result = 'restored';
                break;
            }
            case NetworkInitAction.FORM_CONFIG: {
                logger_1.logger.info(`[INIT TC] Forming from config.`, NS);
                await this.formNetwork(false /*from config*/, configNetworkKey, 0, 0, this.networkOptions.panID, this.networkOptions.extendedPanID, this.networkOptions.channelList[0], (0, node_crypto_1.randomBytes)(consts_2.EMBER_ENCRYPTION_KEY_SIZE), // rnd TC link key
                0);
                result = 'reset';
                break;
            }
            case NetworkInitAction.DONE: {
                logger_1.logger.info(`[INIT TC] Adapter network matches config.`, NS);
                break;
            }
        }
        // can't let frame counter wrap to zero (uint32_t), will force a broadcast after init if getting too close
        if (backup != null && backup.networkKeyInfo.frameCounter > 0xfeeeeeee) {
            // XXX: while this remains a pretty low occurrence in most (small) networks,
            //      currently Z2M won't support the key update because of one-way config...
            //      need to investigate handling this properly
            // logger.warning(`[INIT TC] Network key frame counter is reaching its limit. Scheduling broadcast to update network key. `
            //     + `This may result in some devices (especially battery-powered) temporarily losing connection.`, NS);
            // // XXX: no idea here on the proper timer value, but this will block the network for several seconds on exec
            // //      (probably have to take the behavior of sleepy-end devices into account to improve chances of reaching everyone right away?)
            // setTimeout(async () => {
            //     this.requestQueue.enqueue(async (): Promise<SLStatus> => {
            //         await this.broadcastNetworkKeyUpdate();
            //         return SLStatus.OK;
            //     }, logger.error, true);// no reject just log error if any, will retry next start, & prioritize so we know it'll run when expected
            // }, 300000);
            logger_1.logger.warning(`[INIT TC] Network key frame counter is reaching its limit. A new network key will have to be instaured soon.`, NS);
        }
        return result;
    }
    /**
     * Form a network using given parameters.
     */
    async formNetwork(fromBackup, networkKey, networkKeySequenceNumber, networkKeyFrameCounter, panId, extendedPanId, radioChannel, tcLinkKey, nwkUpdateId) {
        const state = {
            bitmask: enums_1.EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY |
                enums_1.EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY |
                enums_1.EmberInitialSecurityBitmask.HAVE_NETWORK_KEY |
                enums_1.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY |
                enums_1.EmberInitialSecurityBitmask.REQUIRE_ENCRYPTED_KEY,
            preconfiguredKey: { contents: tcLinkKey },
            networkKey: { contents: networkKey },
            networkKeySequenceNumber: networkKeySequenceNumber,
            preconfiguredTrustCenterEui64: ZSpec.BLANK_EUI64,
        };
        if (fromBackup) {
            state.bitmask |= enums_1.EmberInitialSecurityBitmask.NO_FRAME_COUNTER_RESET;
            const status = await this.ezsp.ezspSetNWKFrameCounter(networkKeyFrameCounter);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[INIT FORM] Failed to set NWK frame counter with status=${enums_1.SLStatus[status]}.`);
            }
            // status = await this.ezsp.ezspSetAPSFrameCounter(tcLinkKeyFrameCounter);
            // if (status !== SLStatus.OK) {
            //     throw new Error(`[INIT FORM] Failed to set TC APS frame counter with status=${SLStatus[status]}.`);
            // }
        }
        let status = await this.ezsp.ezspSetInitialSecurityState(state);
        if (status !== enums_1.SLStatus.OK) {
            throw new Error(`[INIT FORM] Failed to set initial security state with status=${enums_1.SLStatus[status]}.`);
        }
        const extended = enums_1.EmberExtendedSecurityBitmask.JOINER_GLOBAL_LINK_KEY | enums_1.EmberExtendedSecurityBitmask.NWK_LEAVE_REQUEST_NOT_ALLOWED;
        status = await this.ezsp.ezspSetExtendedSecurityBitmask(extended);
        if (status !== enums_1.SLStatus.OK) {
            throw new Error(`[INIT FORM] Failed to set extended security bitmask to ${extended} with status=${enums_1.SLStatus[status]}.`);
        }
        if (!fromBackup) {
            status = await this.ezsp.ezspClearKeyTable();
            if (status !== enums_1.SLStatus.OK) {
                logger_1.logger.error(`[INIT FORM] Failed to clear key table with status=${enums_1.SLStatus[status]}.`, NS);
            }
        }
        const netParams = {
            panId,
            extendedPanId,
            radioTxPower: this.adapterOptions.transmitPower || 5,
            radioChannel,
            joinMethod: enums_1.EmberJoinMethod.MAC_ASSOCIATION,
            nwkManagerId: ZSpec.COORDINATOR_ADDRESS,
            nwkUpdateId,
            channels: ZSpec.ALL_802_15_4_CHANNELS_MASK,
        };
        logger_1.logger.info(() => `[INIT FORM] Forming new network with: ${JSON.stringify(netParams)}`, NS);
        status = await this.ezsp.ezspFormNetwork(netParams);
        if (status !== enums_1.SLStatus.OK) {
            throw new Error(`[INIT FORM] Failed form network request with status=${enums_1.SLStatus[status]}.`);
        }
        await this.oneWaitress.startWaitingForEvent({ eventName: oneWaitress_1.OneWaitressEvents.STACK_STATUS_NETWORK_UP }, DEFAULT_NETWORK_REQUEST_TIMEOUT, '[INIT FORM] Form network');
        status = await this.ezsp.ezspStartWritingStackTokens();
        logger_1.logger.debug(`[INIT FORM] Start writing stack tokens status=${enums_1.SLStatus[status]}.`, NS);
        logger_1.logger.info(`[INIT FORM] New network formed!`, NS);
    }
    /**
     * Loads currently stored backup and returns it in internal backup model.
     */
    getStoredBackup() {
        if (!(0, node_fs_1.existsSync)(this.backupPath)) {
            return undefined;
        }
        let data;
        try {
            data = JSON.parse((0, node_fs_1.readFileSync)(this.backupPath).toString());
        }
        catch (error) {
            throw new Error(`[BACKUP] Coordinator backup is corrupted. (${error.stack})`);
        }
        if (data.metadata?.format === 'zigpy/open-coordinator-backup' && data.metadata?.version) {
            if (data.metadata?.version !== 1) {
                throw new Error(`[BACKUP] Unsupported open coordinator backup version (version=${data.metadata?.version}).`);
            }
            if (!data.stack_specific?.ezsp || !data.metadata.internal.ezspVersion) {
                throw new Error(`[BACKUP] Current backup file is not for EmberZNet stack.`);
            }
            if (data.metadata.internal.ezspVersion < BACKUP_OLDEST_SUPPORTED_EZSP_VERSION) {
                (0, node_fs_1.renameSync)(this.backupPath, `${this.backupPath}.old`);
                logger_1.logger.warning(`[BACKUP] Current backup file is from an unsupported EZSP version. Renaming and ignoring.`, NS);
                return undefined;
            }
            return utils_1.BackupUtils.fromUnifiedBackup(data);
        }
        else {
            throw new Error(`[BACKUP] Unknown backup format.`);
        }
    }
    /**
     * Export link keys for backup.
     *
     * @return List of keys data with AES hashed keys
     */
    async exportLinkKeys() {
        const [confStatus, keyTableSize] = await this.ezsp.ezspGetConfigurationValue(enums_2.EzspConfigId.KEY_TABLE_SIZE);
        if (confStatus !== enums_1.SLStatus.OK) {
            throw new Error(`[BACKUP] Failed to retrieve key table size from NCP with status=${enums_1.SLStatus[confStatus]}.`);
        }
        let context;
        let plaintextKey;
        let apsKeyMeta;
        let status;
        const keyList = [];
        for (let i = 0; i < keyTableSize; i++) {
            [status, context, plaintextKey, apsKeyMeta] = await this.ezsp.ezspExportLinkKeyByIndex(i);
            logger_1.logger.debug(`[BACKUP] Export link key at index ${i}, status=${enums_1.SLStatus[status]}.`, NS);
            // only include key if we could retrieve one at index and hash it properly
            if (status === enums_1.SLStatus.OK) {
                // Rather than give the real link key, the backup contains a hashed version of the key.
                // This is done to prevent a compromise of the backup data from compromising the current link keys.
                // This is per the Smart Energy spec.
                const hashedKey = ZSpec.Utils.aes128MmoHash(plaintextKey.contents);
                keyList.push({
                    deviceEui64: context.eui64,
                    key: { contents: hashedKey },
                    outgoingFrameCounter: apsKeyMeta.outgoingFrameCounter,
                    incomingFrameCounter: apsKeyMeta.incomingFrameCounter,
                });
            }
        }
        logger_1.logger.info(`[BACKUP] Retrieved ${keyList.length} link keys.`, NS);
        return keyList;
    }
    /**
     * Import link keys from backup.
     *
     * @param backupData
     */
    async importLinkKeys(backupData) {
        if (!backupData?.length) {
            return;
        }
        const [confStatus, keyTableSize] = await this.ezsp.ezspGetConfigurationValue(enums_2.EzspConfigId.KEY_TABLE_SIZE);
        if (confStatus !== enums_1.SLStatus.OK) {
            throw new Error(`[BACKUP] Failed to retrieve key table size from NCP with status=${enums_1.SLStatus[confStatus]}.`);
        }
        if (backupData.length > keyTableSize) {
            throw new Error(`[BACKUP] Current key table of ${keyTableSize} is too small to import backup of ${backupData.length}!`);
        }
        const networkStatus = await this.ezsp.ezspNetworkState();
        if (networkStatus !== enums_1.EmberNetworkStatus.NO_NETWORK) {
            throw new Error(`[BACKUP] Cannot import TC data while network is up, networkStatus=${enums_1.EmberNetworkStatus[networkStatus]}.`);
        }
        let status;
        for (let i = 0; i < keyTableSize; i++) {
            // erase any key index not present in backup but available on the NCP
            status =
                i >= backupData.length
                    ? await this.ezsp.ezspEraseKeyTableEntry(i)
                    : await this.ezsp.ezspImportLinkKey(i, backupData[i].deviceEui64, backupData[i].key);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[BACKUP] Failed to ${i >= backupData.length ? 'erase' : 'set'} key table entry at index ${i} with status=${enums_1.SLStatus[status]}.`);
            }
        }
        logger_1.logger.info(`[BACKUP] Imported ${backupData.length} keys.`, NS);
    }
    /**
     * Routine to update the network key and broadcast the update to the network after a set time.
     * NOTE: This should run at a large interval, but before the uint32_t of the frame counter is able to reach all Fs (can't wrap to 0).
     *       This may disrupt sleepy end devices that miss the update, but they should be able to TC rejoin (in most cases...).
     *       On the other hand, the more often this runs, the more secure the network is...
     */
    async broadcastNetworkKeyUpdate() {
        return await this.queue.execute(async () => {
            logger_1.logger.warning(`[TRUST CENTER] Performing a network key update. This might take a while and disrupt normal operation.`, NS);
            // zero-filled = let stack generate new random network key
            let status = await this.ezsp.ezspBroadcastNextNetworkKey({ contents: Buffer.alloc(consts_2.EMBER_ENCRYPTION_KEY_SIZE) });
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[TRUST CENTER] Failed to broadcast next network key with status=${enums_1.SLStatus[status]}.`);
            }
            // XXX: this will block other requests for a while, but should ensure the key propagates without interference?
            //      could also stop dispatching entirely and do this outside the queue if necessary/better
            await (0, utils_1.wait)(BROADCAST_NETWORK_KEY_SWITCH_WAIT_TIME);
            status = await this.ezsp.ezspBroadcastNetworkKeySwitch();
            if (status !== enums_1.SLStatus.OK) {
                // XXX: Not sure how likely this is, but this is bad, probably should hard fail?
                throw new Error(`[TRUST CENTER] Failed to broadcast network key switch with status=${enums_1.SLStatus[status]}.`);
            }
        });
    }
    /**
     * Received when EZSP layer alerts of a problem that needs the NCP to be reset.
     * @param status
     */
    async onNcpNeedsResetAndInit(status) {
        logger_1.logger.error(`Adapter fatal error: ${enums_1.EzspStatus[status]}`, NS);
        this.emit('disconnected');
    }
    //---- START Events
    //---- END Events
    //---- START Cache-enabled EZSP wrappers
    /**
     * Clear the cached network values (set to invalid values).
     */
    clearNetworkCache() {
        this.networkCache = (0, initters_1.initNetworkCache)();
    }
    /**
     * Return the EUI 64 of the local node
     * This call caches the results on the host to prevent frequent EZSP transactions.
     * Check against BLANK_EUI64 for validity.
     */
    async emberGetEui64() {
        if (this.networkCache.eui64 === ZSpec.BLANK_EUI64) {
            this.networkCache.eui64 = await this.ezsp.ezspGetEui64();
        }
        return this.networkCache.eui64;
    }
    /**
     * Return the PAN ID of the local node.
     * This call caches the results on the host to prevent frequent EZSP transactions.
     * Check against INVALID_PAN_ID for validity.
     */
    async emberGetPanId() {
        if (this.networkCache.parameters.panId === ZSpec.INVALID_PAN_ID) {
            const [status, , parameters] = await this.ezsp.ezspGetNetworkParameters();
            if (status === enums_1.SLStatus.OK) {
                this.networkCache.parameters = parameters;
            }
            else {
                throw new Error(`Failed to get PAN ID (via network parameters) with status=${enums_1.SLStatus[status]}.`);
            }
        }
        return this.networkCache.parameters.panId;
    }
    /**
     * Return the Extended PAN ID of the local node.
     * This call caches the results on the host to prevent frequent EZSP transactions.
     * Check against BLANK_EXTENDED_PAN_ID for validity.
     */
    async emberGetExtendedPanId() {
        if ((0, es6_1.default)(this.networkCache.parameters.extendedPanId, ZSpec.BLANK_EXTENDED_PAN_ID)) {
            const [status, , parameters] = await this.ezsp.ezspGetNetworkParameters();
            if (status === enums_1.SLStatus.OK) {
                this.networkCache.parameters = parameters;
            }
            else {
                throw new Error(`Failed to get Extended PAN ID (via network parameters) with status=${enums_1.SLStatus[status]}.`);
            }
        }
        return this.networkCache.parameters.extendedPanId;
    }
    /**
     * Return the radio channel (uint8_t) of the current network.
     * This call caches the results on the host to prevent frequent EZSP transactions.
     * Check against INVALID_RADIO_CHANNEL for validity.
     */
    async emberGetRadioChannel() {
        if (this.networkCache.parameters.radioChannel === consts_1.INVALID_RADIO_CHANNEL) {
            const [status, , parameters] = await this.ezsp.ezspGetNetworkParameters();
            if (status === enums_1.SLStatus.OK) {
                this.networkCache.parameters = parameters;
            }
            else {
                throw new Error(`Failed to get radio channel (via network parameters) with status=${enums_1.SLStatus[status]}.`);
            }
        }
        return this.networkCache.parameters.radioChannel;
    }
    //---- END Cache-enabled EZSP wrappers
    //---- START EZSP wrappers
    /**
     * Ensure the Host & NCP are aligned on protocols using version.
     * Cache the retrieved information.
     *
     * NOTE: currently throws on mismatch until support for lower versions is implemented (not planned atm)
     *
     * Does nothing if ncpNeedsResetAndInit == true.
     */
    async emberVersion() {
        // send the Host version number to the NCP.
        // The NCP returns the EZSP version that the NCP is running along with the stackType and stackVersion
        let [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await this.ezsp.ezspVersion(consts_2.EZSP_PROTOCOL_VERSION);
        // verify that the stack type is what is expected
        if (ncpStackType !== consts_2.EZSP_STACK_TYPE_MESH) {
            throw new Error(`Stack type ${ncpStackType} is not expected!`);
        }
        if (ncpEzspProtocolVer === consts_2.EZSP_PROTOCOL_VERSION) {
            logger_1.logger.debug(`Adapter EZSP protocol version (${ncpEzspProtocolVer}) matches Host.`, NS);
        }
        else if (ncpEzspProtocolVer < consts_2.EZSP_PROTOCOL_VERSION && ncpEzspProtocolVer >= consts_2.EZSP_MIN_PROTOCOL_VERSION) {
            [ncpEzspProtocolVer, ncpStackType, ncpStackVer] = await this.ezsp.ezspVersion(ncpEzspProtocolVer);
            logger_1.logger.info(`Adapter EZSP protocol version (${ncpEzspProtocolVer}) lower than Host. Switched.`, NS);
        }
        else {
            throw new Error(`Adapter EZSP protocol version (${ncpEzspProtocolVer}) is not supported by Host [${consts_2.EZSP_MIN_PROTOCOL_VERSION}-${consts_2.EZSP_PROTOCOL_VERSION}].`);
        }
        this.ezsp.setProtocolVersion(ncpEzspProtocolVer);
        logger_1.logger.debug(`Adapter info: EZSPVersion=${ncpEzspProtocolVer} StackType=${ncpStackType} StackVersion=${ncpStackVer}`, NS);
        const [status, versionStruct] = await this.ezsp.ezspGetVersionStruct();
        if (status !== enums_1.SLStatus.OK) {
            // Should never happen with support of only EZSP v13+
            throw new Error(`NCP has old-style version number. Not supported.`);
        }
        this.version = {
            ezsp: ncpEzspProtocolVer,
            revision: `${versionStruct.major}.${versionStruct.minor}.${versionStruct.patch} [${enums_1.EmberVersionType[versionStruct.type]}]`,
            ...versionStruct,
        };
        if (versionStruct.type !== enums_1.EmberVersionType.GA) {
            logger_1.logger.warning(`Adapter is running a non-GA version (${enums_1.EmberVersionType[versionStruct.type]}).`, NS);
        }
        logger_1.logger.info(() => `Adapter version info: ${JSON.stringify(this.version)}`, NS);
    }
    /**
     * This function sets an EZSP config value.
     * WARNING: Do not call for values that cannot be set after init without first resetting NCP (like table sizes).
     *          To avoid an extra NCP call, this does not check for it.
     * @param configId
     * @param value uint16_t
     * @returns
     */
    async emberSetEzspConfigValue(configId, value) {
        const status = await this.ezsp.ezspSetConfigurationValue(configId, value);
        logger_1.logger.debug(`[EzspConfigId] SET '${enums_2.EzspConfigId[configId]}' TO '${value}' with status=${enums_1.SLStatus[status]}.`, NS);
        if (status !== enums_1.SLStatus.OK) {
            logger_1.logger.info(`[EzspConfigId] Failed to SET '${enums_2.EzspConfigId[configId]}' TO '${value}' with status=${enums_1.SLStatus[status]}. Firmware value will be used instead.`, NS);
        }
        return status;
    }
    /**
     * This function sets an EZSP value.
     * @param valueId
     * @param valueLength uint8_t
     * @param value uint8_t *
     * @returns
     */
    async emberSetEzspValue(valueId, valueLength, value) {
        const status = await this.ezsp.ezspSetValue(valueId, valueLength, value);
        logger_1.logger.debug(`[EzspValueId] SET '${enums_2.EzspValueId[valueId]}' TO '${value}' with status=${enums_1.SLStatus[status]}.`, NS);
        return status;
    }
    /**
     * This function sets an EZSP policy.
     * @param policyId
     * @param decisionId Can be bitop
     * @returns
     */
    async emberSetEzspPolicy(policyId, decisionId) {
        const status = await this.ezsp.ezspSetPolicy(policyId, decisionId);
        logger_1.logger.debug(`[EzspPolicyId] SET '${enums_2.EzspPolicyId[policyId]}' TO '${decisionId}' with status=${enums_1.SLStatus[status]}.`, NS);
        return status;
    }
    /**
     * Set the trust center policy bitmask using decision.
     * @param decision
     * @returns
     */
    async emberSetJoinPolicy(decision) {
        let policy = enums_2.EzspDecisionBitmask.DEFAULT_CONFIGURATION;
        switch (decision) {
            case enums_1.EmberJoinDecision.USE_PRECONFIGURED_KEY: {
                policy = enums_2.EzspDecisionBitmask.ALLOW_JOINS | enums_2.EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS;
                break;
            }
            case enums_1.EmberJoinDecision.ALLOW_REJOINS_ONLY: {
                policy = enums_2.EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS;
                break;
            }
            /*case EmberJoinDecision.SEND_KEY_IN_THE_CLEAR: {
                policy = EzspDecisionBitmask.ALLOW_JOINS | EzspDecisionBitmask.ALLOW_UNSECURED_REJOINS | EzspDecisionBitmask.SEND_KEY_IN_CLEAR;
                break;
            }*/
        }
        return await this.emberSetEzspPolicy(enums_2.EzspPolicyId.TRUST_CENTER_POLICY, policy);
    }
    //---- END EZSP wrappers
    //---- START Ember ZDO
    /**
     * ZDO
     * Get the next device request sequence number.
     *
     * Requests have sequence numbers so that they can be matched up with the
     * responses. To avoid complexities, the library uses numbers with the high
     * bit clear and the stack uses numbers with the high bit set.
     *
     * @return uint8_t The next device request sequence number
     */
    nextZDORequestSequence() {
        return (this.zdoRequestSequence = ++this.zdoRequestSequence & APPLICATION_ZDO_SEQUENCE_MASK);
    }
    //---- END Ember ZDO
    //-- START Adapter implementation
    async start() {
        logger_1.logger.info(`======== Ember Adapter Starting ========`, NS);
        const result = await this.initEzsp();
        return result;
    }
    async stop() {
        clearInterval(this.watchdogCountersHandle);
        await this.ezsp.stop();
        this.ezsp.removeAllListeners();
        logger_1.logger.info(`======== Ember Adapter Stopped ========`, NS);
    }
    async getCoordinatorIEEE() {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            // in all likelihood this will be retrieved from cache
            return await this.emberGetEui64();
        });
    }
    async getCoordinatorVersion() {
        return { type: `EmberZNet`, meta: this.version };
    }
    // queued
    async reset(type) {
        throw new Error(`Not supported '${type}'.`);
        // NOTE: although this function is legacy atm, a couple of new untested EZSP functions that could also prove useful:
        // this.ezsp.ezspTokenFactoryReset(true/*excludeOutgoingFC*/, true/*excludeBootCounter*/);
        // this.ezsp.ezspResetNode()
    }
    async supportsBackup() {
        return true;
    }
    // queued
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async backup(ieeeAddressesInDatabase) {
        return await this.queue.execute(async () => {
            // grab fresh version here, bypass cache
            const [netStatus, , netParams] = await this.ezsp.ezspGetNetworkParameters();
            if (netStatus !== enums_1.SLStatus.OK) {
                throw new Error(`[BACKUP] Failed to get network parameters with status=${enums_1.SLStatus[netStatus]}.`);
            }
            // update cache
            this.networkCache.parameters = netParams;
            this.networkCache.eui64 = await this.ezsp.ezspGetEui64();
            const [netKeyStatus, netKeyInfo] = await this.ezsp.ezspGetNetworkKeyInfo();
            if (netKeyStatus !== enums_1.SLStatus.OK) {
                throw new Error(`[BACKUP] Failed to get network keys info with status=${enums_1.SLStatus[netKeyStatus]}.`);
            }
            if (!netKeyInfo.networkKeySet) {
                throw new Error(`[BACKUP] No network key set.`);
            }
            /* v8 ignore next */
            const keyList = ALLOW_APP_KEY_REQUESTS ? await this.exportLinkKeys() : [];
            let context = (0, initters_1.initSecurityManagerContext)();
            context.coreKeyType = enums_1.SecManKeyType.TC_LINK;
            const [tclkStatus, tcLinkKey] = await this.ezsp.ezspExportKey(context);
            if (tclkStatus !== enums_1.SLStatus.OK) {
                throw new Error(`[BACKUP] Failed to export TC Link Key with status=${enums_1.SLStatus[tclkStatus]}.`);
            }
            // const [tcKeyStatus, tcKeyInfo] = await this.ezsp.ezspGetApsKeyInfo(context);
            // if (tcKeyStatus !== SLStatus.OK) {
            //     throw new Error(`[BACKUP] Failed to get TC APS key info with status=${SLStatus[tcKeyStatus]}.`);
            // }
            context = (0, initters_1.initSecurityManagerContext)(); // make sure it's back to zeroes
            context.coreKeyType = enums_1.SecManKeyType.NETWORK;
            context.keyIndex = 0;
            const [nkStatus, networkKey] = await this.ezsp.ezspExportKey(context);
            if (nkStatus !== enums_1.SLStatus.OK) {
                throw new Error(`[BACKUP] Failed to export Network Key with status=${enums_1.SLStatus[nkStatus]}.`);
            }
            return {
                networkOptions: {
                    panId: netParams.panId, // uint16_t
                    extendedPanId: Buffer.from(netParams.extendedPanId),
                    channelList: ZSpec.Utils.uint32MaskToChannels(netParams.channels),
                    networkKey: networkKey.contents,
                    networkKeyDistribute: false,
                },
                logicalChannel: netParams.radioChannel,
                networkKeyInfo: {
                    sequenceNumber: netKeyInfo.networkKeySequenceNumber,
                    frameCounter: netKeyInfo.networkKeyFrameCounter,
                },
                // tcLinkKeyInfo: {
                //     incomingFrameCounter: tcKeyInfo.bitmask & EmberKeyStructBitmask.HAS_INCOMING_FRAME_COUNTER ? tcKeyInfo.incomingFrameCounter : 0,
                //     outgoingFrameCounter: tcKeyInfo.bitmask & EmberKeyStructBitmask.HAS_OUTGOING_FRAME_COUNTER ? tcKeyInfo.outgoingFrameCounter : 0,
                // },
                securityLevel: consts_1.SECURITY_LEVEL_Z3,
                networkUpdateId: netParams.nwkUpdateId,
                coordinatorIeeeAddress: Buffer.from(this.networkCache.eui64.substring(2) /*take out 0x*/, 'hex').reverse(),
                devices: keyList.map(
                /* v8 ignore start */
                (key) => ({
                    networkAddress: null, // not used for restore, no reason to make NCP calls for nothing
                    ieeeAddress: Buffer.from(key.deviceEui64.substring(2) /*take out 0x*/, 'hex').reverse(),
                    isDirectChild: false, // not used
                    linkKey: {
                        key: key.key.contents,
                        rxCounter: key.incomingFrameCounter,
                        txCounter: key.outgoingFrameCounter,
                    },
                })),
                ezsp: {
                    version: this.version.ezsp,
                    hashed_tclk: tcLinkKey.contents,
                    // tokens: tokensBuf.toString('hex'),
                    // altNetworkKey: altNetworkKey.contents,
                },
            };
        });
    }
    // queued, non-InterPAN
    async getNetworkParameters() {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            // first call will cache for the others, but in all likelihood, it will all be from freshly cached after init
            // since Controller caches this also.
            const channel = await this.emberGetRadioChannel();
            const panID = await this.emberGetPanId();
            const extendedPanID = await this.emberGetExtendedPanId();
            return {
                panID,
                extendedPanID: ZSpec.Utils.eui64LEBufferToHex(Buffer.from(extendedPanID)),
                channel,
                nwkUpdateID: this.networkCache.parameters.nwkUpdateId,
            };
        });
    }
    // queued
    async addInstallCode(ieeeAddress, key, hashed) {
        return await this.queue.execute(async () => {
            // Add the key to the transient key table.
            // This will be used while the DUT joins.
            const impStatus = await this.ezsp.ezspImportTransientKey(ieeeAddress, { contents: hashed ? key : ZSpec.Utils.aes128MmoHash(key) });
            if (impStatus == enums_1.SLStatus.OK) {
                logger_1.logger.debug(`[ADD INSTALL CODE] Success for '${ieeeAddress}'.`, NS);
            }
            else {
                throw new Error(`[ADD INSTALL CODE] Failed for '${ieeeAddress}' with status=${enums_1.SLStatus[impStatus]}.`);
            }
        });
    }
    /** WARNING: Adapter impl. Starts timer immediately upon returning */
    waitFor(networkAddress, endpoint, frameType, direction, transactionSequenceNumber, clusterID, commandIdentifier, timeout) {
        const sourceEndpointInfo = endpoints_1.FIXED_ENDPOINTS[0];
        const waiter = this.oneWaitress.waitFor({
            target: networkAddress,
            apsFrame: {
                clusterId: clusterID,
                profileId: sourceEndpointInfo.profileId, // XXX: only used by OTA upstream
                sequence: 0, // set by stack
                sourceEndpoint: sourceEndpointInfo.endpoint,
                destinationEndpoint: endpoint,
                groupId: 0,
                options: enums_1.EmberApsOption.NONE,
            },
            zclSequence: transactionSequenceNumber,
            commandIdentifier,
        }, timeout);
        return {
            cancel: () => this.oneWaitress.remove(waiter.id),
            promise: waiter.start().promise,
        };
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            const clusterName = Zdo.ClusterId[clusterId];
            const messageTag = this.nextZDORequestSequence();
            payload[0] = messageTag;
            const apsFrame = {
                profileId: Zdo.ZDO_PROFILE_ID,
                clusterId,
                sourceEndpoint: Zdo.ZDO_ENDPOINT,
                destinationEndpoint: Zdo.ZDO_ENDPOINT,
                options: exports.DEFAULT_APS_OPTIONS,
                groupId: 0,
                sequence: 0, // set by stack
            };
            let status;
            let apsSequence;
            if (ZSpec.Utils.isBroadcastAddress(networkAddress)) {
                logger_1.logger.debug(() => `~~~> [ZDO ${clusterName} BROADCAST to=${networkAddress} messageTag=${messageTag} payload=${payload.toString('hex')}]`, NS);
                [status, apsSequence] = await this.ezsp.ezspSendBroadcast(ZSpec.NULL_NODE_ID, // alias
                networkAddress, 0, // nwkSequence
                apsFrame, ZDO_REQUEST_RADIUS, messageTag, payload);
                apsFrame.sequence = apsSequence;
                logger_1.logger.debug(`~~~> [SENT ZDO BROADCAST messageTag=${messageTag} apsSequence=${apsSequence} status=${enums_1.SLStatus[status]}]`, NS);
                if (status !== enums_1.SLStatus.OK) {
                    throw new Error(`~x~> [ZDO ${clusterName} BROADCAST to=${networkAddress} messageTag=${messageTag}] Failed to send request with status=${enums_1.SLStatus[status]}.`);
                }
            }
            else {
                logger_1.logger.debug(() => `~~~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} messageTag=${messageTag} payload=${payload.toString('hex')}]`, NS);
                [status, apsSequence] = await this.ezsp.ezspSendUnicast(enums_1.EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, messageTag, payload);
                apsFrame.sequence = apsSequence;
                logger_1.logger.debug(`~~~> [SENT ZDO UNICAST messageTag=${messageTag} apsSequence=${apsSequence} status=${enums_1.SLStatus[status]}]`, NS);
                if (status !== enums_1.SLStatus.OK) {
                    throw new Error(`~x~> [ZDO ${clusterName} UNICAST to=${ieeeAddress}:${networkAddress} messageTag=${messageTag}] Failed to send request with status=${enums_1.SLStatus[status]}.`);
                }
            }
            if (!disableResponse) {
                const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
                if (responseClusterId) {
                    return await this.oneWaitress.startWaitingFor({
                        target: responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE ? ieeeAddress : networkAddress,
                        apsFrame,
                        zdoResponseClusterId: responseClusterId,
                    }, DEFAULT_REQUEST_TIMEOUT);
                }
            }
        }, networkAddress);
    }
    // queued, non-InterPAN
    async permitJoin(seconds, networkAddress) {
        const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
        const preJoining = async () => {
            if (seconds) {
                const plaintextKey = { contents: Buffer.from(consts_1.ZIGBEE_PROFILE_INTEROPERABILITY_LINK_KEY) };
                const impKeyStatus = await this.ezsp.ezspImportTransientKey(ZSpec.BLANK_EUI64, plaintextKey);
                if (impKeyStatus !== enums_1.SLStatus.OK) {
                    throw new Error(`[ZDO] Failed import transient key with status=${enums_1.SLStatus[impKeyStatus]}.`);
                }
                const setJPstatus = await this.emberSetJoinPolicy(enums_1.EmberJoinDecision.USE_PRECONFIGURED_KEY);
                if (setJPstatus !== enums_1.SLStatus.OK) {
                    throw new Error(`[ZDO] Failed set join policy with status=${enums_1.SLStatus[setJPstatus]}.`);
                }
            }
            else {
                await this.ezsp.ezspClearTransientLinkKeys();
                const setJPstatus = await this.emberSetJoinPolicy(enums_1.EmberJoinDecision.ALLOW_REJOINS_ONLY);
                if (setJPstatus !== enums_1.SLStatus.OK) {
                    throw new Error(`[ZDO] Failed set join policy with status=${enums_1.SLStatus[setJPstatus]}.`);
                }
            }
        };
        if (networkAddress) {
            // specific device that is not `Coordinator`
            await this.queue.execute(async () => {
                this.checkInterpanLock();
                await preJoining();
            });
            // `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)) {
                // TODO: will disappear once moved upstream
                throw new Zdo.StatusError(result[0]);
            }
            /* v8 ignore stop */
        }
        else {
            // coordinator-only (0), or all
            await this.queue.execute(async () => {
                this.checkInterpanLock();
                await preJoining();
            });
            const status = await this.ezsp.ezspPermitJoining(seconds);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`[ZDO] Failed coordinator permit joining request with status=${enums_1.SLStatus[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);
            }
        }
    }
    //---- ZCL
    // queued, non-InterPAN
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint) {
        const sourceEndpointInfo = (sourceEndpoint && endpoints_1.FIXED_ENDPOINTS.find((epi) => epi.endpoint === sourceEndpoint)) || endpoints_1.FIXED_ENDPOINTS[0];
        const command = zclFrame.command;
        let commandResponseId;
        if (command.response !== undefined && disableResponse === false) {
            commandResponseId = command.response;
        }
        else if (!zclFrame.header.frameControl.disableDefaultResponse) {
            commandResponseId = Zcl.Foundation.defaultRsp.ID;
        }
        const apsFrame = {
            profileId: sourceEndpointInfo.profileId,
            clusterId: zclFrame.cluster.ID,
            sourceEndpoint: sourceEndpoint || endpoints_1.FIXED_ENDPOINTS[0].endpoint,
            destinationEndpoint: endpoint,
            options: exports.DEFAULT_APS_OPTIONS,
            groupId: 0,
            sequence: 0, // set by stack
        };
        // don't RETRY if no response expected
        if (commandResponseId === undefined) {
            apsFrame.options &= ~enums_1.EmberApsOption.RETRY;
        }
        const data = zclFrame.toBuffer();
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_1.logger.debug(() => `~~~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, NS);
            for (let i = 1; i <= QUEUE_MAX_SEND_ATTEMPTS; i++) {
                let status = enums_1.SLStatus.FAIL;
                try {
                    [status] = await this.ezsp.send(enums_1.EmberOutgoingMessageType.DIRECT, networkAddress, apsFrame, data, 0, // alias
                    0);
                }
                catch (error) {
                    if (error instanceof ezspError_1.EzspError) {
                        switch (error.code) {
                            case enums_1.EzspStatus.NO_TX_SPACE: {
                                status = enums_1.SLStatus.BUSY;
                                break;
                            }
                            case enums_1.EzspStatus.NOT_CONNECTED: {
                                status = enums_1.SLStatus.NETWORK_DOWN;
                                break;
                            }
                        }
                    }
                }
                // `else if` order matters
                if (status === enums_1.SLStatus.OK) {
                    break;
                }
                else if (disableRecovery || i == QUEUE_MAX_SEND_ATTEMPTS) {
                    throw new Error(`~x~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)}] Failed to send request with status=${enums_1.SLStatus[status]}.`);
                }
                else if (status === enums_1.SLStatus.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED || status === enums_1.SLStatus.BUSY) {
                    await (0, utils_1.wait)(QUEUE_BUSY_DEFER_MSEC);
                }
                else if (status === enums_1.SLStatus.NETWORK_DOWN) {
                    await (0, utils_1.wait)(QUEUE_NETWORK_DOWN_DEFER_MSEC);
                }
                else {
                    throw new Error(`~x~> [ZCL to=${ieeeAddr}:${networkAddress} apsFrame=${JSON.stringify(apsFrame)}] Failed to send request with status=${enums_1.SLStatus[status]}.`);
                }
                logger_1.logger.debug(`~x~> [ZCL to=${ieeeAddr}:${networkAddress}] Failed to send request attempt ${i}/${QUEUE_MAX_SEND_ATTEMPTS} with status=${enums_1.SLStatus[status]}.`, NS);
            }
            if (commandResponseId !== undefined) {
                // NOTE: aps sequence number will have been set by send function
                const result = await this.oneWaitress.startWaitingFor({
                    target: networkAddress,
                    apsFrame,
                    zclSequence: zclFrame.header.transactionSequenceNumber,
                    commandIdentifier: commandResponseId,
                }, timeout);
                return result;
            }
        }, networkAddress);
    }
    // queued, non-InterPAN
    async sendZclFrameToGroup(groupID, zclFrame, sourceEndpoint) {
        const sourceEndpointInfo = (sourceEndpoint && endpoints_1.FIXED_ENDPOINTS.find((epi) => epi.endpoint === sourceEndpoint)) || endpoints_1.FIXED_ENDPOINTS[0];
        const apsFrame = {
            profileId: sourceEndpointInfo.profileId,
            clusterId: zclFrame.cluster.ID,
            sourceEndpoint: sourceEndpoint || endpoints_1.FIXED_ENDPOINTS[0].endpoint,
            destinationEndpoint: 0xff,
            options: exports.DEFAULT_APS_OPTIONS,
            groupId: groupID,
            sequence: 0, // set by stack
        };
        const data = zclFrame.toBuffer();
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_1.logger.debug(() => `~~~> [ZCL GROUP apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, NS);
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const [status, messageTag] = await this.ezsp.send(enums_1.EmberOutgoingMessageType.MULTICAST, groupID, // not used with MULTICAST
            apsFrame, data, 0, // alias
            0);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`~x~> [ZCL GROUP groupId=${groupID}] Failed to send with status=${enums_1.SLStatus[status]}.`);
            }
            // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed
            await (0, utils_1.wait)(QUEUE_BUSY_DEFER_MSEC);
        });
    }
    // queued, non-InterPAN
    async sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, destination) {
        const sourceEndpointInfo = endpoints_1.FIXED_ENDPOINTS.find((epi) => epi.endpoint === sourceEndpoint) ?? endpoints_1.FIXED_ENDPOINTS[0];
        const apsFrame = {
            profileId: sourceEndpointInfo.profileId,
            clusterId: zclFrame.cluster.ID,
            sourceEndpoint,
            destinationEndpoint: endpoint,
            options: exports.DEFAULT_APS_OPTIONS,
            groupId: destination,
            sequence: 0, // set by stack
        };
        const data = zclFrame.toBuffer();
        return await this.queue.execute(async () => {
            this.checkInterpanLock();
            logger_1.logger.debug(() => `~~~> [ZCL BROADCAST apsFrame=${JSON.stringify(apsFrame)} header=${JSON.stringify(zclFrame.header)}]`, NS);
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const [status, messageTag] = await this.ezsp.send(enums_1.EmberOutgoingMessageType.BROADCAST, destination, apsFrame, data, 0, // alias
            0);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`~x~> [ZCL BROADCAST destination=${destination}] Failed to send with status=${enums_1.SLStatus[status]}.`);
            }
            // NOTE: since ezspMessageSentHandler could take a while here, we don't block, it'll just be logged if the delivery failed
            await (0, utils_1.wait)(QUEUE_BUSY_DEFER_MSEC);
        });
    }
    //---- InterPAN for Touchlink
    // XXX: There might be a better way to handle touchlink with ZLL ezsp functions, but I don't have any device to test so, didn't look into it...
    // TODO: check all this touchlink/interpan stuff
    // queued
    async setChannelInterPAN(channel) {
        return await this.queue.execute(async () => {
            this.interpanLock = true;
            const status = await this.ezsp.ezspSetLogicalAndRadioChannel(channel);
            if (status !== enums_1.SLStatus.OK) {
                this.interpanLock = false; // XXX: ok?
                throw new Error(`Failed to set InterPAN channel to '${channel}' with status=${enums_1.SLStatus[status]}.`);
            }
        });
    }
    // queued
    async sendZclFrameInterPANToIeeeAddr(zclFrame, ieeeAddress) {
        return await this.queue.execute(async () => {
            const msgBuffalo = new buffalo_1.EzspBuffalo(Buffer.alloc(consts_1.MAXIMUM_INTERPAN_LENGTH));
            // cache-enabled getters
            const sourcePanId = await this.emberGetPanId();
            const sourceEui64 = await this.emberGetEui64();
            msgBuffalo.writeUInt16(consts_1.LONG_DEST_FRAME_CONTROL | consts_1.MAC_ACK_REQUIRED); // macFrameControl
            msgBuffalo.writeUInt8(0); // sequence Skip Sequence number, stack sets the sequence number.
            msgBuffalo.writeUInt16(ZSpec.INVALID_PAN_ID); // destPanId
            msgBuffalo.writeIeeeAddr(ieeeAddress); // destAddress (longAddress)
            msgBuffalo.writeUInt16(sourcePanId); // sourcePanId
            msgBuffalo.writeIeeeAddr(sourceEui64); // sourceAddress
            msgBuffalo.writeUInt16(consts_1.STUB_NWK_FRAME_CONTROL); // nwkFrameControl
            msgBuffalo.writeUInt8(enums_1.EmberInterpanMessageType.UNICAST | consts_1.INTERPAN_APS_FRAME_TYPE); // apsFrameControl
            msgBuffalo.writeUInt16(zclFrame.cluster.ID);
            msgBuffalo.writeUInt16(ZSpec.TOUCHLINK_PROFILE_ID);
            logger_1.logger.debug(() => `~~~> [ZCL TOUCHLINK to=${ieeeAddress} header=${JSON.stringify(zclFrame.header)}]`, NS);
            const status = await this.ezsp.ezspSendRawMessage(Buffer.concat([msgBuffalo.getWritten(), zclFrame.toBuffer()]), enums_1.EmberTransmitPriority.NORMAL, true);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`~x~> [ZCL TOUCHLINK to=${ieeeAddress}] Failed to send with status=${enums_1.SLStatus[status]}.`);
            }
            // NOTE: can use ezspRawTransmitCompleteHandler if needed here
        });
    }
    // queued
    async sendZclFrameInterPANBroadcast(zclFrame, timeout) {
        const command = zclFrame.command;
        if (command.response === undefined) {
            throw new Error(`Command '${command.name}' has no response, cannot wait for response.`);
        }
        const endpoint = endpoints_1.FIXED_ENDPOINTS[0].endpoint;
        // just for waitress
        const apsFrame = {
            profileId: ZSpec.TOUCHLINK_PROFILE_ID,
            clusterId: zclFrame.cluster.ID,
            sourceEndpoint: endpoint, // arbitrary since not sent over-the-air
            destinationEndpoint: endpoint,
            options: enums_1.EmberApsOption.NONE,
            groupId: ZSpec.BroadcastAddress.SLEEPY,
            sequence: 0, // set by stack
        };
        return await this.queue.execute(async () => {
            const msgBuffalo = new buffalo_1.EzspBuffalo(Buffer.alloc(consts_1.MAXIMUM_INTERPAN_LENGTH));
            // cache-enabled getters
            const sourcePanId = await this.emberGetPanId();
            const sourceEui64 = await this.emberGetEui64();
            msgBuffalo.writeUInt16(consts_1.SHORT_DEST_FRAME_CONTROL); // macFrameControl
            msgBuffalo.writeUInt8(0); // sequence Skip Sequence number, stack sets the sequence number.
            msgBuffalo.writeUInt16(ZSpec.INVALID_PAN_ID); // destPanId
            msgBuffalo.writeUInt16(apsFrame.groupId); // destAddress (longAddress)
            msgBuffalo.writeUInt16(sourcePanId); // sourcePanId
            msgBuffalo.writeIeeeAddr(sourceEui64); // sourceAddress
            msgBuffalo.writeUInt16(consts_1.STUB_NWK_FRAME_CONTROL); // nwkFrameControl
            msgBuffalo.writeUInt8(enums_1.EmberInterpanMessageType.BROADCAST | consts_1.INTERPAN_APS_FRAME_TYPE); // apsFrameControl
            msgBuffalo.writeUInt16(apsFrame.clusterId);
            msgBuffalo.writeUInt16(apsFrame.profileId);
            const data = Buffer.concat([msgBuffalo.getWritten(), zclFrame.toBuffer()]);
            logger_1.logger.debug(() => `~~~> [ZCL TOUCHLINK BROADCAST header=${JSON.stringify(zclFrame.header)}]`, NS);
            const status = await this.ezsp.ezspSendRawMessage(data, enums_1.EmberTransmitPriority.NORMAL, true);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`~x~> [ZCL TOUCHLINK BROADCAST] Failed to send with status=${enums_1.SLStatus[status]}.`);
            }
            // NOTE: can use ezspRawTransmitCompleteHandler if needed here
            const result = await this.oneWaitress.startWaitingFor({
                target: undefined,
                apsFrame: apsFrame,
                zclSequence: zclFrame.header.transactionSequenceNumber,
                commandIdentifier: command.response,
            }, timeout);
            return result;
        });
    }
    // queued
    async restoreChannelInterPAN() {
        return await this.queue.execute(async () => {
            const status = await this.ezsp.ezspSetLogicalAndRadioChannel(this.networkOptions.channelList[0]);
            if (status !== enums_1.SLStatus.OK) {
                throw new Error(`Failed to restore InterPAN channel to '${this.networkOptions.channelList[0]}' with status=${enums_1.SLStatus[status]}.`);
            }
            // let adapter settle down
            await (0, utils_1.wait)(QUEUE_NETWORK_DOWN_DEFER_MSEC);
            this.interpanLock = false;
        });
    }
    //-- END Adapter implementation
    checkInterpanLock() {
        if (this.interpanLock) {
            throw new Error(`[INTERPAN MODE] Cannot execute non-InterPAN commands.`);
        }
    }
}
exports.EmberAdapter = EmberAdapter;
//# sourceMappingURL=emberAdapter.js.map