"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 (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __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.PayloadType = exports.GatewayType = exports.closeWatchers = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const utils = __importStar(require("./utils"));
const zwave_js_1 = require("zwave-js");
const core_1 = require("@zwave-js/core");
const Constants = __importStar(require("./Constants"));
const logger_1 = require("./logger");
const configurations_1 = __importDefault(require("../hass/configurations"));
const devices_1 = __importDefault(require("../hass/devices"));
const app_1 = require("../config/app");
const MqttClient_1 = __importDefault(require("./MqttClient"));
const croner_1 = __importDefault(require("croner"));
const crypto_1 = __importDefault(require("crypto"));
const logger = (0, logger_1.module)('Gateway');
const NODE_PREFIX = 'nodeID_';
const UID_DISCOVERY_PREFIX = process.env.UID_DISCOVERY_PREFIX || 'zwavejs2mqtt_';
const GATEWAY_TYPE = {
    VALUEID: 0,
    NAMED: 1,
    MANUAL: 2,
};
const PAYLOAD_TYPE = {
    TIME_VALUE: 0,
    VALUEID: 1,
    RAW: 2,
};
const CUSTOM_DEVICES = app_1.storeDir + '/customDevices';
let allDevices = devices_1.default; // will contain customDevices + hassDevices
// watcher initiates a watch on a file. if this fails (e.g., because the file
// doesn't exist), instead watch the directory. If the directory watch
// triggers, cancel it and try to watch the file again. Meanwhile spam `fn()`
// on any change, trusting that it's idempotent.
const watchers = new Map();
const watch = (filename, fn) => {
    try {
        watchers.set(filename, fs.watch(filename, (e) => {
            fn();
            if (e === 'rename') {
                watchers.get(filename).close();
                watch(filename, fn);
            }
        }));
    }
    catch {
        watchers.set(filename, fs.watch(path.dirname(filename), (e, f) => {
            if (!f ||
                f === 'customDevices.js' ||
                f === 'customDevices.json') {
                watchers.get(filename).close();
                watch(filename, fn);
                fn();
            }
        }));
    }
};
const customDevicesJsPath = CUSTOM_DEVICES + '.js';
const customDevicesJsonPath = CUSTOM_DEVICES + '.json';
let lastCustomDevicesLoad = null;
// loadCustomDevices attempts to load a custom devices file, preferring `.js`
// but falling back to `.json` only if a `.js` file does not exist. It stores
// a sha of the loaded data, and will skip re-loading any time the data has
// not changed.
const loadCustomDevices = () => {
    let loaded = '';
    let devices = null;
    try {
        if (fs.existsSync(customDevicesJsPath)) {
            loaded = customDevicesJsPath;
            devices = require(CUSTOM_DEVICES);
        }
        else if (fs.existsSync(customDevicesJsonPath)) {
            loaded = customDevicesJsonPath;
            devices = JSON.parse(fs.readFileSync(loaded).toString());
        }
        else {
            return;
        }
    }
    catch (error) {
        logger.error(`Failed to load ${loaded}:`, error);
        return;
    }
    const sha = crypto_1.default
        .createHash('sha256')
        .update(JSON.stringify(devices))
        .digest('hex');
    if (lastCustomDevicesLoad === sha) {
        return;
    }
    logger.info(`Loading custom devices from ${loaded}`);
    lastCustomDevicesLoad = sha;
    allDevices = Object.assign({}, devices_1.default, devices);
    logger.info(`Loaded ${Object.keys(devices).length} custom Hass devices configurations`);
};
loadCustomDevices();
watch(customDevicesJsPath, loadCustomDevices);
watch(customDevicesJsonPath, loadCustomDevices);
function closeWatchers() {
    for (const [, watcher] of watchers) {
        watcher.close();
    }
}
exports.closeWatchers = closeWatchers;
var GatewayType;
(function (GatewayType) {
    GatewayType[GatewayType["VALUEID"] = 0] = "VALUEID";
    GatewayType[GatewayType["NAMED"] = 1] = "NAMED";
    GatewayType[GatewayType["MANUAL"] = 2] = "MANUAL";
})(GatewayType || (exports.GatewayType = GatewayType = {}));
var PayloadType;
(function (PayloadType) {
    PayloadType[PayloadType["JSON_TIME_VALUE"] = 0] = "JSON_TIME_VALUE";
    PayloadType[PayloadType["VALUEID"] = 1] = "VALUEID";
    PayloadType[PayloadType["RAW"] = 2] = "RAW";
})(PayloadType || (exports.PayloadType = PayloadType = {}));
class Gateway {
    config;
    _mqtt;
    _zwave;
    topicValues;
    discovered;
    topicLevels;
    _closed;
    jobs = new Map();
    get mqtt() {
        return this._mqtt;
    }
    get zwave() {
        return this._zwave;
    }
    get closed() {
        return this._closed;
    }
    get mqttEnabled() {
        return this.mqtt && !this.mqtt.disabled;
    }
    constructor(config, zwave, mqtt) {
        this.config = config || { type: 1 };
        // clients
        this._mqtt = mqtt;
        this._zwave = zwave;
    }
    async start() {
        // gateway configuration
        this.config.values = this.config.values || [];
        // Object where keys are topic and values can be both zwave valueId object
        // or a valueConf if the topic is a broadcast topic
        this.topicValues = {};
        this.discovered = {};
        this._closed = false;
        // topic levels for subscribes using wildecards
        this.topicLevels = [];
        if (this.mqttEnabled) {
            this._mqtt.on('writeRequest', this._onWriteRequest.bind(this));
            this._mqtt.on('broadcastRequest', this._onBroadRequest.bind(this));
            this._mqtt.on('multicastRequest', this._onMulticastRequest.bind(this));
            this._mqtt.on('apiCall', this._onApiRequest.bind(this));
            this._mqtt.on('hassStatus', this._onHassStatus.bind(this));
            this._mqtt.on('brokerStatus', this._onBrokerStatus.bind(this));
        }
        if (this._zwave) {
            // needed in order to apply gateway values configs like polling
            this._zwave.on('nodeInited', this._onNodeInited.bind(this));
            // needed to init scheduled jobs
            this._zwave.on('driverStatus', this._onDriverStatus.bind(this));
            if (this.mqttEnabled) {
                this._zwave.on('nodeStatus', this._onNodeStatus.bind(this));
                this._zwave.on('nodeLastActive', this._onNodeLastActive.bind(this));
                this._zwave.on('valueChanged', this._onValueChanged.bind(this));
                this._zwave.on('nodeRemoved', this._onNodeRemoved.bind(this));
                this._zwave.on('notification', this._onNotification.bind(this));
                if (this.config.sendEvents) {
                    this._zwave.on('event', this._onEvent.bind(this));
                }
            }
            // this is async but doesn't need to be awaited
            await this._zwave.connect();
        }
        else {
            logger.error('Z-Wave settings are not valid');
        }
    }
    /**
     * Schedule a job
     */
    scheduleJob(jobConfig) {
        if (jobConfig.enabled) {
            if (jobConfig.runOnInit) {
                this.runJob(jobConfig).catch((error) => {
                    logger.error(`Error while executing scheduled job "${jobConfig.name}": ${error.message}`);
                });
            }
            if (jobConfig.cron) {
                try {
                    const job = new croner_1.default(jobConfig.cron, this.runJob.bind(this, jobConfig));
                    if (job?.nextRun()) {
                        this.jobs.set(jobConfig.name, job);
                        logger.info(`Scheduled job "${jobConfig.name}" will run at ${job
                            .nextRun()
                            .toISOString()}`);
                    }
                }
                catch (error) {
                    logger.error(`Error while scheduling job "${jobConfig.name}": ${error.message}`);
                }
            }
        }
    }
    /**
     * Executes a scheduled job
     */
    async runJob(jobConfig) {
        logger.info(`Executing scheduled job "${jobConfig.name}"...`);
        try {
            await this.zwave.driverFunction(jobConfig.code);
        }
        catch (error) {
            logger.error(`Error executing scheduled job "${jobConfig.name}": ${error.message}`);
        }
        const job = this.jobs.get(jobConfig.name);
        if (job?.nextRun()) {
            logger.info(`Next scheduled job "${jobConfig.name}" will run at ${job
                .nextRun()
                .toISOString()}`);
        }
    }
    /**
     * Parse the value of the payload received from mqtt
     * based on the type of the payload and the gateway config
     */
    parsePayload(payload, valueId, valueConf) {
        try {
            payload =
                typeof payload === 'object' &&
                    utils.hasProperty(payload, 'value')
                    ? payload.value
                    : payload;
            // try to parse string to bools
            if (typeof payload === 'string' && isNaN(parseInt(payload))) {
                if (/\btrue\b|\bon\b|\block\b/gi.test(payload))
                    payload = true;
                else if (/\bfalse\b|\boff\b|\bunlock\b/gi.test(payload)) {
                    payload = false;
                }
            }
            // on/off becomes 100%/0%
            if (typeof payload === 'boolean' && valueId.type === 'number') {
                payload = payload ? 0xff : valueId.min;
            }
            // 1/0 becomes true/false
            if (typeof payload === 'number' && valueId.type === 'boolean') {
                payload = payload > 0;
            }
            if (valueId.commandClass === core_1.CommandClasses['Binary Toggle Switch']) {
                payload = 1;
            }
            else if (valueId.commandClass ===
                core_1.CommandClasses['Multilevel Toggle Switch']) {
                payload = valueId.value > 0 ? 0 : 0xff;
            }
            const hassDevice = this.discovered[valueId.id];
            // Hass payload parsing
            if (hassDevice) {
                // map modes coming from hass
                if (valueId.list && isNaN(parseInt(payload))) {
                    // for thermostat_fan_mode command class use the fan_mode_map
                    if (valueId.commandClass ===
                        core_1.CommandClasses['Thermostat Fan Mode'] &&
                        hassDevice.fan_mode_map) {
                        payload = hassDevice.fan_mode_map[payload];
                    }
                    else if (valueId.commandClass ===
                        core_1.CommandClasses['Thermostat Mode'] &&
                        hassDevice.mode_map) {
                        // for other command classes use the mode_map
                        payload = hassDevice.mode_map[payload];
                    }
                }
            }
            if (valueConf) {
                if (this._isValidOperation(valueConf.postOperation)) {
                    let op = valueConf.postOperation;
                    // revert operation to write
                    if (op.includes('/'))
                        op = op.replace(/\//, '*');
                    else if (op.includes('*'))
                        op = op.replace(/\*/g, '/');
                    else if (op.includes('+'))
                        op = op.replace(/\+/, '-');
                    else if (op.includes('-'))
                        op = op.replace(/-/, '+');
                    // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                    payload = eval(`${payload}${op}`);
                }
                if (valueConf.parseReceive) {
                    const node = this._zwave.nodes.get(valueId.nodeId);
                    const parsedVal = this._evalFunction(valueConf.receiveFunction, valueId, payload, node);
                    if (parsedVal != null) {
                        payload = parsedVal;
                    }
                }
            }
        }
        catch (error) {
            logger.error(`Error while parsing payload ${payload} for valueID ${valueId.id}`);
        }
        return payload;
    }
    /**
     * Method used to cancel all scheduled jobs
     */
    cancelJobs() {
        // cancel jobs
        for (const [, job] of this.jobs) {
            job.stop();
        }
        this.jobs.clear();
    }
    /**
     * Method used to close clients connection, use this before destroy
     */
    async close() {
        this._closed = true;
        logger.info('Closing Gateway...');
        if (this._zwave) {
            await this._zwave.close();
        }
        this.cancelJobs();
        // close mqtt client after zwave connection is closed
        if (this.mqttEnabled) {
            await this._mqtt.close();
        }
    }
    /**
     * Calculates the node topic based on gateway settings
     */
    nodeTopic(node) {
        const topic = [];
        if (node.loc && !this.config.ignoreLoc)
            topic.push(node.loc);
        switch (this.config.type) {
            case GATEWAY_TYPE.MANUAL:
            case GATEWAY_TYPE.NAMED:
                topic.push(node.name ? node.name : NODE_PREFIX + node.id);
                break;
            case GATEWAY_TYPE.VALUEID:
                if (!this.config.nodeNames) {
                    topic.push(node.id);
                }
                else {
                    topic.push(node.name ? node.name : NODE_PREFIX + node.id);
                }
                break;
            default:
                topic.push(NODE_PREFIX + node.id);
        }
        // clean topic parts
        for (let i = 0; i < topic.length; i++) {
            topic[i] = utils.sanitizeTopic(topic[i]);
        }
        return topic.join('/');
    }
    /**
     * Calculates the valueId topic based on gateway settings
     *
     */
    valueTopic(node, valueId, returnObject = false) {
        const topic = [];
        let valueConf;
        // check if this value is in configuration values array
        const values = this.config.values.filter((v) => v.device === node.deviceId);
        if (values && values.length > 0) {
            const vID = this._getIdWithoutNode(valueId);
            valueConf = values.find((v) => v.value.id === vID);
        }
        if (valueConf && valueConf.topic) {
            topic.push(node.name ? node.name : NODE_PREFIX + valueId.nodeId);
            topic.push(valueConf.topic);
        }
        let targetTopic;
        if (returnObject && valueId.targetValue) {
            const targetValue = node.values[valueId.targetValue];
            if (targetValue) {
                targetTopic = this.valueTopic(node, targetValue, false);
            }
        }
        // if is not in configuration values array get the topic
        // based on gateway type if manual type this will be skipped
        if (topic.length === 0) {
            switch (this.config.type) {
                case GATEWAY_TYPE.NAMED:
                    topic.push(node.name ? node.name : NODE_PREFIX + valueId.nodeId);
                    topic.push(Constants.commandClass(valueId.commandClass));
                    topic.push('endpoint_' + (valueId.endpoint || 0));
                    topic.push(utils.removeSlash(valueId.propertyName));
                    if (valueId.propertyKey !== undefined) {
                        topic.push(utils.removeSlash(valueId.propertyKey));
                    }
                    break;
                case GATEWAY_TYPE.VALUEID:
                    if (!this.config.nodeNames) {
                        topic.push(valueId.nodeId);
                    }
                    else {
                        topic.push(node.name
                            ? node.name
                            : NODE_PREFIX + valueId.nodeId);
                    }
                    topic.push(valueId.commandClass);
                    topic.push(valueId.endpoint || '0');
                    topic.push(utils.removeSlash(valueId.property));
                    if (valueId.propertyKey !== undefined) {
                        topic.push(utils.removeSlash(valueId.propertyKey));
                    }
                    break;
            }
        }
        // if there is a valid topic for this value publish it
        if (topic.length > 0) {
            // add location prefix
            if (node.loc && !this.config.ignoreLoc)
                topic.unshift(node.loc);
            // clean topic parts
            for (let i = 0; i < topic.length; i++) {
                topic[i] = utils.sanitizeTopic(topic[i]);
            }
            const toReturn = {
                topic: topic.join('/'),
                valueConf: valueConf,
                targetTopic: targetTopic,
            };
            return returnObject ? toReturn : toReturn.topic;
        }
        else {
            return null;
        }
    }
    /**
     * Rediscover all hass devices of this node
     */
    rediscoverNode(nodeID) {
        const node = this._zwave.nodes.get(nodeID);
        if (node) {
            // delete all discovered values
            this._onNodeRemoved(node);
            node.hassDevices = {};
            // rediscover all values
            const nodeDevices = allDevices[node.deviceId] || [];
            nodeDevices.forEach((device) => this.discoverDevice(node, device));
            // discover node values (that are not part of a device)
            // iterate prioritized first, then the remaining
            for (const id of this._getPriorityCCFirst(node.values)) {
                this.discoverValue(node, id);
            }
            this._zwave.emitNodeUpdate(node, {
                hassDevices: node.hassDevices,
            });
        }
    }
    /**
     * Disable the discovery of all devices of this node
     *
     */
    disableDiscovery(nodeId) {
        const node = this._zwave.nodes.get(nodeId);
        if (node && node.hassDevices) {
            for (const id in node.hassDevices) {
                node.hassDevices[id].ignoreDiscovery = true;
            }
            this._zwave.emitNodeUpdate(node, {
                hassDevices: node.hassDevices,
            });
        }
    }
    /**
     * Publish a discovery payload to discover a device in hass using mqtt auto discovery
     *
     */
    publishDiscovery(hassDevice, nodeId, options = {}) {
        try {
            if (!this.mqttEnabled || !this.config.hassDiscovery) {
                logger.debug('Enable MQTT gateway and hass discovery to use this function');
                return;
            }
            logger.log('debug', `${options.deleteDevice ? 'Removing' : 'Publishing'} discovery: %o`, hassDevice);
            this.setDiscovery(nodeId, hassDevice, options.deleteDevice);
            if (this.config.payloadType === PAYLOAD_TYPE.RAW) {
                const p = hassDevice.discovery_payload;
                const template = 'value' +
                    (utils.hasProperty(p, 'payload_on') &&
                        utils.hasProperty(p, 'payload_off')
                        ? " == 'true'"
                        : '');
                for (const k in p) {
                    if (typeof p[k] === 'string') {
                        p[k] = p[k].replace(/value_json\.value/g, template);
                    }
                }
            }
            const skipDiscovery = hassDevice.ignoreDiscovery ||
                (this.config.manualDiscovery && !options.forceUpdate);
            if (!skipDiscovery) {
                this._mqtt.publish(hassDevice.discoveryTopic, options.deleteDevice ? '' : hassDevice.discovery_payload, { qos: 0, retain: this.config.retainedDiscovery || false }, this.config.discoveryPrefix);
            }
            if (options.forceUpdate) {
                this._zwave.updateDevice(hassDevice, nodeId, options.deleteDevice);
            }
        }
        catch (error) {
            logger.log('error', `Error while publishing discovery for node ${nodeId}: ${error.message}. Hass device: %o`, hassDevice);
        }
    }
    /**
     * Set internal discovery reference of a valueId
     *
     */
    setDiscovery(nodeId, hassDevice, deleteDevice = false) {
        for (let k = 0; k < hassDevice.values.length; k++) {
            const vId = nodeId + '-' + hassDevice.values[k];
            if (deleteDevice && this.discovered[vId]) {
                delete this.discovered[vId];
            }
            else {
                this.discovered[vId] = hassDevice;
            }
        }
    }
    /**
     * Rediscover all nodes and their values/devices
     *
     */
    rediscoverAll() {
        // skip discovery if discovery not enabled
        if (!this.config.hassDiscovery)
            return;
        const nodes = this._zwave.nodes ?? [];
        for (const [nodeId, node] of nodes) {
            const devices = node.hassDevices || {};
            for (const id in devices) {
                const d = devices[id];
                if (d && d.discoveryTopic && d.discovery_payload) {
                    this.publishDiscovery(d, nodeId);
                }
            } // end foreach hassdevice
        }
    }
    /**
     * Discover an hass device (from customDevices.js|json)
     */
    discoverDevice(node, hassDevice) {
        if (!this.mqttEnabled || !this.config.hassDiscovery) {
            logger.info('Enable MQTT gateway and hass discovery to use this function');
            return;
        }
        const hassID = hassDevice
            ? hassDevice.type + '_' + hassDevice.object_id
            : null;
        try {
            if (hassID && !node.hassDevices[hassID]) {
                // discover the device
                let payload;
                // copy the configuration without edit the original object
                hassDevice = utils.copy(hassDevice);
                if (hassDevice.type === 'climate') {
                    payload = hassDevice.discovery_payload;
                    const mode = node.values[payload.mode_state_topic];
                    let setId;
                    if (mode !== undefined) {
                        setId =
                            hassDevice.setpoint_topic &&
                                hassDevice.setpoint_topic[mode.value]
                                ? hassDevice.setpoint_topic[mode.value]
                                : hassDevice.default_setpoint;
                        // only setup modes if a state topic was defined
                        payload.mode_state_template =
                            this._getMappedValuesInverseTemplate(hassDevice.mode_map, 'off');
                        payload.mode_state_topic = this._mqtt.getTopic(this.valueTopic(node, mode));
                        payload.mode_command_topic =
                            payload.mode_state_topic + '/set';
                    }
                    else {
                        setId = hassDevice.default_setpoint;
                    }
                    // set properties dynamically using their configuration values
                    this._setDiscoveryValue(payload, 'max_temp', node);
                    this._setDiscoveryValue(payload, 'min_temp', node);
                    const setpoint = node.values[setId];
                    payload.temperature_state_topic = this._mqtt.getTopic(this.valueTopic(node, setpoint));
                    payload.temperature_command_topic =
                        payload.temperature_state_topic + '/set';
                    const action = node.values[payload.action_topic];
                    if (action) {
                        payload.action_topic = this._mqtt.getTopic(this.valueTopic(node, action));
                        if (hassDevice.action_map) {
                            payload.action_template =
                                this._getMappedValuesTemplate(hassDevice.action_map, 'idle');
                        }
                    }
                    const fan = node.values[payload.fan_mode_state_topic];
                    if (fan !== undefined) {
                        payload.fan_mode_state_topic = this._mqtt.getTopic(this.valueTopic(node, fan));
                        payload.fan_mode_command_topic =
                            payload.fan_mode_state_topic + '/set';
                        if (hassDevice.fan_mode_map) {
                            payload.fan_mode_state_template =
                                this._getMappedValuesInverseTemplate(hassDevice.fan_mode_map, 'auto');
                        }
                    }
                    const currTemp = node.values[payload.current_temperature_topic];
                    if (currTemp !== undefined) {
                        payload.current_temperature_topic = this._mqtt.getTopic(this.valueTopic(node, currTemp));
                        if (currTemp.unit) {
                            payload.temperature_unit = currTemp.unit.includes('C')
                                ? 'C'
                                : 'F';
                        }
                        // hass will default the precision to 0.1 for Celsius and 1.0 for Fahrenheit.
                        // 1.0 is not granular enough as a default and there seems to be no harm in making it more precise.
                        if (!payload.precision)
                            payload.precision = 0.1;
                    }
                }
                else {
                    payload = hassDevice.discovery_payload;
                    const topics = {};
                    // populate topics object with valueId: valueTopic
                    for (let i = 0; i < hassDevice.values.length; i++) {
                        const v = hassDevice.values[i]; // the value id
                        topics[v] = node.values[v]
                            ? this._mqtt.getTopic(this.valueTopic(node, node.values[v]))
                            : null;
                    }
                    // set the correct command/state topics
                    for (const key in payload) {
                        if (key.indexOf('topic') >= 0 && topics[payload[key]]) {
                            payload[key] =
                                topics[payload[key]] +
                                    (key.indexOf('command') >= 0 ||
                                        key.indexOf('set_') >= 0
                                        ? '/set'
                                        : '');
                        }
                    }
                }
                if (payload) {
                    const nodeName = this._getNodeName(node, this.config.ignoreLoc);
                    // Set device information using node info
                    payload.device = this._deviceInfo(node, nodeName);
                    this.setDiscoveryAvailability(node, payload);
                    hassDevice.object_id = utils
                        .sanitizeTopic(hassDevice.object_id, true)
                        .toLocaleLowerCase();
                    // Set a friendly name for this component
                    payload.name = this._getEntityName(node, undefined, hassDevice, this.config.entityTemplate, this.config.ignoreLoc);
                    // set a unique id for the component
                    payload.unique_id =
                        UID_DISCOVERY_PREFIX +
                            this._zwave.homeHex +
                            '_Node' +
                            node.id +
                            '_' +
                            hassDevice.object_id;
                    const discoveryTopic = this._getDiscoveryTopic(hassDevice, nodeName);
                    hassDevice.discoveryTopic = discoveryTopic;
                    // This configuration is not stored in nodes.json
                    hassDevice.persistent = false;
                    hassDevice.ignoreDiscovery = !!hassDevice.ignoreDiscovery;
                    node.hassDevices[hassID] = hassDevice;
                    this.publishDiscovery(hassDevice, node.id);
                }
            }
        }
        catch (error) {
            logger.error(`Error while discovering device ${hassID} of node ${node.id}: ${error.message}`, error);
        }
    }
    /**
     * Discover climate devices
     *
     */
    discoverClimates(node) {
        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json#L177
        // check if device it's a thermostat
        if (!node.deviceClass || node.deviceClass.generic !== 0x08) {
            return;
        }
        try {
            const nodeDevices = allDevices[node.deviceId] || [];
            // skip if there is already a climate device
            if (nodeDevices.length > 0 &&
                nodeDevices.find((d) => d.type === 'climate')) {
                return;
            }
            // arrays of strings valueIds (without the node prefix)
            const setpoints = [];
            const temperatures = [];
            const modes = [];
            const actions = [];
            for (const vId in node.values) {
                const v = node.values[vId];
                if (v.commandClass === core_1.CommandClasses['Thermostat Setpoint'] &&
                    v.property === 'setpoint') {
                    setpoints.push(vId);
                }
                else if (v.commandClass === core_1.CommandClasses['Multilevel Sensor'] &&
                    v.property === 'Air temperature') {
                    temperatures.push(vId);
                }
                else if (v.commandClass === core_1.CommandClasses['Thermostat Mode'] &&
                    v.property === 'mode') {
                    modes.push(vId);
                }
                else if (v.commandClass ===
                    core_1.CommandClasses['Thermostat Operating State'] &&
                    v.property === 'state') {
                    actions.push(vId);
                }
            }
            // TODO: if the device supports multiple endpoints how could we identify the correct one to use?
            const temperatureId = temperatures[0];
            if (setpoints.length === 0) {
                logger.warn('Unable to discover climate device, there is no valid setpoint valueId');
                return;
            }
            // generic configuration
            const config = utils.copy(configurations_1.default.thermostat);
            // set empty config.values
            config.values = [];
            if (temperatureId) {
                config.discovery_payload.current_temperature_topic =
                    temperatureId;
                config.values.push(temperatureId);
            }
            else {
                delete config.discovery_payload.current_temperature_template;
                delete config.discovery_payload.current_temperature_topic;
            }
            // take the first as valid
            const modeId = modes[0];
            // some thermostats could support just one mode so haven't a thermostat mode CC
            if (modeId) {
                config.values.push(modeId);
                const mode = node.values[modeId];
                config.discovery_payload.mode_state_topic = modeId;
                config.discovery_payload.mode_command_topic = modeId + '/set';
                // [0, 1, 2 ... ] (['off', 'heat', 'cold', ...])
                const availableModes = mode.states.map((s) => s.value);
                // Hass accepted modes as per: https://www.home-assistant.io/integrations/climate.mqtt/#modes
                const allowedModes = [
                    'off',
                    'heat',
                    'cool',
                    'auto',
                    'dry',
                    'fan_only',
                ];
                // Z-Wave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54
                // up to 0x1F modes
                const hassModes = [
                    'off', // Off
                    'heat', // Heat
                    'cool', // Cool
                    'auto', // Auto
                    undefined, // Aux
                    undefined, // Resume (on)
                    'fan_only', // Fan
                    undefined, // Furnance
                    'dry', // Dry
                    undefined, // Moist
                    'auto', // Auto changeover
                    'heat', // Energy heat
                    'cool', // Energy cool
                    'off', // Away
                    undefined, // No Z-Wave mode 0x0e
                    'heat', // Full power
                    undefined, // Up to 0x1f (manufacturer specific)
                ];
                config.mode_map = {};
                config.setpoint_topic = {};
                // for all available modes update the modes map and setpoint topics
                for (const m of availableModes) {
                    if (hassModes[m] === undefined)
                        continue;
                    let hM = hassModes[m];
                    // it could happen that mode_map already have defined a mode for this value, in this case
                    // map that mode to the first one available in the allowed hass modes
                    let i = 1; // skip 'off'
                    while (config.discovery_payload.modes.includes(hM) &&
                        i < allowedModes.length) {
                        hM = allowedModes[i++];
                    }
                    config.mode_map[hM] = m;
                    config.discovery_payload.modes.push(hM);
                    if (m > 0) {
                        // find the mode setpoint, ignore off
                        const setId = setpoints.find((v) => v.endsWith('-' + m));
                        const setpoint = node.values[setId];
                        if (setpoint) {
                            config.values.push(setId);
                            config.setpoint_topic[m] = setId;
                        }
                        else {
                            // Use first one, if no specific SP found
                            config.values.push(setpoints[0]);
                            config.setpoint_topic[m] = setpoints[0];
                        }
                    }
                }
                // set the default setpoint to 'heat' or to the first setpoint available
                config.default_setpoint =
                    config.setpoint_topic[1] ||
                        config.setpoint_topic[Object.keys(config.setpoint_topic)[0]];
            }
            else {
                config.default_setpoint = setpoints[0];
                delete config.discovery_payload.modes;
                delete config.discovery_payload.mode_state_template;
            }
            if (actions.length > 0) {
                const actionId = actions[0];
                config.values.push(actionId);
                config.discovery_payload.action_topic = actionId;
                const action = node.values[actionId];
                // [0, 1, 2 ... ] list of value fields from objects in states list
                const availableActions = (action.states.map((state) => state.value));
                // Hass accepted actions as per https://www.home-assistant.io/integrations/climate.mqtt/#action_topic:
                // ['off', 'heating', 'cooling', 'drying', 'idle', 'fan']
                // Z-Wave actions/states: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatOperatingStateCC.ts#L43
                const hassActionMap = [
                    'idle',
                    'heating',
                    'cooling',
                    'fan',
                    'idle',
                    'idle',
                    'fan',
                    'heating',
                    'heating',
                    'cooling',
                    'heating',
                    'heating', // 3rd Stage Aux Heat
                ];
                config.action_map = {};
                // for all available actions update the actions map
                for (const availableAction of availableActions) {
                    const hassAction = hassActionMap[availableAction];
                    if (hassAction === undefined)
                        continue;
                    config.action_map[availableAction] = hassAction;
                }
            }
            // add the new climate config to the nodeDevices so it will be
            // discovered later when we call `discoverDevice`
            nodeDevices.push(config);
            logger.log('info', 'New climate device discovered: %o', config);
            allDevices[node.deviceId] = nodeDevices;
        }
        catch (error) {
            logger.error('Unable to discover climate device.', error);
        }
    }
    /**
     * Try to guess the best way to discover this valueId in Hass
     */
    discoverValue(node, vId) {
        if (!this.mqttEnabled || !this.config.hassDiscovery) {
            logger.debug('Enable MQTT gateway and hass discovery to use this function');
            return;
        }
        const valueId = node.values[vId];
        // if the node is not ready means we don't have all values added yet so we are not sure to discover this value properly
        if (!valueId || this.discovered[valueId.id] || !node.ready)
            return;
        try {
            const result = this.valueTopic(node, valueId, true);
            if (!result || !result.topic)
                return;
            const valueConf = result.valueConf;
            const getTopic = this._mqtt.getTopic(result.topic);
            const setTopic = result.targetTopic
                ? this._mqtt.getTopic(result.targetTopic, true)
                : null;
            const nodeName = this._getNodeName(node, this.config.ignoreLoc);
            let cfg;
            const cmdClass = valueId.commandClass;
            switch (cmdClass) {
                case core_1.CommandClasses['Binary Switch']:
                case core_1.CommandClasses['All Switch']:
                case core_1.CommandClasses['Binary Toggle Switch']:
                    if (valueId.isCurrentValue) {
                        cfg = utils.copy(configurations_1.default.switch);
                    }
                    else
                        return;
                    break;
                case core_1.CommandClasses['Barrier Operator']:
                    if (valueId.isCurrentValue) {
                        cfg = utils.copy(configurations_1.default.barrier_state);
                        cfg.discovery_payload.position_topic = getTopic;
                    }
                    else
                        return;
                    break;
                case core_1.CommandClasses['Multilevel Switch']:
                case core_1.CommandClasses['Multilevel Toggle Switch']:
                    if (valueId.isCurrentValue) {
                        const specificDeviceClass = Constants.specificDeviceClass(node.deviceClass.generic, node.deviceClass.specific);
                        // Use a cover_position configuration if ...
                        if ([
                            'specific_type_class_a_motor_control',
                            'specific_type_class_b_motor_control',
                            'specific_type_class_c_motor_control',
                            'specific_type_class_motor_multiposition',
                        ].includes(specificDeviceClass) ||
                            node.deviceId === '615-0-258' // Issue #3088
                        ) {
                            cfg = utils.copy(configurations_1.default.cover_position);
                            cfg.discovery_payload.command_topic = setTopic;
                            cfg.discovery_payload.position_topic = getTopic;
                            cfg.discovery_payload.set_position_topic =
                                cfg.discovery_payload.command_topic;
                            cfg.discovery_payload.position_template =
                                '{{ value_json.value | round(0) }}';
                            cfg.discovery_payload.position_open = 99;
                            cfg.discovery_payload.position_closed = 0;
                            cfg.discovery_payload.payload_open = 99;
                            cfg.discovery_payload.payload_close = 0;
                        }
                        else {
                            cfg = utils.copy(configurations_1.default.light_dimmer);
                            cfg.discovery_payload.brightness_state_topic =
                                getTopic;
                            cfg.discovery_payload.brightness_command_topic =
                                setTopic;
                        }
                    }
                    else
                        return;
                    break;
                case core_1.CommandClasses['Door Lock']:
                    if (valueId.isCurrentValue) {
                        // lock state
                        cfg = utils.copy(configurations_1.default.lock);
                    }
                    else {
                        return;
                    }
                    break;
                case core_1.CommandClasses['Sound Switch']:
                    // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts
                    if (valueId.property === 'volume') {
                        // volume control
                        cfg = utils.copy(configurations_1.default.volume_dimmer);
                        cfg.discovery_payload.brightness_state_topic = getTopic;
                        cfg.discovery_payload.command_topic = getTopic + '/set';
                        cfg.discovery_payload.brightness_command_topic =
                            cfg.discovery_payload.command_topic;
                    }
                    else {
                        return;
                    }
                    break;
                case core_1.CommandClasses['Color Switch']:
                    if (valueId.property === 'currentColor' &&
                        valueId.propertyKey === undefined) {
                        cfg = this._addRgbColorSwitch(node, valueId);
                    }
                    else
                        return;
                    break;
                case core_1.CommandClasses['Central Scene']:
                case core_1.CommandClasses['Scene Activation']:
                    cfg = utils.copy(configurations_1.default.central_scene);
                    // Combile unique Object id, by using all possible scenarios
                    cfg.object_id = utils.joinProps(cfg.object_id, valueId.property, valueId.propertyKey);
                    break;
                case core_1.CommandClasses['Binary Sensor']: {
                    // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts#L41
                    // change the sensorTypeName to use directly valueId.property, as the old way was returning a number
                    // add a comment which shows the old way of achieving this value. This change fixes the Binary Sensor
                    // discovery
                    let sensorTypeName = valueId.property.toString();
                    if (sensorTypeName) {
                        sensorTypeName = utils.sanitizeTopic(sensorTypeName.toLocaleLowerCase(), true);
                    }
                    // TODO: Implement all BinarySensorTypes
                    // Use default Binary sensor, and replace based on sensorTypeName
                    // till now only one type is using the reverse on/off values as states
                    switch (sensorTypeName) {
                        // normal
                        case 'presence':
                        case 'smoke':
                        case 'gas':
                            cfg = this._getBinarySensorConfig(sensorTypeName);
                            break;
                        // reverse
                        case 'lock':
                            cfg = this._getBinarySensorConfig(sensorTypeName, true);
                            break;
                        // moisture - normal
                        case 'contact':
                        case 'water':
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.MOISTURE);
                            break;
                        // safety - normal
                        case 'co':
                        case 'co2':
                        case 'tamper':
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.SAFETY);
                            break;
                        // problem - normal
                        case 'alarm':
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.PROBLEM);
                            break;
                        // connectivity - normal
                        case 'router':
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary
                                .CONNECTIVITY);
                            break;
                        // battery - normal
                        case 'battery_low':
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.BATTERY);
                            break;
                        default:
                            // in the end build the basic cfg if all fails
                            cfg = utils.copy(configurations_1.default.binary_sensor);
                    }
                    cfg.object_id = sensorTypeName;
                    if (valueConf) {
                        if (valueConf.device_class) {
                            cfg.discovery_payload.device_class =
                                valueConf.device_class;
                            cfg.object_id = valueConf.device_class;
                        }
                        // binary sensors doesn't support icons
                    }
                    break;
                }
                case core_1.CommandClasses['Alarm Sensor']:
                    // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts#L40
                    if (valueId.property === 'state') {
                        cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.PROBLEM);
                        cfg.object_id += zwave_js_1.AlarmSensorType[valueId.propertyKey]
                            ? '_' + zwave_js_1.AlarmSensorType[valueId.propertyKey]
                            : '';
                    }
                    else {
                        return;
                    }
                    break;
                case core_1.CommandClasses.Basic:
                case core_1.CommandClasses.Notification:
                    // only support basic events
                    if (cmdClass === core_1.CommandClasses.Basic &&
                        valueId.property !== 'event') {
                        return;
                    }
                    // Try to define Binary sensor
                    if (valueId.states?.length === 2) {
                        let off = 0; // set default off to 0.
                        let discoveredObjectId = valueId.propertyKey;
                        switch (valueId.propertyKeyName) {
                            case 'Access Control':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.LOCK);
                                off = 23; // Closed state
                                break;
                            case 'Cover status':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.OPENING);
                                break;
                            case 'Door state (simple)':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.DOOR);
                                off = 1; // Door closed on payload 1
                                break;
                            case 'Alarm status':
                            case 'Dust in device status':
                            case 'Load error status':
                            case 'Over-current status':
                            case 'Over-load status':
                            case 'Hardware status':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.PROBLEM);
                                break;
                            case 'Heat sensor status':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.HEAT);
                                break;
                            case 'Motion sensor status':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.MOTION);
                                break;
                            case 'Water Alarm':
                                cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary
                                    .MOISTURE);
                                break;
                            // sensor status has multiple Properties. therefore is good to work
                            // on property basis... user friendly
                            case 'Sensor status':
                                switch (valueId.propertyName) {
                                    case 'Smoke Alarm':
                                        cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary
                                            .SMOKE);
                                        break;
                                    case 'Water Alarm':
                                        cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary
                                            .MOISTURE);
                                        break;
                                    default:
                                }
                                discoveredObjectId = valueId.propertyName;
                                break;
                            default:
                        }
                        // cfg not there?
                        cfg = cfg || utils.copy(configurations_1.default.binary_sensor);
                        // correct payload from true/false to numeric values
                        this._setBinaryPayloadFromSensor(cfg, valueId, off);
                        // finally update object_id
                        cfg.object_id = discoveredObjectId.toString();
                    }
                    else if (valueId.states?.length > 2) {
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/NotificationCC.ts
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json
                        cfg = utils.copy(configurations_1.default.sensor_generic);
                        cfg.object_id = utils.joinProps('notification', valueId.property, valueId.propertyKey);
                        // TODO: Improve the icons for different propertyKeys!
                        switch (valueId.propertyKey) {
                            case 'Motion sensor status':
                                cfg.discovery_payload.icon = 'mdi:motion-sensor';
                                break;
                            default:
                                cfg.discovery_payload.icon = 'mdi:alarm-light';
                        }
                        cfg.discovery_payload.value_template =
                            this._getMappedStateTemplate(valueId.states, valueId.default);
                    }
                    else {
                        return;
                    }
                    break;
                case core_1.CommandClasses['Multilevel Sensor']:
                case core_1.CommandClasses.Meter:
                case core_1.CommandClasses['Pulse Meter']:
                case core_1.CommandClasses.Time:
                case core_1.CommandClasses['Energy Production']:
                case core_1.CommandClasses.Battery: {
                    let sensor = null;
                    // set it as been sensor (ex not Binary)
                    let isSensor = true;
                    // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts
                    if (cmdClass === core_1.CommandClasses['Multilevel Sensor']) {
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/sensorTypes.json
                        // In some cases Multilevel Sensors offer Reset option or DeltaTime sensors, but do not include ccSpecific
                        // information. With this change, we target only the sensors and not the additional Properties.
                        if (valueId.ccSpecific) {
                            sensor = Constants.sensorType(valueId.ccSpecific.sensorType);
                        }
                        else {
                            return;
                        }
                    }
                    else if (cmdClass === core_1.CommandClasses.Meter) {
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/MeterCC.ts
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/meters.json
                        // In some cases Metering devices offer Reset option or DeltaTime sensors, but do not include ccSpecific
                        // information. With this change, we target only the sensors and not the additional Properties.
                        if (valueId.ccSpecific) {
                            sensor = Constants.meterType(valueId.ccSpecific, this._zwave.driver.configManager);
                            sensor.objectId += '_' + valueId.property;
                        }
                        else {
                            return;
                        }
                    }
                    else if (cmdClass === core_1.CommandClasses['Pulse Meter']) {
                        sensor = {
                            sensor: 'pulse',
                            objectId: 'meter',
                            props: {},
                        };
                    }
                    else if (cmdClass === core_1.CommandClasses.Time) {
                        if (valueId.isCurrentValue) {
                            sensor = {
                                sensor: 'date',
                                objectId: 'current',
                                props: {
                                    device_class: Constants.deviceClass.sensor.TIMESTAMP,
                                },
                            };
                        }
                        else
                            return;
                    }
                    else if (cmdClass === core_1.CommandClasses['Energy Production']) {
                        // TODO: class not yet supported by zwavejs
                        logger.warn('Energy Production CC not supported so value cannot be discovered');
                        // sensor = Constants.productionType(valueId.property)
                        return;
                    }
                    else if (cmdClass === core_1.CommandClasses.Battery) {
                        // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/BatteryCC.ts#L258
                        if (valueId.property === 'level') {
                            sensor = {
                                sensor: 'battery',
                                objectId: 'level',
                                props: {
                                    device_class: Constants.deviceClass.sensor.BATTERY,
                                    unit_of_measurement: '%', // this is set if Driver doesn't offer unit of measurement
                                },
                            };
                        }
                        else if (valueId.property === 'isLow') {
                            sensor = {
                                sensor: 'battery',
                                objectId: 'isLow',
                                props: {
                                    device_class: Constants.deviceClass.sensor.BATTERY,
                                },
                            };
                            // use battery_low binary sensor
                            cfg = this._getBinarySensorConfig(Constants.deviceClass.sensor_binary.BATTERY);
                            // support the case a binary sensor is served under multilevel sensor CC
                            isSensor = false;
                        }
                        else
                            return;
                    }
                    // check if is a sensor
                    if (isSensor) {
                        cfg = utils.copy(configurations_1.default.sensor_generic);
                    }
                    cfg.object_id = utils.joinProps(sensor.sensor, sensor.objectId);
                    // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/scales.json
                    if (valueId.unit) {
                        cfg.discovery_payload.unit_of_measurement = valueId.unit;
                    }
                    Object.assign(cfg.discovery_payload, sensor.props || {});
                    // check if there is a custom value configuration for this valueID
                    if (valueConf) {
                        if (valueConf.device_class) {
                            cfg.discovery_payload.device_class =
                                valueConf.device_class;
                            cfg.object_id = valueConf.device_class;
                        }
                        if (valueConf.icon)
                            cfg.discovery_payload.icon = valueConf.icon;
                    }
                    break;
                }
                case core_1.CommandClasses.Configuration: {
                    if (!valueId.writeable ||
                        process.env.DISCOVERY_DISABLE_CC_CONFIGURATION ===
                            'true') {
                        return;
                    }
                    let type = valueId.type;
                    if (type === 'number' &&
                        valueId.min === 0 &&
                        valueId.max === 1) {
                        type = 'boolean';
                    }
                    switch (type) {
                        case 'boolean':
                            cfg = utils.copy(configurations_1.default.config_switch);
                            // Combine unique Object id, by using all possible scenarios
                            cfg.object_id = utils.joinProps(cfg.object_id, valueId.property, valueId.propertyKey);
                            break;
                        case 'number':
                            cfg = utils.copy(configurations_1.default.config_number);
                            // Combine unique Object id, by using all possible scenarios
                            cfg.object_id = utils.joinProps(cfg.object_id, valueId.property, valueId.propertyKey);
                            if (valueId.min !== 1) {
                                cfg.discovery_payload.min = valueId.min;
                            }
                            if (valueId.max !== 100) {
                                cfg.discovery_payload.max = valueId.max;
                            }
                            break;
                        default:
                            return;
                    }
                    break;
                }
                default:
                    return;
            }
            const payload = cfg.discovery_payload;
            if (!utils.hasProperty(payload, 'state_topic') ||
                payload.state_topic === true) {
                payload.state_topic = getTopic;
            }
            else if (payload.state_topic === false) {
                delete payload.state_topic;
            }
            if (payload.command_topic === true) {
                payload.command_topic = setTopic || getTopic + '/set';
            }
            this.setDiscoveryAvailability(node, payload);
            if (['binary_sensor', 'sensor', 'lock', 'climate', 'fan'].includes(cfg.type)) {
                payload.json_attributes_topic = payload.state_topic;
            }
            // Set device information using node info
            payload.device = this._deviceInfo(node, nodeName);
            // multi instance devices would have same object_id
            if (valueId.endpoint)
                cfg.object_id += '_' + valueId.endpoint;
            // remove chars that are not allowed in object ids
            cfg.object_id = utils
                .sanitizeTopic(cfg.object_id, true)
                .toLocaleLowerCase();
            // Check if another value already exists and add the index to object_id to make it unique
            if (node.hassDevices[cfg.type + '_' + cfg.object_id]) {
                cfg.object_id += '_' + valueId.endpoint;
            }
            // Set a friendly name for this component
            payload.name = this._getEntityName(node, valueId, cfg, this.config.entityTemplate, this.config.ignoreLoc);
            // Set a unique id for the component
            payload.unique_id =
                UID_DISCOVERY_PREFIX +
                    this._zwave.homeHex +
                    '_' +
                    utils.sanitizeTopic(valueId.id, true);
            const discoveryTopic = this._getDiscoveryTopic(cfg, nodeName);
            cfg.discoveryTopic = discoveryTopic;
            cfg.values = cfg.values || [];
            if (!cfg.values.includes(vId)) {
                cfg.values.push(vId);
            }
            if (valueId.targetValue) {
                cfg.values.push(valueId.targetValue);
            }
            // This configuration is not stored in nodes.json
            cfg.persistent = false;
            // skip discovery flag, default to false
            cfg.ignoreDiscovery = false;
            node.hassDevices[cfg.type + '_' + cfg.object_id] = cfg;
            this.publishDiscovery(cfg, node.id);
        }
        catch (error) {
            logger.error(`Error while discovering value ${valueId.id} of node ${node.id}: ${error.message}`, error);
        }
    }
    /**
     * Update all in memory node topics
     *
     */
    updateNodeTopics(nodeId) {
        const node = this._zwave.nodes.get(nodeId);
        if (node) {
            const topics = Object.keys(this.topicValues).filter((k) => this.topicValues[k].nodeId === node.id);
            for (const t of topics) {
                const valueId = this.topicValues[t];
                delete this.topicValues[t];
                const topic = this.valueTopic(node, valueId);
                this.topicValues[topic] = valueId;
            }
        }
    }
    /**
     * Removes all retained messages of the specified node
     */
    removeNodeRetained(nodeId) {
        if (!this.mqttEnabled) {
            logger.info('Enable MQTT gateway to use this function');
            return;
        }
        const node = this._zwave.nodes.get(nodeId);
        if (node) {
            const topics = Object.keys(node.values).map((v) => this.valueTopic(node, node.values[v]));
            for (const t of topics) {
                this._mqtt.publish(t, '', { retain: true });
            }
        }
    }
    /**
     * Catch all Z-Wave events
     */
    _onEvent(emitter, eventName, ...args) {
        const topic = `${MqttClient_1.default.EVENTS_PREFIX}/${this._mqtt.clientID}/${emitter}/${eventName.replace(/\s/g, '_')}`;
        this._mqtt.publish(topic, { data: args }, { qos: 1, retain: false });
    }
    /**
     * Z-Wave event triggered when a node is removed
     */
    _onNodeRemoved(node) {
        const prefix = node.id + '-';
        // delete discovered values
        for (const id in this.discovered) {
            if (id.startsWith(prefix)) {
                delete this.discovered[id];
            }
        }
        // clean topicValues
        for (const topic in this.topicValues) {
            if (this.topicValues[topic].nodeId === node.id) {
                delete this.topicValues[topic];
            }
        }
    }
    /**
     * Triggered when a value change is detected in Z-Wave Network
     */
    _onValueChanged(valueId, node, changed) {
        const isDiscovered = this.discovered[valueId.id];
        // check if this value isn't discovered yet (values added after node is ready)
        if (this.config.hassDiscovery && !isDiscovered) {
            this.discoverValue(node, this._getIdWithoutNode(valueId));
        }
        const result = this.valueTopic(node, valueId, true);
        if (!result) {
            if (this.config.type !== GATEWAY_TYPE.MANUAL) {
                // if config is manual it is normal that some values are not mapped
                logger.debug(`No topic found for value ${valueId.id}`);
            }
            return;
        }
        // if there is a valid topic for this value publish it
        const topic = result.topic;
        const valueConf = result.valueConf;
        // Parse valueId value and create the payload
        let tmpVal = valueId.value;
        if (valueConf) {
            if (this._isValidOperation(valueConf.postOperation)) {
                tmpVal = eval(valueId.value + valueConf.postOperation);
            }
            if (valueConf.parseSend) {
                const parsedVal = this._evalFunction(valueConf.sendFunction, valueId, tmpVal, node);
                if (parsedVal != null) {
                    tmpVal = parsedVal;
                }
            }
        }
        // Check if I need to update discovery topics of this device
        if (this.config.hassDiscovery &&
            changed &&
            valueId.list &&
            this.discovered[valueId.id]) {
            const hassDevice = this.discovered[valueId.id];
            const isOff = hassDevice.mode_map
                ? hassDevice.mode_map.off === valueId.value
                : false;
            if (hassDevice && hassDevice.setpoint_topic && !isOff) {
                const setId = hassDevice.setpoint_topic[valueId.value];
                if (setId && node.values[setId]) {
                    // check if the setpoint topic has changed
                    const setpoint = node.values[setId];
                    const setTopic = this._mqtt.getTopic(this.valueTopic(node, setpoint));
                    if (setTopic !==
                        hassDevice.discovery_payload.temperature_state_topic) {
                        hassDevice.discovery_payload.temperature_state_topic =
                            setTopic;
                        hassDevice.discovery_payload.temperature_command_topic =
                            setTopic + '/set';
                        this.publishDiscovery(hassDevice, node.id);
                    }
                }
            }
        }
        let data;
        switch (this.config.payloadType) {
            case PAYLOAD_TYPE.VALUEID:
                data = utils.copy(valueId);
                data.value = tmpVal;
                break;
            case PAYLOAD_TYPE.RAW:
                data = tmpVal;
                break;
            default:
                data = { time: valueId.lastUpdate, value: tmpVal };
        }
        if (this.config.includeNodeInfo && typeof data === 'object') {
            data.nodeName = node.name;
            data.nodeLocation = node.loc;
        }
        const shouldSubscribe = valueId.writeable || valueId.targetValue;
        // valueId is writeable or it has a target value, subscribe for updates
        if (shouldSubscribe && !this.topicValues[topic]) {
            const levels = topic.split('/').length;
            logger.debug(`Subscribing to updates of ${valueId.id}`);
            if (this.topicLevels.indexOf(levels) < 0) {
                this.topicLevels.push(levels);
                this._mqtt
                    .subscribe('+'.repeat(levels).split('').join('/'))
                    .catch(() => {
                    // ignore, handled by mqtt client
                });
            }
            // I need to add the conf to the valueId but I don't want to edit
            // original valueId object so I create a copy
            if (valueConf) {
                valueId = utils.copy(valueId);
                valueId.conf = valueConf;
            }
            // handle the case the conf is set on current value but not in target value
            if (valueId.targetValue && node.values[valueId.targetValue]) {
                const targetValueId = utils.copy(node.values[valueId.targetValue]);
                targetValueId.conf = valueConf;
                this.topicValues[topic] = targetValueId;
            }
            else {
                this.topicValues[topic] = valueId;
            }
        }
        let mqttOptions = valueId.stateless
            ? { retain: false }
            : null;
        if (valueConf) {
            mqttOptions = mqttOptions || {};
            if (valueConf.qos !== undefined) {
                mqttOptions.qos = valueConf.qos;
            }
            if (valueConf.retain !== undefined) {
                mqttOptions.retain = valueConf.retain;
            }
        }
        const isFromCache = !node.ready;
        // prevent to send cached values if them are stateless
        if (isFromCache && valueId.stateless) {
            logger.debug(`Skipping send of stateless value ${valueId.id}: it's from cache`);
        }
        else {
            this._mqtt.publish(topic, data, mqttOptions);
        }
    }
    /**
     * Triggered when a notification is received from a node in Z-Wave Client
     */
    _onNotification(node, valueId, data) {
        const topic = this.valueTopic(node, valueId);
        if (this.config.payloadType !== PAYLOAD_TYPE.RAW) {
            data = { time: Date.now(), value: data };
        }
        this._mqtt.publish(topic, data, { retain: false });
    }
    _onNodeInited(node) {
        // enable poll if required
        const values = this.config.values?.filter((v) => v.enablePoll && v.device === node.deviceId);
        for (let i = 0; i < values.length; i++) {
            // don't edit the original object, copy it
            const valueId = utils.copy(values[i].value);
            valueId.nodeId = node.id;
            valueId.id = node.id + '-' + valueId.id;
            try {
                this._zwave.setPollInterval(valueId, values[i].pollInterval);
            }
            catch (error) {
                logger.error(`Error while enabling poll interval: ${error.message}`);
            }
        }
        if (this.mqttEnabled && this.config.hassDiscovery) {
            for (const id in node.hassDevices) {
                if (node.hassDevices[id].persistent) {
                    this.publishDiscovery(node.hassDevices[id], node.id);
                }
            }
            // check if there are climates to discover
            this.discoverClimates(node);
            const nodeDevices = allDevices[node.deviceId] || [];
            nodeDevices.forEach((device) => this.discoverDevice(node, device));
            // discover node values (that are not part of a device)
            // iterate prioritized first, then the remaining
            for (const id of this._getPriorityCCFirst(node.values)) {
                this.discoverValue(node, id);
            }
        }
    }
    /**
     * When there is a node status update
     *
     */
    _onNodeStatus(node) {
        if (!this.mqttEnabled) {
            return;
        }
        const nodeTopic = this.nodeTopic(node);
        if (!this.config.ignoreStatus) {
            let data;
            if (this.config.payloadType === PAYLOAD_TYPE.RAW) {
                data = node.available;
            }
            else {
                data = {
                    time: Date.now(),
                    value: node.available,
                    status: node.status,
                    nodeId: node.id,
                };
            }
            this._mqtt.publish(nodeTopic + '/status', data);
        }
        // Publish Node Info on separate topic
        // remove bulky  data like hassDevices, Groups and values
        if (this.config.publishNodeDetails) {
            const nodeData = utils.copy(node);
            delete nodeData.groups;
            delete nodeData.hassDevices;
            delete nodeData.values;
            this._mqtt.publish(nodeTopic + '/nodeinfo', nodeData);
        }
    }
    /**
     * When a packet is received from a node to update it's last activity timestamp
     *
     */
    _onNodeLastActive(node) {
        if (!this.mqttEnabled) {
            return;
        }
        const nodeTopic = this.nodeTopic(node);
        if (!this.config.ignoreStatus) {
            let data;
            if (this.config.payloadType === PAYLOAD_TYPE.RAW) {
                data = node.lastActive;
            }
            else {
                data = {
                    time: Date.now(),
                    value: node.lastActive,
                };
            }
            this._mqtt.publish(nodeTopic + '/lastActive', data);
        }
    }
    /**
     * Driver status updates
     */
    _onDriverStatus(ready) {
        logger.info(`Driver is ${ready ? 'READY' : 'CLOSED'}`);
        this.cancelJobs();
        if (ready) {
            if (this.config.jobs?.length > 0) {
                for (const jobConfig of this.config.jobs) {
                    this.scheduleJob(jobConfig);
                }
            }
        }
        if (this.mqttEnabled) {
            this._mqtt.publish('driver/status', ready);
        }
    }
    /**
     * When mqtt client goes online/offline
     *
     */
    _onBrokerStatus(online) {
        if (online) {
            this.rediscoverAll();
        }
    }
    /**
     * Hass will/birth
     *
     */
    _onHassStatus(online) {
        logger.info(`Home Assistant is ${online ? 'ONLINE' : 'OFFLINE'}`);
        if (online) {
            this.rediscoverAll();
        }
    }
    /**
     * Handle api requests reeceived from MQTT client
     *
     */
    async _onApiRequest(topic, apiName, payload) {
        if (this._zwave) {
            const args = payload.args || [];
            let result;
            if (Array.isArray(args)) {
                result = await this._zwave.callApi(apiName, ...args);
                result.origin = payload;
            }
            else {
                result = {
                    success: false,
                    message: 'Args must be an array',
                    origin: payload,
                };
            }
            this._mqtt.publish(topic, result, { retain: false });
        }
        else {
            logger.error(`Requested Z-Wave api ${apiName} doesn't exist`);
        }
    }
    /**
     * Handle broadcast request reeived from Mqtt client
     */
    async _onBroadRequest(parts, payload) {
        if (parts.length > 0) {
            // multiple writes (back compatibility mode)
            const topic = parts.join('/');
            const values = Object.keys(this.topicValues).filter((t) => t.endsWith(topic));
            if (values.length > 0) {
                // all values are the same type just different node,parse the Payload by using the first one
                payload = this.parsePayload(payload, this.topicValues[values[0]], this.topicValues[values[0]].conf);
                for (let i = 0; i < values.length; i++) {
                    await this._zwave.writeValue(this.topicValues[values[i]], payload, payload?.options);
                }
            }
        }
        else {
            // try real zwave broadcast
            if (payload.value === undefined) {
                logger.error('No value found in broadcast request');
                return;
            }
            const error = utils.isValueId(payload);
            if (typeof error === 'string') {
                logger.error('Invalid valueId: ' + error);
                return;
            }
            await this._zwave.writeBroadcast(payload, payload.value, payload.options);
        }
    }
    /**
     * Handle write request received from Mqtt Client
     *
     */
    async _onWriteRequest(parts, payload) {
        const valueTopic = parts.join('/');
        const valueId = this.topicValues[valueTopic];
        if (valueId) {
            const value = this.parsePayload(payload, valueId, valueId.conf);
            await this._zwave.writeValue(valueId, value, payload?.options);
        }
        else {
            logger.debug(`No writeable valueId found for ${valueTopic}`);
        }
    }
    async _onMulticastRequest(payload) {
        const nodes = payload.nodes;
        const valueId = {
            commandClass: payload.commandClass,
            property: payload.property,
            propertyKey: payload.propertyKey,
            endpoint: payload.endpoint,
        };
        const value = payload.value;
        if (!nodes || nodes.length === 0) {
            logger.error('No nodes found in multicast request');
            return;
        }
        const error = utils.isValueId(valueId);
        if (typeof error === 'string') {
            logger.error('Invalid valueId: ' + error);
            return;
        }
        if (payload.value === undefined) {
            logger.error('No value found in multicast request');
            return;
        }
        await this._zwave.writeMulticast(nodes, valueId, value, payload.options);
    }
    /**
     * Checks if an operation is valid, it must exist and must contains
     * only numbers and operators
     */
    _isValidOperation(op) {
        return op && !/[^0-9.()\-+*/,]/g.test(op);
    }
    /**
     * Evaluate the return value of a custom parse Function
     *
     */
    _evalFunction(code, valueId, value, node) {
        let result = null;
        try {
            /* eslint-disable no-new-func */
            // eslint-disable-next-line @typescript-eslint/no-implied-eval
            const parseFunc = new Function('value', 'valueId', 'node', 'logger', code);
            result = parseFunc(value, valueId, node, logger);
        }
        catch (error) {
            logger.error(`Error eval function of value ${valueId.id} ${error.message}`);
        }
        return result;
    }
    /**
     * Get node name from node object
     */
    _getNodeName(node, ignoreLoc) {
        return ((!ignoreLoc && node.loc ? node.loc + '-' : '') +
            (node.name ? node.name : NODE_PREFIX + node.id));
    }
    /**
     *  Return re-arranged based on critical CCs
     */
    _getPriorityCCFirst(values) {
        const priorityCC = [core_1.CommandClasses['Color Switch']];
        const prioritizedValueIds = [];
        for (const id in values) {
            if (priorityCC.includes(values[id].commandClass)) {
                prioritizedValueIds.unshift(id);
            }
            else {
                prioritizedValueIds.push(id);
            }
        }
        return prioritizedValueIds;
    }
    /**
     * Returns the value id without the node prefix
     */
    _getIdWithoutNode(valueId) {
        return valueId.id.replace(valueId.nodeId + '-', '');
    }
    /**
     * Get the device Object to send in discovery payload
     */
    _deviceInfo(node, nodeName) {
        return {
            identifiers: [
                UID_DISCOVERY_PREFIX + this._zwave.homeHex + '_node' + node.id,
            ],
            manufacturer: node.manufacturer,
            model: node.productDescription + ' (' + node.productLabel + ')',
            name: nodeName,
            sw_version: node.firmwareVersion || utils.getVersion(),
        };
    }
    setDiscoveryAvailability(node, payload) {
        // Set availability config using node status topic, client status topic
        // (which is the LWT), and driver status topic
        payload.availability = [
            {
                payload_available: 'true',
                payload_not_available: 'false',
                topic: this.mqtt.getTopic(this.nodeTopic(node)) + '/status',
            },
            {
                topic: this.mqtt.getStatusTopic(),
                value_template: "{{'online' if value_json.value else 'offline'}}",
            },
            {
                payload_available: 'true',
                payload_not_available: 'false',
                topic: this.mqtt.getTopic('driver/status'),
            },
        ];
        if (this.config.payloadType !== PAYLOAD_TYPE.RAW) {
            payload.availability[0].value_template =
                "{{'true' if value_json.value else 'false'}}";
        }
        payload.availability_mode = 'all';
    }
    /**
     * Get the Hass discovery topic for the specific node and hassDevice
     */
    _getDiscoveryTopic(hassDevice, nodeName) {
        return `${hassDevice.type}/${utils.sanitizeTopic(nodeName, true)}/${hassDevice.object_id}/config`;
    }
    /**
     * Generate the template string to use for value templates.
     * Note that the keys need to be numeric.
     */
    _getMappedValuesTemplate(valueMap, defaultValue) {
        const map = [];
        // JSON.stringify converts props to strings and this breaks the template
        // Error: "0": "off", Working: 0: "off"
        for (const key in valueMap) {
            map.push(`${key}: "${valueMap[key]}"`);
        }
        return `{{ {${map.join(', ')}}[value_json.value] | default('${defaultValue}') }}`;
    }
    /**
     * Generate the template string to use for value templates
     * by inverting the value map
     */
    _getMappedValuesInverseTemplate(valueMap, defaultValue) {
        const map = [];
        // JSON.stringify converts props to strings and this breaks the template
        // Error: "0": "off" Working: 0: "off"
        for (const key in valueMap) {
            map.push(`${valueMap[key]}: "${key}"`);
        }
        return `{{ {${map.join(', ')}}[value_json.value] | default('${defaultValue}') }}`;
    }
    /**
     * Calculate the correct template string to use for templates with state
     * list based on gateway settings and mapped mode values
     */
    _getMappedStateTemplate(states, defaultValueKey) {
        const map = [];
        let defaultValue = 'value_json.value';
        for (const s of states) {
            map.push(`${typeof s.value === 'number' ? s.value : '"' + s.value + '"'}: "${s.text}"`);
            if (s.value === defaultValueKey) {
                defaultValue = `'${s.text}'`;
            }
        }
        return `{{ {${map.join(',')}}[value_json.value] | default(${defaultValue}) }}`;
    }
    /**
     * Generates payload for Binary use from a state object
     */
    _setBinaryPayloadFromSensor(cfg, valueId, offStateValue = 0) {
        const stateKeys = valueId.states.map((s) => s.value);
        // Set on/off state from keys
        if (stateKeys[0] === offStateValue) {
            cfg.discovery_payload.payload_off = stateKeys[0];
            cfg.discovery_payload.payload_on = stateKeys[1];
        }
        else {
            cfg.discovery_payload.payload_off = stateKeys[1];
            cfg.discovery_payload.payload_on = stateKeys[0];
        }
        return cfg;
    }
    /**
     * Create a binary sensor configuration with a specific device class
     */
    _getBinarySensorConfig(devClass, reversePayload = false) {
        const cfg = utils.copy(configurations_1.default.binary_sensor);
        cfg.discovery_payload.device_class = devClass;
        if (reversePayload) {
            cfg.discovery_payload.payload_on = false;
            cfg.discovery_payload.payload_off = true;
        }
        return cfg;
    }
    /**
     * Retrieves the value of a property from the node valueId
     */
    _setDiscoveryValue(payload, prop, node) {
        if (typeof payload[prop] === 'string') {
            const valueId = node.values[payload[prop]];
            if (valueId && valueId.value != null) {
                payload[prop] = valueId.value;
            }
        }
    }
    /**
     * Check if this node supports rgb and if so add it to discovery configuration
     */
    _addRgbColorSwitch(node, currentColorValue) {
        const cfg = utils.copy(configurations_1.default.light_rgb_dimmer);
        const currentColorTopics = this.valueTopic(node, currentColorValue, true);
        const endpoint = currentColorValue.endpoint;
        // current color values are automatically added later in discoverValue function
        cfg.values = [];
        cfg.discovery_payload.rgb_state_topic = this._mqtt.getTopic(currentColorTopics.topic);
        cfg.discovery_payload.rgb_command_topic = this._mqtt.getTopic(currentColorTopics.targetTopic, true);
        // The following part of code, checks if ML or Binary works. If one exists the other
        let brightnessValue;
        let switchValue;
        if (node.values[`38-${endpoint}-currentValue`]) {
            brightnessValue = `38-${endpoint}-currentValue`;
            // Next if is about Fibaro like RGBW which use the endpoint 1 as multilevel
        }
        else if (endpoint === 0 && node.values['38-1-currentValue']) {
            brightnessValue = '38-1-currentValue';
        }
        else if (node.values[`37-${endpoint}-currentValue`]) {
            switchValue = `37-${endpoint}-currentValue`;
        }
        /* Find the control switch of the device Brightness or Binary
     If multilevel is not there use binary
     Some devices use also endpoint + 1 as on/off/brightness... try to guess that too!
  */
        let discoveredStateTopic;
        let discoveredCommandTopic;
        if (brightnessValue || switchValue) {
            const vID = brightnessValue || switchValue;
            const valueIdState = node.values[vID];
            const topics = this.valueTopic(node, valueIdState, true);
            if (!topics) {
                throw Error(`Can't find topics for ${vID}`);
            }
            cfg.values.push(vID, valueIdState.targetValue);
            discoveredStateTopic = this._mqtt.getTopic(topics.topic);
            discoveredCommandTopic = this._mqtt.getTopic(topics.targetTopic, true);
        }
        if (brightnessValue) {
            cfg.discovery_payload.brightness_state_topic = discoveredStateTopic;
            cfg.discovery_payload.brightness_command_topic =
                discoveredCommandTopic;
            cfg.discovery_payload.state_topic = discoveredStateTopic;
            cfg.discovery_payload.command_topic = discoveredCommandTopic;
        }
        else if (switchValue) {
            cfg.discovery_payload.state_topic = discoveredStateTopic;
            cfg.discovery_payload.command_topic = discoveredCommandTopic;
            cfg.discovery_payload.state_value_template =
                '{{ value_template.json }}';
            cfg.discovery_payload.on_command_type = 'last';
        }
        const whiteValue = node.values[`51-${endpoint}-currentcolor-0`];
        // if whitevalue exists, use currentColor value to get/set white
        if (whiteValue && currentColorValue) {
            // still use currentColor but change the template
            cfg.discovery_payload.color_temp_state_topic =
                cfg.discovery_payload.rgb_state_topic;
            cfg.discovery_payload.color_temp_command_topic =
                cfg.discovery_payload.rgb_command_topic;
            cfg.discovery_payload.color_temp_command_template =
                "{{ {'warmWhite': ((value - 245)|round(0)), 'coldWhite': (255 - (value - 245))|round(0))}|to_json }}";
            cfg.discovery_payload.color_temp_value_template =
                "{{ '%03d%03d' | format((value_json.value.warmWhite || 0), (value_json.value.coldWhite || 0)) }}";
        }
        return cfg;
    }
    _getEntityName(node, valueId, cfg, entityTemplate, ignoreLoc) {
        entityTemplate = entityTemplate || '%ln_%o';
        // when getting the entity name of a device use node props
        let propertyKey = cfg.type;
        let propertyName = cfg.type;
        let property = cfg.type;
        let label = cfg.object_id;
        if (valueId) {
            property = valueId.property?.toString();
            propertyKey = valueId.propertyKey?.toString();
            propertyName = valueId.propertyName?.toString();
            label = valueId.label;
        }
        return entityTemplate
            .replace(/%nid/g, NODE_PREFIX + node.id)
            .replace(/%ln/g, this._getNodeName(node, ignoreLoc))
            .replace(/%loc/g, node.loc || '')
            .replace(/%pk/g, propertyKey)
            .replace(/%pn/g, propertyName)
            .replace(/%p/g, property)
            .replace(/%o/g, cfg.object_id)
            .replace(/%n/g, node.name || NODE_PREFIX + node.id)
            .replace(/%l/g, label);
    }
}
exports.default = Gateway;
