"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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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.Controller = void 0;
const bind_decorator_1 = __importDefault(require("bind-decorator"));
const json_stable_stringify_without_jsonify_1 = __importDefault(require("json-stable-stringify-without-jsonify"));
const zigbee_herdsman_1 = require("zigbee-herdsman");
const zigbee_herdsman_converters_1 = require("zigbee-herdsman-converters");
const eventBus_1 = __importDefault(require("./eventBus"));
// Extensions
const availability_1 = __importDefault(require("./extension/availability"));
const bind_1 = __importDefault(require("./extension/bind"));
const bridge_1 = __importDefault(require("./extension/bridge"));
const configure_1 = __importDefault(require("./extension/configure"));
const externalConverters_1 = __importDefault(require("./extension/externalConverters"));
const externalExtensions_1 = __importDefault(require("./extension/externalExtensions"));
const groups_1 = __importDefault(require("./extension/groups"));
const health_1 = __importDefault(require("./extension/health"));
const networkMap_1 = __importDefault(require("./extension/networkMap"));
const onEvent_1 = __importDefault(require("./extension/onEvent"));
const otaUpdate_1 = __importDefault(require("./extension/otaUpdate"));
const publish_1 = __importDefault(require("./extension/publish"));
const receive_1 = __importDefault(require("./extension/receive"));
const mqtt_1 = __importDefault(require("./mqtt"));
const state_1 = __importDefault(require("./state"));
const logger_1 = __importDefault(require("./util/logger"));
const sd_notify_1 = require("./util/sd-notify");
const settings = __importStar(require("./util/settings"));
const utils_1 = __importDefault(require("./util/utils"));
const zigbee_1 = __importDefault(require("./zigbee"));
class Controller {
    /** Allows canceling in-progress startup sequence if necessary. Signal can be passed to long-running ops as necessary for finer control. */
    #startAbortController;
    eventBus;
    zigbee;
    state;
    mqtt;
    restartCallback;
    exitCallback;
    extensions;
    extensionArgs;
    sdNotify;
    constructor(restartCallback, exitCallback) {
        logger_1.default.init();
        (0, zigbee_herdsman_1.setLogger)(logger_1.default);
        (0, zigbee_herdsman_converters_1.setLogger)(logger_1.default);
        this.eventBus = new eventBus_1.default();
        this.zigbee = new zigbee_1.default(this.eventBus);
        this.mqtt = new mqtt_1.default(this.eventBus);
        this.state = new state_1.default(this.eventBus, this.zigbee);
        this.restartCallback = restartCallback;
        this.exitCallback = exitCallback;
        // Initialize extensions.
        this.extensionArgs = [
            this.zigbee,
            this.mqtt,
            this.state,
            this.publishEntityState,
            this.eventBus,
            this.enableDisableExtension,
            this.restartCallback,
            this.addExtension,
        ];
        this.extensions = new Set([
            new externalConverters_1.default(...this.extensionArgs),
            new onEvent_1.default(...this.extensionArgs),
            new bridge_1.default(...this.extensionArgs),
            new publish_1.default(...this.extensionArgs),
            new receive_1.default(...this.extensionArgs),
            new configure_1.default(...this.extensionArgs),
            new networkMap_1.default(...this.extensionArgs),
            new groups_1.default(...this.extensionArgs),
            new bind_1.default(...this.extensionArgs),
            new otaUpdate_1.default(...this.extensionArgs),
            new externalExtensions_1.default(...this.extensionArgs),
            new availability_1.default(...this.extensionArgs),
            new health_1.default(...this.extensionArgs),
        ]);
    }
    async start() {
        this.#startAbortController = new AbortController();
        const abortSignal = this.#startAbortController.signal;
        if (settings.get().frontend.enabled) {
            const { Frontend } = await import("./extension/frontend.js");
            this.extensions.add(new Frontend(...this.extensionArgs));
        }
        if (settings.get().homeassistant.enabled) {
            const { HomeAssistant } = await import("./extension/homeassistant.js");
            this.extensions.add(new HomeAssistant(...this.extensionArgs));
        }
        this.state.start();
        if (abortSignal.aborted) {
            logger_1.default.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
            return;
        }
        const info = await utils_1.default.getZigbee2MQTTVersion();
        logger_1.default.info(`Starting Zigbee2MQTT version ${info.version} (commit #${info.commitHash})`);
        // Start zigbee
        try {
            const started = await this.zigbee.start(abortSignal);
            if (started) {
                this.eventBus.onAdapterDisconnected(this, async () => {
                    logger_1.default.error("Adapter disconnected, stopping");
                    await this.stop(false, 2);
                });
            }
        }
        catch (error) {
            // skip if aborted by `stop`
            if (error !== "SIGINT" && error !== "SIGTERM" && error !== "STOPABORT") {
                logger_1.default.error("Failed to start zigbee-herdsman");
                logger_1.default.error("Check https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html for possible solutions");
                logger_1.default.error("Exiting...");
                // biome-ignore lint/style/noNonNullAssertion: always Error
                logger_1.default.error(error.stack);
                /* v8 ignore start */
                if (error.message.includes("USB adapter discovery error (No valid USB adapter found)")) {
                    logger_1.default.error("If this happens after updating to Zigbee2MQTT 2.0.0, see https://github.com/Koenkk/zigbee2mqtt/discussions/24364");
                }
                /* v8 ignore stop */
                return await this.exit(1);
            }
        }
        if (abortSignal.aborted) {
            logger_1.default.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
            return;
        }
        // Log zigbee clients on startup
        let deviceCount = 0;
        for (const device of this.zigbee.devicesIterator(utils_1.default.deviceNotCoordinator)) {
            // `definition` validated by `isSupported`
            const model = device.isSupported
                ? // biome-ignore lint/style/noNonNullAssertion: valid from `isSupported`
                    `${device.definition.model} - ${device.definition.vendor} ${device.definition.description}`
                : "Not supported";
            logger_1.default.info(`${device.name} (${device.ieeeAddr}): ${model} (${device.zh.type})`);
            deviceCount++;
        }
        logger_1.default.info(`Currently ${deviceCount} devices are joined.`);
        // MQTT
        try {
            await this.mqtt.connect();
        }
        catch (error) {
            logger_1.default.error(`MQTT failed to connect, exiting... (${error.message})`);
            await this.zigbee.stop();
            return await this.exit(1);
        }
        if (abortSignal.aborted) {
            logger_1.default.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
            return;
        }
        // copy current Set of extensions to ignore possible external extensions added while looping
        for (const extension of new Set(this.extensions)) {
            await this.startExtension(extension);
            if (abortSignal.aborted) {
                logger_1.default.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
                return;
            }
        }
        // Send all cached states.
        if (settings.get().advanced.cache_state_send_on_startup && settings.get().advanced.cache_state) {
            for (const entity of this.zigbee.devicesAndGroupsIterator()) {
                if (this.state.exists(entity)) {
                    await this.publishEntityState(entity, this.state.get(entity), "publishCached");
                    if (abortSignal.aborted) {
                        logger_1.default.info(`Zigbee2MQTT start aborted, reason=${abortSignal.reason}`);
                        return;
                    }
                }
            }
        }
        this.eventBus.onLastSeenChanged(this, (data) => {
            utils_1.default.publishLastSeen(data, settings.get(), false, this.publishEntityState).catch(() => { });
        });
        logger_1.default.info("Zigbee2MQTT started!");
        this.sdNotify = await (0, sd_notify_1.initSdNotify)();
        settings.setOnboarding(false);
        this.#startAbortController = undefined; // gc
    }
    async enableDisableExtension(enable, name) {
        if (enable) {
            let extension;
            switch (name) {
                case "Frontend": {
                    if (!settings.get().frontend.enabled) {
                        throw new Error("Tried to enable Frontend extension disabled in settings");
                    }
                    // this is not actually used, not tested either
                    /* v8 ignore start */
                    const { Frontend } = await import("./extension/frontend.js");
                    extension = new Frontend(...this.extensionArgs);
                    break;
                    /* v8 ignore stop */
                }
                case "HomeAssistant": {
                    if (!settings.get().homeassistant.enabled) {
                        throw new Error("Tried to enable HomeAssistant extension disabled in settings");
                    }
                    const { HomeAssistant } = await import("./extension/homeassistant.js");
                    extension = new HomeAssistant(...this.extensionArgs);
                    break;
                }
                default: {
                    throw new Error(`Extension ${name} does not exist (should be added with 'addExtension') or is built-in that cannot be enabled at runtime`);
                }
            }
            const existingExtension = this.getExtension(name);
            if (existingExtension) {
                await this.removeExtension(existingExtension);
            }
            await this.extensions.add(extension);
        }
        else {
            switch (name) {
                case "Frontend": {
                    if (settings.get().frontend.enabled) {
                        throw new Error("Tried to disable Frontend extension enabled in settings");
                    }
                    break;
                }
                case "HomeAssistant": {
                    if (settings.get().homeassistant.enabled) {
                        throw new Error("Tried to disable HomeAssistant extension enabled in settings");
                    }
                    break;
                }
                case "Availability":
                case "Bind":
                case "Bridge":
                case "Configure":
                case "ExternalConverters":
                case "ExternalExtensions":
                case "Groups":
                case "NetworkMap":
                case "OnEvent":
                case "OTAUpdate":
                case "Publish":
                case "Receive": {
                    throw new Error(`Built-in extension ${name} cannot be disabled at runtime`);
                }
            }
            const extension = this.getExtension(name);
            if (extension) {
                await this.removeExtension(extension);
            }
        }
    }
    getExtension(name) {
        for (const extension of this.extensions) {
            if (extension.constructor.name === name) {
                return extension;
            }
        }
    }
    async addExtension(extension) {
        for (const ext of this.extensions) {
            if (ext.constructor.name === extension.constructor.name) {
                throw new Error(`Extension with name ${ext.constructor.name} already present`);
            }
        }
        this.extensions.add(extension);
        await this.startExtension(extension);
    }
    async removeExtension(extension) {
        if (this.extensions.delete(extension)) {
            await this.stopExtension(extension);
        }
    }
    async startExtension(extension) {
        try {
            await extension.start();
        }
        catch (error) {
            logger_1.default.error(`Failed to start '${extension.constructor.name}' (${error.stack})`);
        }
    }
    async stopExtension(extension) {
        try {
            await extension.stop();
        }
        catch (error) {
            logger_1.default.error(`Failed to stop '${extension.constructor.name}' (${error.stack})`);
        }
    }
    async stop(restart = false, code = 0, signal = undefined) {
        logger_1.default.info(`Stopping Zigbee2MQTT (restart=${restart}, code=${code}, signal=${signal})`);
        this.#startAbortController?.abort(signal ?? "STOPABORT");
        this.sdNotify?.notifyStopping();
        let localCode = 0;
        for (const extension of this.extensions) {
            try {
                await extension.stop();
            }
            catch (error) {
                logger_1.default.error(`Failed to stop '${extension.constructor.name}' (${error.stack})`);
                localCode = 1;
            }
        }
        this.eventBus.removeListeners(this);
        // Wrap-up
        this.state.stop();
        await this.mqtt.disconnect();
        try {
            await this.zigbee.stop();
            logger_1.default.info("Stopped Zigbee2MQTT");
        }
        catch (error) {
            logger_1.default.error(`Failed to stop Zigbee2MQTT (${error.stack})`);
            localCode = 1;
        }
        this.sdNotify?.stop();
        return await this.exit(code !== 0 ? code : localCode, restart);
    }
    async exit(code, restart = false) {
        await logger_1.default.end();
        return await this.exitCallback(code, restart);
    }
    async publishEntityState(entity, payload, stateChangeReason) {
        let message = { ...payload };
        // Update state cache with new state.
        const newState = this.state.set(entity, payload, stateChangeReason);
        if (settings.get().advanced.cache_state) {
            // Add cached state to payload
            message = newState;
        }
        const options = {
            clientOptions: {
                retain: utils_1.default.getObjectProperty(entity.options, "retain", false),
                qos: utils_1.default.getObjectProperty(entity.options, "qos", 0),
            },
            meta: {
                isEntityState: true,
            },
        };
        const retention = utils_1.default.getObjectProperty(entity.options, "retention", false);
        if (retention !== false) {
            options.clientOptions.properties = { messageExpiryInterval: retention };
        }
        if (entity.isDevice() && settings.get().mqtt.include_device_information) {
            message.device = {
                friendlyName: entity.name,
                model: entity.definition?.model,
                ieeeAddr: entity.ieeeAddr,
                networkAddress: entity.zh.networkAddress,
                type: entity.zh.type,
                manufacturerID: entity.zh.manufacturerID,
                powerSource: entity.zh.powerSource,
                applicationVersion: entity.zh.applicationVersion,
                stackVersion: entity.zh.stackVersion,
                zclVersion: entity.zh.zclVersion,
                hardwareVersion: entity.zh.hardwareVersion,
                dateCode: entity.zh.dateCode,
                softwareBuildID: entity.zh.softwareBuildID,
                // Manufacturer name can contain \u0000, remove this.
                // https://github.com/home-assistant/core/issues/85691
                /* v8 ignore next */
                manufacturerName: entity.zh.manufacturerName?.split("\u0000")[0],
            };
        }
        // Add lastseen
        const lastSeen = settings.get().advanced.last_seen;
        if (entity.isDevice() && lastSeen !== "disable" && entity.zh.lastSeen) {
            message.last_seen = utils_1.default.formatDate(entity.zh.lastSeen, lastSeen);
        }
        // Add device linkquality.
        if (entity.isDevice() && entity.zh.linkquality !== undefined) {
            message.linkquality = entity.zh.linkquality;
        }
        for (const extension of this.extensions) {
            extension.adjustMessageBeforePublish?.(entity, message);
        }
        // Filter mqtt message attributes
        utils_1.default.filterProperties(entity.options.filtered_attributes, message);
        if (!utils_1.default.objectIsEmpty(message)) {
            const output = settings.get().advanced.output;
            if (output === "attribute_and_json" || output === "json") {
                await this.mqtt.publish(entity.name, (0, json_stable_stringify_without_jsonify_1.default)(message), options);
            }
            if (output === "attribute_and_json" || output === "attribute") {
                await this.iteratePayloadAttributeOutput(`${entity.name}/`, message, options);
            }
        }
        this.eventBus.emitPublishEntityState({ entity, message, stateChangeReason, payload });
    }
    async iteratePayloadAttributeOutput(topicRoot, payload, options) {
        for (const [key, value] of Object.entries(payload)) {
            let subPayload = value;
            let message = null;
            // Special cases
            if (key === "color" && utils_1.default.objectHasProperties(subPayload, ["r", "g", "b"])) {
                subPayload = [subPayload.r, subPayload.g, subPayload.b];
            }
            // Check Array first, since it is also an Object
            if (subPayload === null || subPayload === undefined) {
                message = "";
            }
            else if (Array.isArray(subPayload)) {
                message = subPayload.map((x) => `${x}`).join(",");
            }
            else if (typeof subPayload === "object") {
                await this.iteratePayloadAttributeOutput(`${topicRoot}${key}-`, subPayload, options);
            }
            else {
                message = typeof subPayload === "string" ? subPayload : (0, json_stable_stringify_without_jsonify_1.default)(subPayload);
            }
            if (message !== null) {
                await this.mqtt.publish(`${topicRoot}${key}`, message, options);
            }
        }
    }
}
exports.Controller = Controller;
__decorate([
    bind_decorator_1.default
], Controller.prototype, "enableDisableExtension", null);
__decorate([
    bind_decorator_1.default
], Controller.prototype, "addExtension", null);
__decorate([
    bind_decorator_1.default
], Controller.prototype, "publishEntityState", null);
//# sourceMappingURL=data:application/json;base64, 