"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 zigbeeEntity_1 = require("./zigbeeEntity");
const NS = "zh:controller:endpoint";
class Endpoint extends zigbeeEntity_1.ZigbeeEntity {
    deviceID;
    inputClusters;
    outputClusters;
    profileID;
    // biome-ignore lint/style/useNamingConvention: cross-repo impact
    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 = 
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            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() {
        const device = this.getDevice();
        return this._configuredReportings.map((entry, index) => {
            const cluster = Zcl.Utils.getCluster(entry.cluster, entry.manufacturerCode, device.customClusters);
            const attribute = Zcl.Utils.getClusterAttribute(cluster, entry.attrId, entry.manufacturerCode) ?? {
                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() {
        const device = device_1.default.byIeeeAddr(this.deviceIeeeAddress);
        if (!device) {
            logger_1.logger.error(`Tried to get unknown/deleted device ${this.deviceIeeeAddress} from endpoint ${this.ID}.`, NS);
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            logger_1.logger.debug(new Error().stack, NS);
        }
        // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
        return device;
    }
    /**
     * @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.inputClusters.map((c) => this.getCluster(c));
    }
    /**
     * @returns {ZclTypes.Cluster[]}
     */
    getOutputClusters() {
        return this.outputClusters.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 != null) {
                entry.attributes = entry.attrs;
                delete entry.attrs;
            }
        }
        // Migrate cluster renames from https://github.com/Koenkk/zigbee-herdsman/pull/1503 @deprecated 3.0
        /* v8 ignore start */
        if (record.clusters.piRetailTunnel) {
            record.clusters.retailTunnel = record.clusters.piRetailTunnel;
            delete record.clusters.piRetailTunnel;
        }
        if (record.clusters.tunneling) {
            record.clusters.seTunneling = record.clusters.tunneling;
            delete record.clusters.tunneling;
        }
        if (record.clusters.haMeterIdentification) {
            record.clusters.seMeterIdentification = record.clusters.haMeterIdentification;
            delete record.clusters.haMeterIdentification;
        }
        /* v8 ignore stop */
        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 in list) {
            this.clusters[cluster.name].attributes[attribute] = list[attribute];
        }
    }
    getClusterAttributeValue(clusterKey, attributeKey) {
        const cluster = this.getCluster(clusterKey);
        if (this.clusters[cluster.name] && this.clusters[cluster.name].attributes) {
            // XXX: used to throw (behavior changed in #1455)
            const attribute = Zcl.Utils.getClusterAttribute(cluster, attributeKey, undefined);
            if (attribute) {
                return this.clusters[cluster.name].attributes[attribute.name];
            }
        }
        return undefined;
    }
    saveClusterAttributeReportConfig(clusterId, manufacturerCode, reportConfigs) {
        for (const entry of reportConfigs) {
            if (entry.direction === Zcl.Direction.SERVER_TO_CLIENT) {
                continue;
            }
            const existingConfigIdx = this._configuredReportings.findIndex((r) => r.cluster === clusterId &&
                r.attrId === entry.attrId &&
                (manufacturerCode === undefined || manufacturerCode === r.manufacturerCode));
            if (entry.status === Zcl.Status.SUCCESS) {
                if (existingConfigIdx > -1) {
                    this._configuredReportings[existingConfigIdx].minRepIntval = entry.minRepIntval;
                    this._configuredReportings[existingConfigIdx].maxRepIntval = entry.maxRepIntval;
                    this._configuredReportings[existingConfigIdx].repChange = entry.repChange;
                }
                else {
                    this._configuredReportings.push({
                        cluster: clusterId,
                        attrId: entry.attrId,
                        minRepIntval: entry.minRepIntval,
                        maxRepIntval: entry.maxRepIntval,
                        repChange: entry.repChange,
                        manufacturerCode,
                    });
                }
            }
            else {
                // UNSUPPORTED_ATTRIBUTE, UNREPORTABLE_ATTRIBUTE, NOT_FOUND
                if (existingConfigIdx > -1) {
                    this._configuredReportings.splice(existingConfigIdx, 1);
                }
            }
        }
        this.save();
    }
    saveBindings(binds) {
        this._binds = binds;
        this.save();
    }
    clearBindings() {
        this._binds.length = 0;
        this.save();
    }
    hasPendingRequests() {
        return this.pendingRequests.size > 0;
    }
    async sendPendingRequests(fastPolling) {
        return await this.pendingRequests.send(fastPolling);
    }
    async sendRequest(frame, options, func = () => {
        return entity_1.default.adapter.sendZclFrameToEndpoint(this.deviceIeeeAddress, this.deviceNetworkAddress, this.ID, frame, options.timeout, options.disableResponse, options.disableRecovery, options.srcEndpoint, options.profileId);
    }) {
        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, undefined, options?.manufacturerCode);
        const payload = [];
        // TODO: handle `attr.report !== true`
        for (const nameOrID in attributes) {
            const attribute = Zcl.Utils.getClusterAttribute(cluster, nameOrID, options?.manufacturerCode);
            if (attribute) {
                payload.push({ attrId: attribute.ID, attrData: attributes[nameOrID], dataType: attribute.type });
            }
            else if (!Number.isNaN(Number(nameOrID))) {
                const value = attributes[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(cluster, "report", payload, options, attributes);
    }
    async write(clusterKey, attributes, options) {
        const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
        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 in attributes) {
            const attribute = Zcl.Utils.getClusterAttribute(cluster, nameOrID, options?.manufacturerCode);
            if (attribute) {
                // TODO: handle `attr.writeOptional !== true`
                const attrData = Zcl.Utils.processAttributeWrite(attribute, attributes[nameOrID]);
                payload.push({ attrId: attribute.ID, attrData, dataType: attribute.type });
            }
            else if (!Number.isNaN(Number(nameOrID))) {
                const value = attributes[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(cluster, 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, undefined, options?.manufacturerCode);
        const payload = [];
        for (const nameOrID in attributes) {
            // biome-ignore lint/style/noNonNullAssertion: from loop
            const value = attributes[nameOrID];
            if (value.status !== undefined) {
                const attribute = Zcl.Utils.getClusterAttribute(cluster, nameOrID, options?.manufacturerCode);
                if (attribute) {
                    payload.push({ attrId: attribute.ID, status: value.status });
                }
                else if (!Number.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(cluster, "writeRsp", payload, { direction: Zcl.Direction.SERVER_TO_CLIENT, ...options, transactionSequenceNumber }, attributes);
    }
    // XXX: ideally, the return type should limit to the contents of the `attributes` param
    async read(clusterKey, attributes, options) {
        const device = this.getDevice();
        const cluster = this.getCluster(clusterKey, device, options?.manufacturerCode);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, attributes, optionsWithDefaults.manufacturerCode, "read");
        const payload = [];
        // TODO: handle `attr.required !== true` => should not throw
        for (const attribute of attributes) {
            if (typeof attribute === "number") {
                payload.push({ attrId: attribute });
            }
            else {
                const attr = Zcl.Utils.getClusterAttribute(cluster, attribute, options?.manufacturerCode);
                if (attr) {
                    Zcl.Utils.processAttributePreRead(attr);
                    payload.push({ attrId: attr.ID });
                }
                else {
                    logger_1.logger.warning(`Ignoring unknown attribute ${attribute} in cluster ${cluster.name}`, NS);
                }
            }
        }
        // TODO: could be sending empty array payload
        const resultFrame = await this.zclCommand(cluster, "read", payload, optionsWithDefaults, attributes, true);
        return resultFrame
            ? ZclFrameConverter.attributeKeyValue(resultFrame, device.manufacturerID, device.customClusters)
            : {};
    }
    async readResponse(clusterKey, transactionSequenceNumber, attributes, options) {
        (0, node_assert_1.default)(options?.transactionSequenceNumber === undefined, "Use parameter");
        const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
        const payload = [];
        for (const nameOrID in attributes) {
            const attribute = Zcl.Utils.getClusterAttribute(cluster, nameOrID, options?.manufacturerCode);
            if (attribute) {
                payload.push({ attrId: attribute.ID, attrData: attributes[nameOrID], dataType: attribute.type, status: 0 });
            }
            else if (!Number.isNaN(Number(nameOrID))) {
                const value = attributes[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(cluster, "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})`;
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    save() {
        this.getDevice().save();
    }
    async unbind(clusterKey, target, force = false) {
        // When force is true the unbind is done even when the bind is not in the bind list, additionally when the target is a number
        // it will not check if the group exists.
        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) {
                target = groupTarget;
            }
            else if (!force) {
                throw new Error(`${action} invalid target '${target}' (no group with this ID exists).`);
            }
        }
        const destinationAddress = target instanceof Endpoint ? target.deviceIeeeAddress : target instanceof group_1.default ? target.groupID : target;
        const log = `${action} from '${target instanceof Endpoint ? `${destinationAddress}/${target.ID}` : destinationAddress}'`;
        const index = target instanceof Endpoint || target instanceof group_1.default ? this.getBindIndex(cluster.ID, target) : -1;
        if (index === -1 && !force) {
            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 : typeof target === "number" ? target : 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]);
                }
            }
            if (index !== -1) {
                this._binds.splice(index, 1);
                this.save();
            }
        }
        catch (error) {
            const err = error;
            err.message = `${log} failed (${err.message})`;
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            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, undefined, options?.manufacturerCode);
        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;
            let attrId;
            if (typeof item.attribute === "object") {
                dataType = item.attribute.type;
                attrId = item.attribute.ID;
            }
            else {
                const attribute = Zcl.Utils.getClusterAttribute(cluster, item.attribute, optionsWithDefaults.manufacturerCode);
                if (attribute) {
                    dataType = attribute.type;
                    attrId = attribute.ID;
                }
                else {
                    throw new Error(`Invalid attribute '${item.attribute}' for cluster '${clusterKey}'`);
                }
            }
            return {
                direction: Zcl.Direction.CLIENT_TO_SERVER,
                attrId,
                dataType,
                minRepIntval: item.minimumReportInterval,
                maxRepIntval: item.maximumReportInterval,
                repChange: item.reportableChange,
            };
        });
        await this.zclCommand(cluster, "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,
                    // expects items[].attribute to always point to a number DataType
                    repChange: entry.repChange,
                    manufacturerCode: optionsWithDefaults.manufacturerCode,
                });
            }
        }
        this.save();
    }
    async readReportingConfig(clusterKey, items, options) {
        const cluster = this.getCluster(clusterKey, undefined, options?.manufacturerCode);
        const optionsWithDefaults = this.getOptionsWithDefaults(options, true, Zcl.Direction.CLIENT_TO_SERVER, cluster.manufacturerCode);
        optionsWithDefaults.manufacturerCode = this.ensureManufacturerCodeIsUniqueAndGet(cluster, items, optionsWithDefaults.manufacturerCode, "readReportingConfig");
        const payload = [];
        for (const item of items) {
            if (typeof item.attribute === "object") {
                payload.push({ direction: item.direction ?? Zcl.Direction.CLIENT_TO_SERVER, attrId: item.attribute.ID });
            }
            else {
                const attribute = Zcl.Utils.getClusterAttribute(cluster, item.attribute, optionsWithDefaults.manufacturerCode);
                if (attribute) {
                    payload.push({ direction: item.direction ?? Zcl.Direction.CLIENT_TO_SERVER, attrId: attribute.ID });
                }
                else {
                    logger_1.logger.warning(`Ignoring unknown attribute ${item.attribute} in cluster ${cluster.name}`, NS);
                }
            }
        }
        // TODO: could be sending empty array payload
        // don't check status otherwise whole command fails (we want to cherry-pick here)
        const response = await this.zclCommand(cluster, "readReportConfig", payload, optionsWithDefaults, items, false);
        if (response) {
            this.saveClusterAttributeReportConfig(response.cluster.ID, optionsWithDefaults.manufacturerCode, response.payload);
            return response.payload;
        }
        throw new Error("No response received");
    }
    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, options?.manufacturerCode);
        const command = Zcl.Utils.getClusterCommandResponse(cluster, 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, cluster, 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 {
            // Broadcast Green Power responses
            if (this.ID === 242) {
                await this.sendRequest(frame, optionsWithDefaults, async () => {
                    await entity_1.default.adapter.sendZclFrameToAll(242, frame, 242, enums_1.BroadcastAddress.RX_ON_WHEN_IDLE);
                });
            }
            else {
                await this.sendRequest(frame, optionsWithDefaults);
            }
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            logger_1.logger.debug(err.stack, NS);
            throw error;
        }
    }
    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) {
        let firstManufacturerCode;
        let codeSet = false;
        for (const nameOrID of attributes) {
            let attributeID;
            if (typeof nameOrID === "object") {
                // ConfigureReportingItem
                if (typeof nameOrID.attribute !== "object") {
                    attributeID = nameOrID.attribute;
                }
                else {
                    if (!codeSet) {
                        firstManufacturerCode = fallbackManufacturerCode;
                        codeSet = true;
                    }
                    else if (firstManufacturerCode !== fallbackManufacturerCode) {
                        throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
                    }
                    continue;
                }
            }
            else {
                // string || number
                attributeID = nameOrID;
            }
            // we fall back to caller|cluster provided manufacturerCode
            const attribute = Zcl.Utils.getClusterAttribute(cluster, attributeID, undefined);
            const manufacturerCode = attribute
                ? attribute.manufacturerCode === undefined
                    ? fallbackManufacturerCode
                    : attribute.manufacturerCode
                : fallbackManufacturerCode;
            if (!codeSet) {
                firstManufacturerCode = manufacturerCode;
                codeSet = true;
            }
            else if (firstManufacturerCode !== manufacturerCode) {
                throw new Error(`Cannot have attributes with different manufacturerCode in single '${caller}' call`);
            }
        }
        return firstManufacturerCode;
    }
    async addToGroup(group) {
        await this.zclCommand("genGroups", "add", { groupid: group.groupID, groupname: "" }, undefined, undefined, true, Zcl.FrameType.SPECIFIC);
        group.addMember(this);
    }
    getCluster(clusterKey, device = undefined, manufacturerCode = undefined) {
        if (!device) {
            device = this.getDevice();
        }
        return Zcl.Utils.getCluster(clusterKey, manufacturerCode ?? 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.zclCommand("genGroups", "remove", { groupid: group instanceof group_1.default ? group.groupID : group }, undefined, undefined, true, Zcl.FrameType.SPECIFIC);
        if (group instanceof group_1.default) {
            group.removeMember(this);
        }
    }
    async removeFromAllGroups() {
        await this.zclCommand("genGroups", "removeAll", {}, { disableDefaultResponse: true }, undefined, false, Zcl.FrameType.SPECIFIC);
        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 = typeof clusterKey === "object" ? clusterKey : this.getCluster(clusterKey, device, options?.manufacturerCode);
        const command = typeof commandKey === "object"
            ? commandKey
            : frameType === Zcl.FrameType.GLOBAL
                ? Zcl.Utils.getGlobalCommand(commandKey)
                : Zcl.Utils.getClusterCommand(cluster, 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, cluster, 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 (checkStatus && !optionsWithDefaults.disableResponse) {
                    this.checkStatus(resultFrame.payload);
                }
                return resultFrame;
            }
        }
        catch (error) {
            const err = error;
            err.message = `${createLogMessage()} failed (${err.message})`;
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            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, options?.manufacturerCode);
        const command = Zcl.Utils.getClusterCommand(cluster, 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, cluster, 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