"use strict";
/* v8 ignore start */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZiGateAdapter = void 0;
const utils_1 = require("../../../utils");
const logger_1 = require("../../../utils/logger");
const ZSpec = __importStar(require("../../../zspec"));
const Zcl = __importStar(require("../../../zspec/zcl"));
const Zdo = __importStar(require("../../../zspec/zdo"));
const adapter_1 = __importDefault(require("../../adapter"));
const constants_1 = require("../driver/constants");
const zigate_1 = __importDefault(require("../driver/zigate"));
const patchZdoBuffaloBE_1 = require("./patchZdoBuffaloBE");
const NS = "zh:zigate";
const default_bind_group = 901; // https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/lib/constants.js#L3
class ZiGateAdapter extends adapter_1.default {
    driver;
    joinPermitted;
    waitress;
    closing;
    queue;
    constructor(networkOptions, serialPortOptions, backupPath, adapterOptions) {
        (0, patchZdoBuffaloBE_1.patchZdoBuffaloBE)();
        super(networkOptions, serialPortOptions, backupPath, adapterOptions);
        this.hasZdoMessageOverhead = false; // false for requests, true for responses
        this.manufacturerID = Zcl.ManufacturerCode.RESERVED_10;
        this.joinPermitted = false;
        this.closing = false;
        const concurrent = this.adapterOptions?.concurrent ? this.adapterOptions.concurrent : 2;
        logger_1.logger.debug(`Adapter concurrent: ${concurrent}`, NS);
        this.queue = new utils_1.Queue(concurrent);
        // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
        this.driver = new zigate_1.default(serialPortOptions.path, serialPortOptions);
        this.waitress = new utils_1.Waitress(adapter_1.default.zclWaitressValidator, adapter_1.default.clusterWaitressTimeoutFormatter);
        this.driver.on("received", this.dataListener.bind(this));
        this.driver.on("LeaveIndication", this.leaveIndicationListener.bind(this));
        this.driver.on("DeviceAnnounce", this.deviceAnnounceListener.bind(this));
        this.driver.on("close", this.onZiGateClose.bind(this));
        this.driver.on("zdoResponse", this.onZdoResponse.bind(this));
    }
    /**
     * Adapter methods
     */
    async start() {
        let startResult = "resumed";
        try {
            await this.driver.open();
            logger_1.logger.info("Connected to ZiGate adapter successfully.", NS);
            const resetResponse = await this.driver.sendCommand(constants_1.ZiGateCommandCode.Reset, {}, 5000);
            if (resetResponse.code === constants_1.ZiGateMessageCode.RestartNonFactoryNew) {
                startResult = "resumed";
            }
            else if (resetResponse.code === constants_1.ZiGateMessageCode.RestartFactoryNew) {
                startResult = "reset";
            }
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.RawMode, { enabled: 0x01 });
            // @todo check
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.SetDeviceType, {
                deviceType: constants_1.DeviceType.Coordinator,
            });
            await this.initNetwork();
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.AddGroup, {
                addressMode: constants_1.AddressMode.Short,
                shortAddress: ZSpec.COORDINATOR_ADDRESS,
                sourceEndpoint: ZSpec.HA_ENDPOINT,
                destinationEndpoint: ZSpec.HA_ENDPOINT,
                groupAddress: default_bind_group,
            });
            if (this.adapterOptions.transmitPower != null) {
                await this.driver.sendCommand(constants_1.ZiGateCommandCode.SetTXpower, { value: this.adapterOptions.transmitPower });
            }
        }
        catch (error) {
            throw new Error(`failed to connect to zigate adapter ${error.message}`);
        }
        return startResult; // 'resumed' | 'reset' | 'restored'
    }
    async stop() {
        this.closing = true;
        await this.driver.close();
    }
    async getCoordinatorIEEE() {
        const networkResponse = await this.driver.sendCommand(constants_1.ZiGateCommandCode.GetNetworkState);
        return networkResponse.payload.extendedAddress;
    }
    async getCoordinatorVersion() {
        const result = await this.driver.sendCommand(constants_1.ZiGateCommandCode.GetVersion, {});
        const meta = {
            transportrev: 0,
            product: 0,
            majorrel: Number.parseInt(result.payload.major, 10).toString(16),
            minorrel: Number.parseInt(result.payload.minor, 10).toString(16),
            maintrel: Number.parseInt(result.payload.revision, 10).toString(16),
            revision: Number.parseInt(result.payload.revision, 10).toString(16),
        };
        return {
            type: "zigate",
            meta: meta,
        };
    }
    async permitJoin(seconds, networkAddress) {
        const clusterId = Zdo.ClusterId.PERMIT_JOINING_REQUEST;
        if (networkAddress !== undefined) {
            // specific device that is not `Coordinator`
            // `authentication`: TC significance always 1 (zb specs)
            const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, clusterId, seconds, 1, []);
            const result = await this.sendZdo(ZSpec.BLANK_EUI64, networkAddress, clusterId, zdoPayload, false);
            if (!Zdo.Buffalo.checkStatus(result)) {
                // TODO: will disappear once moved upstream
                throw new Zdo.StatusError(result[0]);
            }
        }
        else {
            // broadcast permit joining ZDO
            // `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);
        }
        this.joinPermitted = seconds !== 0;
    }
    async addInstallCode(_ieeeAddress, _key, _hashed) {
        await Promise.reject(new Error("Add install code is not supported"));
    }
    async reset(type) {
        if (type === "soft") {
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.Reset, {}, 5000);
        }
        else if (type === "hard") {
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.ErasePersistentData, {}, 5000);
        }
    }
    async getNetworkParameters() {
        try {
            const result = await this.driver.sendCommand(constants_1.ZiGateCommandCode.GetNetworkState, {}, 10000);
            return {
                panID: result.payload.PANID,
                extendedPanID: result.payload.ExtPANID, // read as IEEEADDR, so `0x${string}`
                channel: result.payload.Channel,
                nwkUpdateID: 0,
            };
        }
        catch (error) {
            throw new Error(`Get network parameters failed ${error}`);
        }
    }
    /**
     * https://zigate.fr/documentation/deplacer-le-pdm-de-la-zigate/
     * pdm from host
     */
    async supportsBackup() {
        return await Promise.resolve(false);
    }
    async backup() {
        return await Promise.reject(new Error("This adapter does not support backup"));
    }
    async sendZdo(ieeeAddress, networkAddress, clusterId, payload, disableResponse) {
        return await this.queue.execute(async () => {
            // stack-specific requirements
            // https://zigate.fr/documentation/commandes-zigate/
            switch (clusterId) {
                case Zdo.ClusterId.LEAVE_REQUEST: {
                    // extra zero for `removeChildren`
                    const prefixedPayload = Buffer.alloc(payload.length + 1);
                    prefixedPayload.set(payload, 0);
                    payload = prefixedPayload;
                    break;
                }
                case Zdo.ClusterId.BIND_REQUEST:
                case Zdo.ClusterId.UNBIND_REQUEST: {
                    // only need adjusting when Zdo.MULTICAST_BINDING
                    if (payload.length === 14) {
                        // extra zero for `endpoint`
                        const prefixedPayload = Buffer.alloc(payload.length + 1);
                        prefixedPayload.set(payload, 0);
                        payload = prefixedPayload;
                    }
                    break;
                }
                case Zdo.ClusterId.PERMIT_JOINING_REQUEST:
                case Zdo.ClusterId.SYSTEM_SERVER_DISCOVERY_REQUEST:
                case Zdo.ClusterId.LQI_TABLE_REQUEST:
                case Zdo.ClusterId.ROUTING_TABLE_REQUEST:
                case Zdo.ClusterId.BINDING_TABLE_REQUEST:
                case Zdo.ClusterId.NWK_UPDATE_REQUEST: {
                    const prefixedPayload = Buffer.alloc(payload.length + 2);
                    prefixedPayload.writeUInt16BE(networkAddress, 0);
                    prefixedPayload.set(payload, 2);
                    payload = prefixedPayload;
                    break;
                }
            }
            let waiter;
            if (!disableResponse) {
                const responseClusterId = Zdo.Utils.getResponseClusterId(clusterId);
                if (responseClusterId) {
                    waiter = this.driver.zdoWaitFor({
                        clusterId: responseClusterId,
                        target: responseClusterId === Zdo.ClusterId.NETWORK_ADDRESS_RESPONSE || responseClusterId === Zdo.ClusterId.LEAVE_RESPONSE
                            ? ieeeAddress
                            : networkAddress,
                    });
                }
            }
            await this.driver.requestZdo(clusterId, payload);
            if (waiter) {
                const result = await waiter.start().promise;
                return result.zdo;
            }
        }, networkAddress);
    }
    async sendZclFrameToEndpoint(ieeeAddr, networkAddress, endpoint, zclFrame, timeout, disableResponse, disableRecovery, sourceEndpoint, profileId) {
        return await this.queue.execute(async () => {
            return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint || 1, zclFrame, timeout, disableResponse, disableRecovery, 0, 0, false, false, profileId);
        }, networkAddress);
    }
    async sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt, dataRequestAttempt, checkedNetworkAddress, discoveredRoute, profileId) {
        logger_1.logger.debug(`sendZclFrameToEndpointInternal ${ieeeAddr}:${networkAddress}/${endpoint} (${responseAttempt},${dataRequestAttempt},${this.queue.count()})`, NS);
        let response = null;
        const data = zclFrame.toBuffer();
        const command = zclFrame.command;
        const payload = {
            addressMode: constants_1.AddressMode.Short, //nwk
            targetShortAddress: networkAddress,
            sourceEndpoint: sourceEndpoint || ZSpec.HA_ENDPOINT,
            destinationEndpoint: endpoint,
            profileID: profileId ?? ZSpec.HA_PROFILE_ID,
            clusterID: zclFrame.cluster.ID,
            securityMode: 0x02,
            radius: 30,
            dataLength: data.length,
            data: data,
        };
        if (command.response !== undefined && disableResponse === false) {
            response = this.waitFor(networkAddress, endpoint, zclFrame.header.frameControl.frameType, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, command.response, timeout);
        }
        else if (!zclFrame.header.frameControl.disableDefaultResponse) {
            response = this.waitFor(networkAddress, endpoint, Zcl.FrameType.GLOBAL, Zcl.Direction.SERVER_TO_CLIENT, zclFrame.header.transactionSequenceNumber, zclFrame.cluster.ID, Zcl.Foundation.defaultRsp.ID, timeout);
        }
        try {
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.RawAPSDataRequest, payload, undefined, {}, disableResponse);
        }
        catch {
            if (responseAttempt < 1 && !disableRecovery) {
                // @todo discover route
                return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt + 1, dataRequestAttempt, checkedNetworkAddress, discoveredRoute);
            }
        }
        // @TODO add dataConfirmResult
        // @TODO if error codes route / no_resourses wait and resend
        if (response !== null) {
            try {
                return await response.promise;
                // @todo discover route
            }
            catch (error) {
                logger_1.logger.error(`Response error ${error.message} (${ieeeAddr}:${networkAddress},${responseAttempt})`, NS);
                if (responseAttempt < 1 && !disableRecovery) {
                    return await this.sendZclFrameToEndpointInternal(ieeeAddr, networkAddress, endpoint, sourceEndpoint, zclFrame, timeout, disableResponse, disableRecovery, responseAttempt + 1, dataRequestAttempt, checkedNetworkAddress, discoveredRoute);
                }
                throw error;
            }
        }
    }
    async sendZclFrameToAll(endpoint, zclFrame, sourceEndpoint, destination, profileId) {
        return await this.queue.execute(async () => {
            if (sourceEndpoint !== 0x01 /*&& sourceEndpoint !== 242*/) {
                // @todo on zigate firmware without gp causes hang
                logger_1.logger.error(`source endpoint ${sourceEndpoint}, not supported`, NS);
                return;
            }
            const data = zclFrame.toBuffer();
            const payload = {
                addressMode: constants_1.AddressMode.Short, //nwk
                targetShortAddress: destination,
                sourceEndpoint: sourceEndpoint,
                destinationEndpoint: endpoint,
                profileID: profileId ?? ZSpec.HA_PROFILE_ID,
                clusterID: zclFrame.cluster.ID,
                securityMode: 0x02,
                radius: 30,
                dataLength: data.length,
                data: data,
            };
            logger_1.logger.debug(() => `sendZclFrameToAll ${JSON.stringify(payload)}`, NS);
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.RawAPSDataRequest, payload, undefined, {}, true);
            await (0, utils_1.wait)(200);
        });
    }
    async sendZclFrameToGroup(groupID, zclFrame, sourceEndpoint, profileId) {
        return await this.queue.execute(async () => {
            const data = zclFrame.toBuffer();
            const payload = {
                addressMode: constants_1.AddressMode.Group, //nwk
                targetShortAddress: groupID,
                sourceEndpoint: sourceEndpoint || ZSpec.HA_ENDPOINT,
                destinationEndpoint: 0xff,
                profileID: profileId ?? ZSpec.HA_PROFILE_ID,
                clusterID: zclFrame.cluster.ID,
                securityMode: 0x02,
                radius: 30,
                dataLength: data.length,
                data: data,
            };
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.RawAPSDataRequest, payload, undefined, {}, true);
            await (0, utils_1.wait)(200);
        });
    }
    /**
     * Supplementary functions
     */
    async initNetwork() {
        logger_1.logger.debug(`Set channel mask ${this.networkOptions.channelList} key`, NS);
        await this.driver.sendCommand(constants_1.ZiGateCommandCode.SetChannelMask, {
            channelMask: ZSpec.Utils.channelsToUInt32Mask(this.networkOptions.channelList),
        });
        logger_1.logger.debug("Set security key", NS);
        await this.driver.sendCommand(constants_1.ZiGateCommandCode.SetSecurityStateKey, {
            keyType: this.networkOptions.networkKeyDistribute
                ? constants_1.ZPSNwkKeyState.ZPS_ZDO_DISTRIBUTED_LINK_KEY
                : constants_1.ZPSNwkKeyState.ZPS_ZDO_PRECONFIGURED_LINK_KEY,
            key: this.networkOptions.networkKey,
        });
        try {
            // The block is wrapped in trapping because if the network is already created, the firmware does not accept the new key.
            logger_1.logger.debug(`Set EPanID ${
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            this.networkOptions.extendedPanID.toString()}`, NS);
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.SetExtendedPANID, {
                panId: this.networkOptions.extendedPanID,
            });
            await this.driver.sendCommand(constants_1.ZiGateCommandCode.StartNetwork, {});
        }
        catch (error) {
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            logger_1.logger.error(error.stack, NS);
        }
    }
    waitFor(networkAddress, endpoint, _frameType, _direction, transactionSequenceNumber, clusterId, commandId, timeout) {
        const payload = { address: networkAddress, endpoint, clusterId, commandId, transactionSequenceNumber };
        const waiter = this.waitress.waitFor(payload, timeout);
        const cancel = () => this.waitress.remove(waiter.ID);
        return { promise: waiter.start().promise, cancel };
    }
    /**
     * InterPAN !!! not implemented
     */
    async setChannelInterPAN(_channel) {
        await Promise.reject(new Error("Not supported"));
    }
    async sendZclFrameInterPANToIeeeAddr(_zclFrame, _ieeeAddress) {
        await Promise.reject(new Error("Not supported"));
    }
    async sendZclFrameInterPANBroadcast(_zclFrame, _timeout, _disableResponse) {
        return await Promise.reject(new Error("Not supported"));
    }
    async restoreChannelInterPAN() {
        await Promise.reject(new Error("Not supported"));
    }
    deviceAnnounceListener(response) {
        // @todo debounce
        if (this.joinPermitted === true) {
            this.emit("deviceJoined", { networkAddress: response.nwkAddress, ieeeAddr: response.eui64 });
        }
        else {
            // convert to `zdoResponse` to avoid needing extra event upstream
            this.emit("zdoResponse", Zdo.ClusterId.END_DEVICE_ANNOUNCE, [Zdo.Status.SUCCESS, response]);
        }
    }
    onZdoResponse(clusterId, response) {
        this.emit("zdoResponse", clusterId, response);
    }
    dataListener(ziGateObject) {
        const payload = {
            address: ziGateObject.payload.sourceAddress,
            clusterID: ziGateObject.payload.clusterID,
            data: ziGateObject.payload.payload,
            header: Zcl.Header.fromBuffer(ziGateObject.payload.payload),
            endpoint: ziGateObject.payload.sourceEndpoint,
            // biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
            linkquality: ziGateObject.frame.readRSSI(), // read: frame valid
            groupID: 0, // @todo
            wasBroadcast: false, // TODO
            destinationEndpoint: ziGateObject.payload.destinationEndpoint,
        };
        if (payload.header !== undefined) {
            this.waitress.resolve(payload);
        }
        this.emit("zclPayload", payload);
    }
    leaveIndicationListener(ziGateObject) {
        logger_1.logger.debug(() => `LeaveIndication ${JSON.stringify(ziGateObject)}`, NS);
        const payload = {
            networkAddress: ziGateObject.payload.extendedAddress,
            ieeeAddr: ziGateObject.payload.extendedAddress,
        };
        this.emit("deviceLeave", payload);
    }
    onZiGateClose() {
        if (!this.closing) {
            this.emit("disconnected");
        }
    }
}
exports.ZiGateAdapter = ZiGateAdapter;
//# sourceMappingURL=zigateAdapter.js.map