import * as fs from 'node:fs'
import * as path from 'node:path'
import * as utils from './utils.ts'
import type { SetValueAPIOptions } from 'zwave-js'
import { AlarmSensorType } from 'zwave-js'
import type { ValueID } from '@zwave-js/core'
import { CommandClasses } from '@zwave-js/core'
import * as Constants from './Constants.ts'
import type { LogLevel } from './logger.ts'
import { module } from './logger.ts'
import type { ColorMode } from '../hass/configurations.ts'
import hassCfg from '../hass/configurations.ts'
import hassDevices from '../hass/devices.ts'
import { storeDir } from '../config/app.ts'
import type { IClientPublishOptions } from 'mqtt'
import MqttClient from './MqttClient.ts'
import type {
	AllowedApis,
	CallAPIResult,
	EventSource,
	HassDevice,
	ZUINode,
	ZUIValueId,
	ZUIValueIdState,
} from './ZwaveClient.ts'
import type ZwaveClient from './ZwaveClient.ts'
import Cron from 'croner'

import crypto from 'node:crypto'
import type { IMeterCCSpecific } from './Constants.ts'

const logger = 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,
} as const

const PAYLOAD_TYPE = {
	JSON_TIME_VALUE: 0,
	VALUEID: 1,
	RAW: 2,
} as const

const CUSTOM_DEVICES = storeDir + '/customDevices'
let allDevices = hassDevices // 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: Map<string, fs.FSWatcher> = new Map()
const watch = (filename: string, fn: () => void) => {
	try {
		watchers.set(
			filename,
			fs.watch(filename, (e: string) => {
				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
		.createHash('sha256')
		.update(JSON.stringify(devices))
		.digest('hex')
	if (lastCustomDevicesLoad === sha) {
		return
	}

	logger.info(`Loading custom devices from ${loaded}`)

	lastCustomDevicesLoad = sha

	allDevices = Object.assign({}, hassDevices, devices)
	logger.info(
		`Loaded ${
			Object.keys(devices).length
		} custom Hass devices configurations`,
	)
}

loadCustomDevices()
watch(customDevicesJsPath, loadCustomDevices)
watch(customDevicesJsonPath, loadCustomDevices)

export function closeWatchers() {
	for (const [, watcher] of watchers) {
		watcher.close()
	}
}

export const GatewayType = GATEWAY_TYPE
export type GatewayType = (typeof GATEWAY_TYPE)[keyof typeof GATEWAY_TYPE]

export const PayloadType = PAYLOAD_TYPE
export type PayloadType = (typeof PAYLOAD_TYPE)[keyof typeof PAYLOAD_TYPE]

export type GatewayValue = {
	device: string
	value: ZUIValueId
	topic?: string
	device_class?: string
	icon?: string
	postOperation?: string
	enablePoll?: boolean
	pollInterval?: number
	parseSend?: boolean
	sendFunction?: string
	parseReceive?: boolean
	receiveFunction?: string
	qos?: 0 | 1 | 2
	retain?: boolean
	ccConfigEnableDiscovery?: boolean
}

export type ScheduledJob = {
	name: string
	cron?: string
	enabled: boolean
	runOnInit: boolean
	code: string
}

export type GatewayConfig = {
	type: GatewayType
	payloadType?: PayloadType
	nodeNames?: boolean
	ignoreLoc?: boolean
	sendEvents?: boolean
	ignoreStatus?: boolean
	includeNodeInfo?: boolean
	publishNodeDetails?: boolean
	retainedDiscovery?: boolean
	entityTemplate?: string
	hassDiscovery?: boolean
	discoveryPrefix?: string
	useLocationAsSuggestedArea?: boolean
	logEnabled?: boolean
	logLevel?: LogLevel
	logToFile?: boolean
	values?: GatewayValue[]
	jobs?: ScheduledJob[]
	plugins?: string[]
	logFileName?: string
	manualDiscovery?: boolean
	authEnabled?: boolean
	versions?: {
		driver?: string
		app?: string
		server?: string
	}
	disableChangelog?: boolean
	notifyNewVersions?: boolean
}

interface ValueIdTopic {
	topic: string
	valueConf: GatewayValue
	targetTopic?: string
}

interface DeviceInfo {
	identifiers: string[]
	manufacturer: string
	model: string
	name: string
	sw_version: string
	suggested_area?: string
}

export default class Gateway {
	private config: GatewayConfig
	private _mqtt: MqttClient
	private _zwave: ZwaveClient
	private topicValues: { [key: string]: ZUIValueId }
	private discovered: { [key: string]: HassDevice }
	private topicLevels: number[]
	private _closed: boolean
	private jobs: Map<string, Cron> = new Map()

	public get mqtt() {
		return this._mqtt
	}

	public get zwave() {
		return this._zwave
	}

	public get closed() {
		return this._closed
	}

	private get mqttEnabled() {
		return this.mqtt && !this.mqtt.disabled
	}

	constructor(config: GatewayConfig, zwave: ZwaveClient, mqtt: MqttClient) {
		this.config = config || { type: 1 }
		// clients
		this._mqtt = mqtt
		this._zwave = zwave
	}

	async start(): Promise<void> {
		// 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: ScheduledJob) {
		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 Cron(
						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
	 */
	private async runJob(jobConfig: ScheduledJob) {
		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: any, valueId: ZUIValueId, valueConf: GatewayValue) {
		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 === CommandClasses['Binary Toggle Switch']
			) {
				payload = 1
			} else if (
				valueId.commandClass ===
				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 ===
							CommandClasses['Thermostat Fan Mode'] &&
						hassDevice.fan_mode_map
					) {
						payload = hassDevice.fan_mode_map[payload]
					} else if (
						valueId.commandClass ===
							CommandClasses['Thermostat Mode'] &&
						hassDevice.mode_map
					) {
						// for other command classes use the mode_map
						payload = hassDevice.mode_map[payload]
					}
				} else if (
					hassDevice.type === 'cover' &&
					valueId.property === 'targetValue'
				) {
					// ref issue https://github.com/zwave-js/zwave-js-ui/issues/3862
					if (
						payload ===
						(hassDevice.discovery_payload.payload_stop ?? 'STOP')
					) {
						this._zwave
							.writeValue(
								{
									...valueId,
									property: 'Up',
								},
								false,
							)
							.catch(() => {})
						return null
					}
				}
			}

			if (valueConf) {
				if (utils.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(/-/, '+')

					payload = utils.applyOperation(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(): Promise<void> {
		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: ZUINode): string {
		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: ZUINode,
		valueId: ZUIValueId,
		returnObject = false,
	): string | ValueIdTopic {
		const topic = []
		let valueConf: GatewayValue

		// check if this value is in configuration values array
		const values = this.config.values.filter(
			(v: GatewayValue) => v.device === node.deviceId,
		)
		if (values && values.length > 0) {
			const vID = this._getIdWithoutNode(valueId)
			valueConf = values.find((v: GatewayValue) => v.value.id === vID)
		}

		if (valueConf && valueConf.topic) {
			topic.push(node.name ? node.name : NODE_PREFIX + valueId.nodeId)
			topic.push(valueConf.topic)
		}

		let targetTopic: string

		if (returnObject && valueId.targetValue) {
			const targetValue = node.values[valueId.targetValue]
			if (targetValue) {
				targetTopic = this.valueTopic(
					node,
					targetValue,
					false,
				) as string
			}
		}

		// 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: number): void {
		const node = this._zwave.nodes.get(nodeID)
		if (node && !node.virtual) {
			// 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: number): void {
		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: HassDevice,
		nodeId: number,
		options: { deleteDevice?: boolean; forceUpdate?: boolean } = {},
	): void {
		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: number,
		hassDevice: HassDevice,
		deleteDevice = false,
	): void {
		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(): void {
		// 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: ZUINode, hassDevice: HassDevice): void {
		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: string | number

					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) as string,
						)
						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) as string,
					)
					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) as string,
						)
						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) as string,
						)
						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) as string,
						)

						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],
									) as string,
								)
							: 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: ZUINode): void {
		// 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: { type: string }) => 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 === CommandClasses['Thermostat Setpoint'] &&
					v.property === 'setpoint'
				) {
					setpoints.push(vId)
				} else if (
					v.commandClass === CommandClasses['Multilevel Sensor'] &&
					v.property === 'Air temperature'
				) {
					temperatures.push(vId)
				} else if (
					v.commandClass === CommandClasses['Thermostat Mode'] &&
					v.property === 'mode'
				) {
					modes.push(vId)
				} else if (
					v.commandClass ===
						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(hassCfg.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,
				) as number[]

				// 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,
				) as number[]
				// 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: ZUINode, vId: string): void {
		if (!this.mqttEnabled || !this.config.hassDiscovery) {
			logger.debug(
				'Enable MQTT gateway and hass discovery to use this function',
			)
			return
		}

		// Virtual nodes don't have deviceClass/endpoints needed for discovery
		if (node.virtual) 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) as ValueIdTopic

			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: HassDevice

			const cmdClass = valueId.commandClass

			const deviceClass =
				node.endpoints[valueId.endpoint]?.deviceClass ??
				node.deviceClass

			switch (cmdClass) {
				case CommandClasses['Binary Switch']:
				case CommandClasses['All Switch']:
				case CommandClasses['Binary Toggle Switch']:
					if (valueId.isCurrentValue) {
						cfg = utils.copy(hassCfg.switch)
					} else return
					break
				case CommandClasses['Barrier Operator']:
					if (valueId.isCurrentValue) {
						cfg = utils.copy(hassCfg.barrier_state)
						cfg.discovery_payload.position_topic = getTopic
					} else return
					break
				case CommandClasses['Multilevel Switch']:
				case CommandClasses['Multilevel Toggle Switch']:
					if (valueId.isCurrentValue) {
						const specificDeviceClass =
							Constants.specificDeviceClass(
								deviceClass.generic,
								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',
								'specific_type_motor_multiposition',
							].includes(specificDeviceClass) ||
							node.deviceId === '615-0-258' // Issue #3088
						) {
							cfg = utils.copy(hassCfg.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(hassCfg.light_dimmer)
							cfg.discovery_payload.supported_color_modes = [
								'brightness',
							] as ColorMode[]
							cfg.discovery_payload.brightness_state_topic =
								getTopic
							cfg.discovery_payload.brightness_command_topic =
								setTopic
						}
					} else return
					break
				case CommandClasses['Door Lock']:
					if (valueId.isCurrentValue) {
						// lock state
						cfg = utils.copy(hassCfg.lock)
					} else {
						return
					}
					break
				case 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(hassCfg.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 CommandClasses['Color Switch']:
					if (
						valueId.property === 'currentColor' &&
						valueId.propertyKey === undefined
					) {
						cfg = this._addRgbColorSwitch(node, valueId)
					} else return
					break
				case CommandClasses['Central Scene']:
				case CommandClasses['Scene Activation']:
					cfg = utils.copy(hassCfg.central_scene)

					// Combile unique Object id, by using all possible scenarios
					cfg.object_id = utils.joinProps(
						cfg.object_id,
						valueId.property,
						valueId.propertyKey,
					)
					if (valueId.value?.unit) {
						cfg.discovery_payload.value_template =
							"{{ value_json.value.value | default('') }}"
					}
					break
				case 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(hassCfg.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 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 += AlarmSensorType[valueId.propertyKey]
							? '_' + AlarmSensorType[valueId.propertyKey]
							: ''
					} else {
						return
					}
					break
				case CommandClasses.Basic:
				case CommandClasses.Notification:
					// only support basic events
					if (
						cmdClass === 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(hassCfg.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(hassCfg.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 CommandClasses['Multilevel Sensor']:
				case CommandClasses.Meter:
				case CommandClasses['Pulse Meter']:
				case CommandClasses.Time:
				case CommandClasses['Energy Production']:
				case 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 === 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 === 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 as IMeterCCSpecific,
							)

							sensor.objectId += '_' + valueId.property
						} else {
							return
						}
					} else if (cmdClass === CommandClasses['Pulse Meter']) {
						sensor = {
							sensor: 'pulse',
							objectId: 'meter',
							props: {},
						}
					} else if (cmdClass === CommandClasses.Time) {
						if (valueId.isCurrentValue) {
							sensor = {
								sensor: 'date',
								objectId: 'current',
								props: {
									device_class:
										Constants.deviceClass.sensor.TIMESTAMP,
								},
							}
						} else return
					} else if (
						cmdClass === 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 === 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(hassCfg.sensor_generic)
					}

					cfg.object_id = utils.joinProps(
						sensor.sensor,
						sensor.objectId,
					)

					let unit = null
					// https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/scales.json
					if (valueId.unit) {
						unit = valueId.unit
					} else if (valueId.value?.unit) {
						unit = valueId.value.unit
					}

					if (unit) {
						// Home Assistant requires time units to be abbreviated
						// https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L408
						if (unit === 'seconds') {
							unit = 's'
						} else if (unit === 'minutes') {
							unit = 'min'
						} else if (unit === 'hours') {
							unit = 'h'
						} else if (unit === 'kVar') {
							// HA reactive_power device class requires lowercase unit
							unit = 'kvar'
						} else if (unit === 'kVarh') {
							// Normalize reactive energy unit to lowercase
							unit = 'kvarh'
						}
						cfg.discovery_payload.unit_of_measurement = 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 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(hassCfg.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(hassCfg.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
					}

					// by default configuration CC discovery entities are disabled
					// them can be enabled by setting ccConfigEnableDiscovery to true
					// on gateway values configuration
					cfg.discovery_payload.enabled_by_default =
						!!valueConf?.ccConfigEnableDiscovery

					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: number): void {
		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) as string
				this.topicValues[topic] = valueId
			}
		}
	}

	/**
	 * Removes all retained messages of the specified node
	 */
	removeNodeRetained(nodeId: number): void {
		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]) as string,
			)

			for (const t of topics) {
				this._mqtt.publish(t, '', { retain: true })
			}
		}
	}

	/**
	 * Catch all Z-Wave events
	 */
	private _onEvent(
		emitter: EventSource,
		eventName: string,
		...args: any[]
	): void {
		const topic = `${MqttClient.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
	 */
	private _onNodeRemoved(node: Partial<ZUINode>): void {
		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
	 */
	private _onValueChanged(
		valueId: ZUIValueId,
		node: ZUINode,
		changed: boolean,
	): void {
		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) as ValueIdTopic

		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 (utils.isValidOperation(valueConf.postOperation)) {
				tmpVal = utils.applyOperation(
					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.commandClass === CommandClasses['Thermostat Mode'] &&
			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) as string,
					)
					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: Record<string, any>

		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: IClientPublishOptions = 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
	 */
	private _onNotification(
		node: ZUINode,
		valueId: ZUIValueId,
		data: Record<string, any>,
	): void {
		const topic = this.valueTopic(node, valueId) as string

		if (this.config.payloadType !== PAYLOAD_TYPE.RAW) {
			data = { time: Date.now(), value: data }
		}

		this._mqtt.publish(topic, data, { retain: false })
	}

	private _onNodeInited(node: ZUINode): void {
		// Virtual nodes (broadcast/multicast) don't need polling or HA discovery
		if (node.virtual) return

		// enable poll if required
		const values = this.config.values?.filter(
			(v: GatewayValue) => 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
	 *
	 */
	private _onNodeStatus(node: ZUINode): void {
		if (!this.mqttEnabled) {
			return
		}

		const nodeTopic = this.nodeTopic(node)

		if (!this.config.ignoreStatus) {
			let data: any

			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
	 *
	 */
	private _onNodeLastActive(node: ZUINode): void {
		if (!this.mqttEnabled) {
			return
		}

		const nodeTopic = this.nodeTopic(node)

		if (!this.config.ignoreStatus) {
			let data: any

			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
	 */
	private _onDriverStatus(ready: boolean): void {
		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
	 *
	 */
	private _onBrokerStatus(online: boolean): void {
		if (online) {
			this.rediscoverAll()
		}
	}

	/**
	 * Hass will/birth
	 *
	 */
	private _onHassStatus(online: boolean): void {
		logger.info(`Home Assistant is ${online ? 'ONLINE' : 'OFFLINE'}`)

		if (online) {
			this.rediscoverAll()
		}
	}

	/**
	 * Handle api requests reeceived from MQTT client
	 *
	 */
	private async _onApiRequest(
		topic: string,
		apiName: AllowedApis,
		payload: { args: Parameters<ZwaveClient[AllowedApis]> },
	): Promise<void> {
		if (this._zwave) {
			const args = payload.args || []

			let result: CallAPIResult<AllowedApis> & { origin?: any }

			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 received from Mqtt client
	 */
	private async _onBroadRequest(
		parts: string[],
		payload: ValueID & { value: unknown; options?: SetValueAPIOptions },
	): Promise<void> {
		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,
				)

				if (payload === null) {
					return
				}

				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
	 *
	 */
	private async _onWriteRequest(
		parts: string[],
		payload: any,
	): Promise<void> {
		const valueTopic = parts.join('/')
		const valueId = this.topicValues[valueTopic]

		if (valueId) {
			const value = this.parsePayload(payload, valueId, valueId.conf)

			if (value === null) {
				return
			}

			await this._zwave.writeValue(valueId, value, payload?.options)
		} else {
			logger.debug(`No writeable valueId found for ${valueTopic}`)
		}
	}

	private async _onMulticastRequest(
		payload: ZUIValueId & {
			nodes: number[]
			value: any
			options?: SetValueAPIOptions
		},
	): Promise<void> {
		const nodes = payload.nodes
		const valueId: 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 as ZUIValueId,
			value,
			payload.options,
		)
	}

	/**
	 * Evaluate the return value of a custom parse Function
	 *
	 */
	private _evalFunction(
		code: string,
		valueId: ZUIValueId,
		value: unknown,
		node: ZUINode,
	) {
		let result = null

		try {
			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
	 */
	private _getNodeName(node: ZUINode, ignoreLoc: boolean): string {
		return (
			(!ignoreLoc && node.loc ? node.loc + '-' : '') +
			(node.name ? node.name : NODE_PREFIX + node.id)
		)
	}

	/**
	 *  Return re-arranged based on critical CCs
	 */

	private _getPriorityCCFirst(values: {
		[key: string]: ZUIValueId
	}): string[] {
		const priorityCC = [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
	 */
	private _getIdWithoutNode(valueId: ZUIValueId): string {
		return valueId.id.replace(valueId.nodeId + '-', '')
	}

	/**
	 * Get the device Object to send in discovery payload
	 */
	private _deviceInfo(node: ZUINode, nodeName: string): DeviceInfo {
		const deviceInfo: DeviceInfo = {
			identifiers: [
				UID_DISCOVERY_PREFIX + this._zwave.homeHex + '_node' + node.id,
			],
			manufacturer: node.manufacturer || 'Z-Wave JS',
			model:
				node.productDescription && node.productLabel
					? node.productDescription + ' (' + node.productLabel + ')'
					: node.name || 'Virtual Node',
			name: nodeName,
			sw_version: node.firmwareVersion || utils.getVersion(),
		}

		const suggestedArea = node.loc?.trim()
		if (this.config.useLocationAsSuggestedArea && suggestedArea) {
			deviceInfo.suggested_area = suggestedArea
		}

		return deviceInfo
	}

	private setDiscoveryAvailability(
		node: ZUINode,
		payload: { [key: string]: any },
	) {
		// 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
	 */
	private _getDiscoveryTopic(
		hassDevice: HassDevice,
		nodeName: string,
	): string {
		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.
	 */
	private _getMappedValuesTemplate(
		valueMap: { [x: string]: any },
		defaultValue: string,
	): string {
		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
	 */
	private _getMappedValuesInverseTemplate(
		valueMap: { [x: string]: number },
		defaultValue: string,
	): string {
		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
	 */
	private _getMappedStateTemplate(
		states: ZUIValueIdState[],
		defaultValueKey: string | number,
	): string {
		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
	 */
	private _setBinaryPayloadFromSensor(
		cfg: HassDevice,
		valueId: ZUIValueId,
		offStateValue = 0,
	): HassDevice {
		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
	 */
	private _getBinarySensorConfig(
		devClass: string,
		reversePayload = false,
	): HassDevice {
		const cfg = utils.copy(hassCfg.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
	 */
	private _setDiscoveryValue(
		payload: any,
		prop: string,
		node: ZUINode,
	): void {
		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
	 */
	private _addRgbColorSwitch(
		node: ZUINode,
		currentColorValue: ZUIValueId,
	): HassDevice {
		const cfg = utils.copy(hassCfg.light_rgb_dimmer)

		const currentColorTopics = this.valueTopic(
			node,
			currentColorValue,
			true,
		) as ValueIdTopic

		const endpoint = currentColorValue.endpoint

		const supportedColors: ColorMode[] = []

		cfg.discovery_payload.supported_color_modes = supportedColors

		supportedColors.push('rgb')

		// 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: string
		let switchValue: string
		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: string
		let discoveredCommandTopic: string

		if (brightnessValue || switchValue) {
			const vID = brightnessValue || switchValue

			const valueIdState = node.values[vID]
			const topics = this.valueTopic(
				node,
				valueIdState,
				true,
			) as ValueIdTopic

			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) {
			supportedColors.push('brightness')
			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) {
			supportedColors.push('onoff')
			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) {
			supportedColors.push('white')
			// 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
	}

	private _getEntityName(
		node: ZUINode,
		valueId: ZUIValueId,
		cfg: HassDevice,
		entityTemplate: string,
		ignoreLoc: boolean,
	): string {
		entityTemplate = entityTemplate || '%ln_%o'
		// when getting the entity name of a device use node props
		let propertyKey: string = cfg.type
		let propertyName: string = cfg.type
		let property: string = cfg.type
		let label: string = 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)
	}
}
