"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.Endpoint = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const logger_1 = require("../../utils/logger");
const ZSpec = __importStar(require("../../zspec"));
const enums_1 = require("../../zspec/enums");
const Zcl = __importStar(require("../../zspec/zcl"));
const Zdo = __importStar(require("../../zspec/zdo"));
const request_1 = __importDefault(require("../helpers/request"));
const requestQueue_1 = __importDefault(require("../helpers/requestQueue"));
const ZclFrameConverter = __importStar(require("../helpers/zclFrameConverter"));
const zclTransactionSequenceNumber_1 = __importDefault(require("../helpers/zclTransactionSequenceNumber"));
const device_1 = __importDefault(require("./device"));
const entity_1 = __importDefault(require("./entity"));
const group_1 = __importDefault(require("./group"));
const NS = 'zh:controller:endpoint';
class Endpoint extends entity_1.default {
    deviceID;
    inputClusters;
    outputClusters;
    profileID;
    ID;
    clusters;
    deviceIeeeAddress;
    deviceNetworkAddress;
    _binds;
    _configuredReportings;
    meta;
    pendingRequests;
    // Getters/setters
    get binds() {
        const binds = [];
        for (const bind of this._binds) {
            // XXX: properties assumed valid when associated to `type`
            const target = bind.type === 'endpoint' ? device_1.default.byIeeeAddr(bind.deviceIeeeAddress)?.getEndpoint(bind.endpointID) : group_1.default.byGroupID(bind.groupID);
            if (target) {
                binds.push({ target, cluster: this.getCluster(bind.cluster) });
            }
        }
        return binds;
    }
    get configuredReportings() {
        return this._configuredReportings.map((entry, index) => {
            const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, this.getDevice().customClusters);
            const attribute = cluster.hasAttribute(entry.attrId)
                ? cluster.getAttribute(entry.attrId)
                : { ID: entry.attrId, name: `attr${index}`, type: Zcl.DataType.UNKNOWN, manufacturerCode: undefined };
            return {
                cluster,
                attribute,
                minimumReportInterval: entry.minRepIntval,
                maximumReportInterval: entry.maxRepIntval,
                reportableChange: entry.repChange,
            };
        });
    }
    constructor(ID, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, clusters, binds, configuredReportings, meta) {
        super();
        this.ID = ID;
        this.profileID = profileID;
        this.deviceID = deviceID;
        this.inputClusters = inputClusters;
        this.outputClusters = outputClusters;
        this.deviceNetworkAddress = deviceNetworkAddress;
        this.deviceIeeeAddress = deviceIeeeAddress;
        this.clusters = clusters;
        this._binds = binds;
        this._configuredReportings = configuredReportings;
        this.meta = meta;
        this.pendingRequests = new requestQueue_1.default(this);
    }
    /**
     * Get device of this endpoint
     */
    getDevice() {
        return device_1.default.byIeeeAddr(this.deviceIeeeAddress); // XXX: no way for device to not exist?
    }
    /**
     * @param {number|string} clusterKey
     * @returns {boolean}
     */
    supportsInputCluster(clusterKey) {
        const cluster = this.getCluster(clusterKey);
        return this.inputClusters.includes(cluster.ID);
    }
    /**
     * @param {number|string} clusterKey
     * @returns {boolean}
     */
    supportsOutputCluster(clusterKey) {
        const cluster = this.getCluster(clusterKey);
        return this.outputClusters.includes(cluster.ID);
    }
    /**
     * @returns {ZclTypes.Cluster[]}
     */
    getInputClusters() {
        return this.clusterNumbersToClusters(this.inputClusters);
    }
    /**
     * @returns {ZclTypes.Cluster[]}
     */
    getOutputClusters() {
        return this.clusterNumbersToClusters(this.outputClusters);
    }
    clusterNumbersToClusters(clusterNumbers) {
        return clusterNumbers.map((c) => this.getCluster(c));
    }
    /*
     * CRUD
     */
    static fromDatabaseRecord(record, deviceNetworkAddress, deviceIeeeAddress) {
        // Migrate attrs to attributes
        for (const entryKey in record.clusters) {
            const entry = record.clusters[entryKey];
            if (entry.attrs != undefined) {
                entry.attributes = entry.attrs;
                delete entry.attrs;
            }
        }
        return new Endpoint(record.epId, record.profId, record.devId, record.inClusterList, record.outClusterList, deviceNetworkAddress, deviceIeeeAddress, record.clusters, record.binds || [], record.configuredReportings || [], record.meta || {});
    }
    toDatabaseRecord() {
        return {
            profId: this.profileID,
            epId: this.ID,
            devId: this.deviceID,
            inClusterList: this.inputClusters,
            outClusterList: this.outputClusters,
            clusters: this.clusters,
            binds: this._binds,
            configuredReportings: this._configuredReportings,
            meta: this.meta,
        };
    }
    static create(ID, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress) {
        return new Endpoint(ID, profileID, deviceID, inputClusters, outputClusters, deviceNetworkAddress, deviceIeeeAddress, {}, [], [], {});
    }
    saveClusterAttributeKeyValue(clusterKey, list) {
        const cluster = this.getCluster(clusterKey);
        if (!this.clusters[cluster.name])
            this.clusters[cluster.name] = { attributes: {} };
        for (const [attribute, value] of Object.entries(list)) {
            this.clusters[cluster.name].attributes[attribute] = value;
        }
    }
    getClusterAttributeValue(clusterKey, attributeKey) {
        const cluster = this.getCluster(clusterKey);
        const attribute = cluster.getAttribute(attributeKey);
        if (this.clusters[cluster.name] && this.clusters[cluster.name].attributes) {
            return this.clusters[cluster.name].attributes[attribute.name];
        }
        return undefined;
    }
    hasPendingRequests() {
        return this.pendingRequests.size > 0;
    }
    async sendPendingRequests(fastPolling) {
        return await this.pendingRequests.send(fastPolling);
    }
    async sendRequest(frame, options, func = (d) => {
        return entity_1.default.adapter.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, d, options.timeout, options.disableResponse, options.disableRecovery, options.srcEndpoint);
    }) {
        const logPrefix = `Request Queue (${this.deviceIeeeAddress}/${this.ID}): `;
        const device = this.getDevice();
        const request = new request_1.default(func, frame, device.pendingRequestTimeout, options.sendPolicy);
        if (request.sendPolicy !== 'bulk') {
            // Check if such a request is already in the queue and remove the old one(s) if necessary
            this.pendingRequests.filter(request);
        }
        // send without queueing if sendPolicy is 'immediate' or if the device has no timeout set
        if (request.sendPolicy === 'immediate' || !device.pendingRequestTimeout) {
            if (device.pendingRequestTimeout > 0) {
                logger_1.logger.debug(logPrefix + `send ${frame.command.name} request immediately (sendPolicy=${options.sendPolicy})`, NS);
            }
            return await request.send();
        }
        // If this is a bulk message, we queue directly.
        if (request.sendPolicy === 'bulk') {
            logger_1.logger.debug(logPrefix + `queue request (${this.pendingRequests.size})`, NS);
            return await this.pendingRequests.queue(request);
        }
        try {
            logger_1.logger.debug(logPrefix + `send request`, NS);
            return await request.send();
        }
        catch (error) {
            // If we got a failed transaction, the device is likely sleeping.
            // Queue for transmission later.
            logger_1.logger.debug(logPrefix + `queue request (transaction failed) (${error})`, NS);
            return await this.pendingRequests.queue(request);
        }
    }
    /*
     * Zigbee functions
     */
    checkStatus(payload) {
        const codes = Array.isArray(payload) ? payload.map((i) => i.status) : [payload.statusCode];
        const invalid = codes.find((c) => c !== Zcl.Status.SUCCESS);
        if (invalid)
            throw new Zcl.StatusError(invalid);
    }
    async report(clusterKey, attributes, options) {
        const cluster = this.getCluster(clusterKey);
        const payload = [];
        for (const [nameOrID, value] of Object.entries(attributes)) {
            if (cluster.hasAttribute(nameOrID)) {
                const attribute = cluster.getAttribute(nameOrID);
                payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type });
            }
            else if (!isNaN(Number(nameOrID))) {
                payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type });
            }
            else {
                throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
            }
        }
        await this.zclCommand(clusterKey, 'report', payload, options, attributes);
    }
    async write(clusterKey, attributes, options) {
        const cluster = this.getCluster(clusterKey);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, Object.keys(attributes), optionsWithDefaults.manufacturerCode, 'write');
        const payload = [];
        for (const [nameOrID, value] of Object.entries(attributes)) {
            if (cluster.hasAttribute(nameOrID)) {
                const attribute = cluster.getAttribute(nameOrID);
                payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type });
            }
            else if (!isNaN(Number(nameOrID))) {
                payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type });
            }
            else {
                throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
            }
        }
        await this.zclCommand(clusterKey, optionsWithDefaults.writeUndiv ? 'writeUndiv' : 'write', payload, optionsWithDefaults, attributes, true);
    }
    async writeResponse(clusterKey, transactionSequenceNumber, attributes, options) {
        (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, 'Use parameter');
        const cluster = this.getCluster(clusterKey);
        const payload = [];
        for (const [nameOrID, value] of Object.entries(attributes)) {
            if (value.status !== undefined) {
                if (cluster.hasAttribute(nameOrID)) {
                    const attribute = cluster.getAttribute(nameOrID);
                    payload.push({ attrId: attribute.ID, status: value.status });
                }
                else if (!isNaN(Number(nameOrID))) {
                    payload.push({ attrId: Number(nameOrID), status: value.status });
                }
                else {
                    throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
                }
            }
            else {
                throw new Error(`Missing attribute 'status'`);
            }
        }
        await this.zclCommand(clusterKey, 'writeRsp', payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes);
    }
    async read(clusterKey, attributes, options) {
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, optionsWithDefaults.manufacturerCode, 'read');
        const payload = [];
        for (const attribute of attributes) {
            payload.push({ attrId: typeof attribute === 'number' ? attribute : cluster.getAttribute(attribute).ID });
        }
        const resultFrame = await this.zclCommand(clusterKey, 'read', payload, optionsWithDefaults, attributes, true);
        if (resultFrame) {
            return ZclFrameConverter.attributeKeyValue(resultFrame, device.manufacturerID, device.customClusters);
        }
        return {};
    }
    async readResponse(clusterKey, transactionSequenceNumber, attributes, options) {
        (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, 'Use parameter');
        const cluster = this.getCluster(clusterKey);
        const payload = [];
        for (const [nameOrID, value] of Object.entries(attributes)) {
            if (cluster.hasAttribute(nameOrID)) {
                const attribute = cluster.getAttribute(nameOrID);
                payload.push({ attrId: attribute.ID, attrData: value, dataType: attribute.type, status: 0 });
            }
            else if (!isNaN(Number(nameOrID))) {
                payload.push({ attrId: Number(nameOrID), attrData: value.value, dataType: value.type, status: 0 });
            }
            else {
                throw new Error(`Unknown attribute '${nameOrID}', specify either an existing attribute or a number`);
            }
        }
        await this.zclCommand(clusterKey, 'readRsp', payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes);
    }
    async updateSimpleDescriptor() {
        const clusterId = Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST;
        const zdoPayload = Zdo.Buffalo.buildRequest(entity_1.default.adapter.hasZdoMessageOverhead, clusterId, this.deviceNetworkAddress, this.ID);
        const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, clusterId, zdoPayload, false);
        if (!Zdo.Buffalo.checkStatus(response)) {
            throw new Zdo.StatusError(response[0]);
        }
        const simpleDescriptor = response[1];
        this.profileID = simpleDescriptor.profileId;
        this.deviceID = simpleDescriptor.deviceId;
        this.inputClusters = simpleDescriptor.inClusterList;
        this.outputClusters = simpleDescriptor.outClusterList;
    }
    hasBind(clusterId, target) {
        return this.getBindIndex(clusterId, target) !== -1;
    }
    getBindIndex(clusterId, target) {
        return this.binds.findIndex((b) => b.cluster.ID === clusterId && b.target === target);
    }
    addBinding(clusterKey, target) {
        const cluster = this.getCluster(clusterKey);
        if (typeof target === 'number') {
            target = group_1.default.byGroupID(target) || group_1.default.create(target);
        }
        this.addBindingInternal(cluster, target);
    }
    addBindingInternal(cluster, target) {
        if (!this.hasBind(cluster.ID, target)) {
            if (target instanceof group_1.default) {
                this._binds.push({ cluster: cluster.ID, groupID: target.groupID, type: 'group' });
            }
            else {
                this._binds.push({
                    cluster: cluster.ID,
                    type: 'endpoint',
                    deviceIeeeAddress: target.deviceIeeeAddress,
                    endpointID: target.ID,
                });
            }
            this.save();
        }
    }
    async bind(clusterKey, target) {
        const cluster = this.getCluster(clusterKey);
        if (typeof target === 'number') {
            target = group_1.default.byGroupID(target) || group_1.default.create(target);
        }
        const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID;
        const log = `Bind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
        logger_1.logger.debug(log, NS);
        try {
            const zdoClusterId = Zdo.ClusterId.BIND_REQUEST;
            const zdoPayload = Zdo.Buffalo.buildRequest(entity_1.default.adapter.hasZdoMessageOverhead, zdoClusterId, this.deviceIeeeAddress, this.ID, cluster.ID, target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, target instanceof Endpoint ? target.deviceIeeeAddress : ZSpec.BLANK_EUI64, target instanceof group_1.default ? target.groupID : 0, target instanceof Endpoint ? target.ID : 0xff);
            const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false);
            if (!Zdo.Buffalo.checkStatus(response)) {
                throw new Zdo.StatusError(response[0]);
            }
            this.addBindingInternal(cluster, target);
        }
        catch (error) {
            const err = error;
            err.message = `${log} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    save() {
        this.getDevice().save();
    }
    async unbind(clusterKey, target) {
        const cluster = this.getCluster(clusterKey);
        const action = `Unbind ${this.deviceIeeeAddress}/${this.ID} ${cluster.name}`;
        if (typeof target === 'number') {
            const groupTarget = group_1.default.byGroupID(target);
            if (!groupTarget) {
                throw new Error(`${action} invalid target '${target}' (no group with this ID exists).`);
            }
            target = groupTarget;
        }
        const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target.groupID;
        const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
        const index = this.getBindIndex(cluster.ID, target);
        if (index === -1) {
            logger_1.logger.debug(`${log} no bind present, skipping.`, NS);
            return;
        }
        logger_1.logger.debug(log, NS);
        try {
            const zdoClusterId = Zdo.ClusterId.UNBIND_REQUEST;
            const zdoPayload = Zdo.Buffalo.buildRequest(entity_1.default.adapter.hasZdoMessageOverhead, zdoClusterId, this.deviceIeeeAddress, this.ID, cluster.ID, target instanceof Endpoint ? Zdo.UNICAST_BINDING : Zdo.MULTICAST_BINDING, target instanceof Endpoint ? target.deviceIeeeAddress : ZSpec.BLANK_EUI64, target instanceof group_1.default ? target.groupID : 0, target instanceof Endpoint ? target.ID : 0xff);
            const response = await entity_1.default.adapter.sendZdo(this.deviceIeeeAddress, this.deviceNetworkAddress, zdoClusterId, zdoPayload, false);
            if (!Zdo.Buffalo.checkStatus(response)) {
                if (response[0] === Zdo.Status.NO_ENTRY) {
                    logger_1.logger.debug(`${log} no entry on device, removing entry from database.`, NS);
                }
                else {
                    throw new Zdo.StatusError(response[0]);
                }
            }
            this._binds.splice(index, 1);
            this.save();
        }
        catch (error) {
            const err = error;
            err.message = `${log} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    async defaultResponse(commandID, status, clusterID, transactionSequenceNumber, options) {
        (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, 'Use parameter');
        const payload = { cmdId: commandID, statusCode: status };
        await this.zclCommand(clusterID, 'defaultRsp', payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber });
    }
    async configureReporting(clusterKey, items, options) {
        const cluster = this.getCluster(clusterKey);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, items, optionsWithDefaults.manufacturerCode, 'configureReporting');
        const payload = items.map((item) => {
            let dataType, attrId;
            if (typeof item.attribute === 'object') {
                dataType = item.attribute.type;
                attrId = item.attribute.ID;
            }
            else {
                if (cluster.hasAttribute(item.attribute)) {
                    const attribute = cluster.getAttribute(item.attribute);
                    dataType = attribute.type;
                    attrId = attribute.ID;
                }
            }
            return {
                direction: Zcl.Direction.CLIENT_TO_SERVER,
                attrId,
                dataType,
                minRepIntval: item.minimumReportInterval,
                maxRepIntval: item.maximumReportInterval,
                repChange: item.reportableChange,
            };
        });
        await this.zclCommand(clusterKey, 'configReport', payload, optionsWithDefaults, items, true);
        for (const e of payload) {
            this._configuredReportings = this._configuredReportings.filter((c) => !(c.attrId === e.attrId &&
                c.cluster === cluster.ID &&
                (!('manufacturerCode' in c) || c.manufacturerCode === optionsWithDefaults.manufacturerCode)));
        }
        for (const entry of payload) {
            if (entry.maxRepIntval !== 0xffff) {
                this._configuredReportings.push({
                    cluster: cluster.ID,
                    attrId: entry.attrId,
                    minRepIntval: entry.minRepIntval,
                    maxRepIntval: entry.maxRepIntval,
                    repChange: entry.repChange,
                    manufacturerCode: optionsWithDefaults.manufacturerCode,
                });
            }
        }
        this.save();
    }
    async writeStructured(clusterKey, payload, options) {
        await this.zclCommand(clusterKey, 'writeStructured', payload, options);
        // TODO: support `writeStructuredResponse`
    }
    async command(clusterKey, commandKey, payload, options) {
        const frame = await this.zclCommand(clusterKey, commandKey, payload, options, undefined, false, Zcl.FrameType.SPECIFIC);
        if (frame) {
            return frame.payload;
        }
    }
    async commandResponse(clusterKey, commandKey, payload, options, transactionSequenceNumber) {
        (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, 'Use parameter');
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device);
        const command = cluster.getCommandResponse(commandKey);
        transactionSequenceNumber = transactionSequenceNumber || zclTransactionSequenceNumber_1.default.next();
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.SERVER_TO_CLIENT, cluster.manufacturerCode);
        const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, transactionSequenceNumber, command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits);
        const createLogMessage = () => `CommandResponse ${this.deviceIeeeAddress}/${this.ID} ` +
            `${cluster.name}.${command.name}(${JSON.stringify(payload)}, ${JSON.stringify(optionsWithDefaults)})`;
        logger_1.logger.debug(createLogMessage, NS);
        try {
            await this.sendRequest(frame, optionsWithDefaults, async (f) => {
                // Broadcast Green Power responses
                if (this.ID === 242) {
                    await entity_1.default.adapter.sendZclFrameToAll(242, f, 242, enums_1.BroadcastAddress.RX_ON_WHEN_IDLE);
                }
                else {
                    await entity_1.default.adapter.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, f, optionsWithDefaults.timeout, optionsWithDefaults.disableResponse, optionsWithDefaults.disableRecovery, optionsWithDefaults.srcEndpoint);
                }
            });
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    waitForCommand(clusterKey, commandKey, transactionSequenceNumber, timeout) {
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device);
        const command = cluster.getCommand(commandKey);
        const waiter = entity_1.default.adapter.waitFor(this.deviceNetworkAddress, this.ID, Zcl.FrameType.SPECIFIC, Zcl.Direction.CLIENT_TO_SERVER, transactionSequenceNumber, cluster.ID, command.ID, timeout);
        const promise = new Promise((resolve, reject) => {
            waiter.promise.then((payload) => {
                const frame = Zcl.Frame.fromBuffer(payload.clusterID, payload.header, payload.data, device.customClusters);
                resolve({ header: frame.header, payload: frame.payload });
            }, (error) => reject(error));
        });
        return { promise, cancel: waiter.cancel };
    }
    getOptionsWithDefaults(options, disableDefaultResponse, direction, manufacturerCode) {
        return {
            timeout: 10000,
            disableResponse: false,
            disableRecovery: false,
            disableDefaultResponse,
            direction,
            srcEndpoint: undefined,
            reservedBits: 0,
            manufacturerCode,
            transactionSequenceNumber: undefined,
            writeUndiv: false,
            ...(options || {}),
        };
    }
    ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, fallbackManufacturerCode, // XXX: problematic undefined for a "fallback"?
    caller) {
        const manufacturerCodes = new Set(attributes.map((nameOrID) => {
            let attributeID;
            if (typeof nameOrID == 'object') {
                // ConfigureReportingItem
                if (typeof nameOrID.attribute !== 'object') {
                    attributeID = nameOrID.attribute;
                }
                else {
                    return fallbackManufacturerCode;
                }
            }
            else {
                // string || number
                attributeID = nameOrID;
            }
            // we fall back to caller|cluster provided manufacturerCode
            if (cluster.hasAttribute(attributeID)) {
                const attribute = cluster.getAttribute(attributeID);
                return attribute.manufacturerCode === undefined ? fallbackManufacturerCode : attribute.manufacturerCode;
            }
            else {
                // unknown attribute, we should not fail on this here
                return fallbackManufacturerCode;
            }
        }));
        if (manufacturerCodes.size == 1) {
            return manufacturerCodes.values().next().value;
        }
        else {
            throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
        }
    }
    async addToGroup(group) {
        await this.command('genGroups', 'add', { groupid: group.groupID, groupname: '' });
        group.addMember(this);
    }
    getCluster(clusterKey, device = undefined) {
        device = device ?? this.getDevice();
        return Zcl.Utils.getCluster(clusterKey, device.manufacturerID, device.customClusters);
    }
    /**
     * Remove endpoint from a group, accepts both a Group and number as parameter.
     * The number parameter type should only be used when removing from a group which is not known
     * to zigbee-herdsman.
     */
    async removeFromGroup(group) {
        await this.command('genGroups', 'remove', { groupid: group instanceof group_1.default ? group.groupID : group });
        if (group instanceof group_1.default) {
            group.removeMember(this);
        }
    }
    async removeFromAllGroups() {
        await this.command('genGroups', 'removeAll', {}, { disableDefaultResponse: true });
        this.removeFromAllGroupsDatabase();
    }
    removeFromAllGroupsDatabase() {
        for (const group of group_1.default.allIterator()) {
            if (group.hasMember(this)) {
                group.removeMember(this);
            }
        }
    }
    async zclCommand(clusterKey, commandKey, payload, options, logPayload, checkStatus = false, frameType = Zcl.FrameType.GLOBAL) {
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device);
        const command = frameType == Zcl.FrameType.GLOBAL ? Zcl.Utils.getGlobalCommand(commandKey) : cluster.getCommand(commandKey);
        const hasResponse = frameType == Zcl.FrameType.GLOBAL ? true : command.response != undefined;
        const optionsWithDefaults = this.getOptionsWithDefaults(options, hasResponse, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        const frame = Zcl.Frame.create(frameType, optionsWithDefaults.direction, optionsWithDefaults.disableDefaultResponse, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits);
        const createLogMessage = () => `ZCL command ${this.deviceIeeeAddress}/${this.ID} ` +
            `${cluster.name}.${command.name}(${JSON.stringify(logPayload ? logPayload : payload)}, ${JSON.stringify(optionsWithDefaults)})`;
        logger_1.logger.debug(createLogMessage, NS);
        try {
            const result = await this.sendRequest(frame, optionsWithDefaults);
            if (result) {
                const resultFrame = Zcl.Frame.fromBuffer(result.clusterID, result.header, result.data, device.customClusters);
                if (result && checkStatus && !optionsWithDefaults.disableResponse) {
                    this.checkStatus(resultFrame.payload);
                }
                return resultFrame;
            }
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    async zclCommandBroadcast(endpoint, destination, clusterKey, commandKey, payload, options) {
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device);
        const command = cluster.getCommand(commandKey);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        const sourceEndpoint = optionsWithDefaults.srcEndpoint ?? this.ID;
        const frame = Zcl.Frame.create(Zcl.FrameType.SPECIFIC, optionsWithDefaults.direction, true, optionsWithDefaults.manufacturerCode, optionsWithDefaults.transactionSequenceNumber ?? zclTransactionSequenceNumber_1.default.next(), command.name, cluster.name, payload, device.customClusters, optionsWithDefaults.reservedBits);
        logger_1.logger.debug(() => `ZCL command broadcast ${this.deviceIeeeAddress}/${sourceEndpoint} to ${destination}/${endpoint} ` +
            `${cluster.name}.${command.name}(${JSON.stringify({ payload, optionsWithDefaults })})`, NS);
        // if endpoint===0xFF ("broadcast endpoint"), deliver to all endpoints supporting cluster, should be avoided whenever possible
        await entity_1.default.adapter.sendZclFrameToAll(endpoint, frame, sourceEndpoint, destination);
    }
}
exports.Endpoint = Endpoint;
exports.default = Endpoint;
//# sourceMappingURL=endpoint.js.map