UNPKG

iobroker.lorawan

Version:

converts the desired lora gateway data to a ioBroker structure

1,077 lines (1,002 loc) 154 kB
const bridgeMqttClientClass = require('./bridgeMqttclient'); const schedule = require('node-schedule'); /* Also er published irgendwie nicht den Mode => und es kommt virtual_Mode nicht subcribed.... */ /** * this class handles the bridge to foreign system */ class bridgeClass { /** * @param adapter adapter data (eg. for logging) */ constructor(adapter) { this.adapter = adapter; this.InitDone = false; // Activates work /********************************************************************* * ************** Definition Assigns (externel Module) *************** * ******************************************************************/ this.bridgeMqttClient = new bridgeMqttClientClass(this.adapter, this.adapter.config); // Structure of actual vaulues in Bridge (till las start of Adapter) this.CheckedIds = {}; this.OldDiscoveredIds = {}; this.DiscoveredIds = {}; this.SubscribedTopics = {}; this.PublishedIds = {}; this.VitualIds = {}; this.Notifications = {}; this.BridgeDiscoveryPrefix = { HA: 'homeassistant/', SH: 'smarthome/', }; this.ForeignBridgeMembers = {}; this.MinTime = 100; // ms between publish and subscribe same value this.DiscoveryCronjob = {}; this.EndingSet = '/set'; this.EndingState = '/state'; this.EndingVirtualClimate = '.virtual_climate'; this.EndingVirtualHumidifier = '.virtual_humiditier'; this.EndingVirtualCover = '.virtual_cover'; this.EndingVirtualLock = '.virtual_lock'; this.EndingVirtualLight = '.virtual_light'; this.EndingVirtualMode = '.virtual_mode'; this.NotificationId = '.notification'; this.GeneralId = '.general'; this.OfflineId = '.offline'; this.OnlineId = '.online'; this.EndingNotification = '.notification'; this.ClimateEntityType = 'climate'; this.HumidifierEntityType = 'humidifier'; this.LightEntityType = 'light'; this.DeHumidifierEntityType = 'dehumidifier'; this.NotificationEntityType = 'device_automation'; this.CoverEntityType = 'cover'; this.LockEntityType = 'lock'; this.MaxValueCount = 5; this.Words = { notification: 'notification', general: 'general', offline: 'offline', online: 'online', }; this.Notificationlevel = { all: 'allNotifications', bridgeConnection: 'bridgeConnection', newDiscover: 'newDiscover', deviceState: 'deviceState', }; // Timeoutput always like german view this.Timeoutput = { Argument: 'de-DE', Format: { year: 'numeric', month: '2-digit', day: '2-digit', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }, }; this.discoveredDevices = {}; this.oldDiscoveredDevices = {}; // Unitmapping zur Zuweisung der passenden Unit, wenn diese falsch geschrieben ist this.unitMap = { '°C': { device_class: 'temperature' }, '°F': { device_class: 'temperature' }, K: { device_class: 'temperature' }, lx: { device_class: 'illuminance' }, V: { device_class: 'voltage' }, mV: { device_class: 'voltage' }, kV: { device_class: 'voltage' }, A: { device_class: 'current' }, mA: { device_class: 'current' }, W: { device_class: 'power' }, kW: { device_class: 'power' }, VA: { device_class: 'apparent_power' }, kVA: { device_class: 'apparent_power' }, var: { device_class: 'reactive_power' }, kvar: { device_class: 'reactive_power' }, Wh: { device_class: 'energy', state_class: 'total_increasing' }, kWh: { device_class: 'energy', state_class: 'total_increasing' }, MWh: { device_class: 'energy', state_class: 'total_increasing' }, 'm³': { device_class: 'gas', state_class: 'total_increasing' }, 'ft³': { device_class: 'gas', state_class: 'total_increasing' }, L: { device_class: 'water', state_class: 'total_increasing' }, mL: { device_class: 'volume' }, gal: { device_class: 'volume' }, 'L/min': { device_class: 'volumetric_flow_rate' }, 'L/s': { device_class: 'volumetric_flow_rate' }, 'm³/h': { device_class: 'volumetric_flow_rate' }, Pa: { device_class: 'pressure' }, hPa: { device_class: 'pressure' }, kPa: { device_class: 'pressure' }, mbar: { device_class: 'pressure' }, bar: { device_class: 'pressure' }, psi: { device_class: 'pressure' }, ppm: { device_class: 'carbon_dioxide' }, ppb: { device_class: 'volatile_organic_compounds_parts' }, 'µg/m³': { device_class: 'volatile_organic_compounds' }, 'W/m²': { device_class: 'irradiance' }, mm: { device_class: 'precipitation' }, 'mm/h': { device_class: 'precipitation_intensity' }, dB: { device_class: 'sound_pressure' }, dBm: { device_class: 'signal_strength' }, 'm/s': { device_class: 'speed' }, 'km/h': { device_class: 'speed' }, mph: { device_class: 'speed' }, kn: { device_class: 'speed' }, m: { device_class: 'distance' }, km: { device_class: 'distance' }, mi: { device_class: 'distance' }, ft: { device_class: 'distance' }, g: { device_class: 'weight' }, kg: { device_class: 'weight' }, 'bit/s': { device_class: 'data_rate' }, 'kbit/s': { device_class: 'data_rate' }, 'Mbit/s': { device_class: 'data_rate' }, 'Gbit/s': { device_class: 'data_rate' }, B: { device_class: 'data_size' }, kB: { device_class: 'data_size' }, MB: { device_class: 'data_size' }, GB: { device_class: 'data_size' }, Hz: { device_class: 'frequency' }, kHz: { device_class: 'frequency' }, MHz: { device_class: 'frequency' }, ms: { device_class: 'duration' }, s: { device_class: 'duration' }, min: { device_class: 'duration' }, h: { device_class: 'duration' }, }; } /********************************************************************* * ********************* Message vom der Bridge ********************** * ******************************************************************/ /** * @param topic topic of the foreign system message * @param message message of the foreign system */ async handleMessage(topic, message) { const activeFunction = 'bridge.js - handleMessage'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (this.SubscribedTopics[topic]) { // safe old values (10 last values) if (this.SubscribedTopics[topic].values) { if (!this.SubscribedTopics[topic].oldValues) { this.SubscribedTopics[topic].oldValues = []; } if (this.SubscribedTopics[topic].oldValues.length >= this.MaxValueCount) { this.SubscribedTopics[topic].oldValues.pop(); } this.SubscribedTopics[topic].oldValues.unshift( structuredClone(this.SubscribedTopics[topic].values), ); } if (!this.SubscribedTopics[topic].values) { this.SubscribedTopics[topic].values = {}; } this.SubscribedTopics[topic].values.val = message; this.SubscribedTopics[topic].values.ts = Date.now(); this.SubscribedTopics[topic].values.time = new Date(Date.now()).toLocaleString( this.Timeoutput.Argument, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error this.Timeoutput.Format, ); if (this.SubscribedTopics[topic].id.endsWith(this.EndingVirtualMode)) { this.adapter.log.debug( `The value ${message} is assigned to virtual id: ${this.SubscribedTopics[topic].id}`, ); this.VitualIds[this.SubscribedTopics[topic].id] = message; // Return the virtual mode await this.publishId(this.SubscribedTopics[topic].id, message, {}); return; } // Light if (this.SubscribedTopics[topic].light) { if (message.state) { message.state = message.state === 'ON' ? true : false; await this.adapter.setForeignStateAsync(this.SubscribedTopics[topic].LightIds.onOff, { val: message.state, c: 'from bridge', }); } if (message.brightness) { await this.adapter.setForeignStateAsync(this.SubscribedTopics[topic].LightIds.brightness, { val: message.brightness, c: 'from bridge', }); } if (message.color) { const color = this.rgbToHex(message.color); await this.adapter.setForeignStateAsync(this.SubscribedTopics[topic].LightIds.color, { val: color, c: 'from bridge', }); } if (message.effect) { const effect = this.SubscribedTopics[topic].effects[message.effect] ?? this.SubscribedTopics[topic].effects[0]; await this.adapter.setForeignStateAsync(this.SubscribedTopics[topic].LightIds.effects, { val: effect, c: 'from bridge', }); } return; } // Cover if (this.SubscribedTopics[topic].cover) { if (this.SubscribedTopics[topic].messageAssign) { if (this.SubscribedTopics[topic].messageAssign[message]) { message = this.SubscribedTopics[topic].messageAssign[message]; } else { this.adapter.log.warn( `Incomming Message: ${message} at topic: ${topic} can not be found in possible values.`, ); return; } } } // Lock if (this.SubscribedTopics[topic].lock) { if (this.SubscribedTopics[topic].messageAssign) { if (this.SubscribedTopics[topic].messageAssign[message]) { message = this.SubscribedTopics[topic].messageAssign[message]; } else { this.adapter.log.warn( `Incomming Message: ${message} at topic: ${topic} can not be found in possible values.`, ); return; } } } // Check for namespace and write own, oder foreign state if (this.SubscribedTopics[topic].id.startsWith(this.adapter.namespace)) { // Special DataExchange if (this.SubscribedTopics[topic].dataExchange) { if (typeof message === 'object') { message = JSON.stringify(message); } await this.adapter.setState( this.SubscribedTopics[topic].id, { val: message, c: 'from bridge' }, true, ); // All Adapter internal States } else { await this.adapter.setState(this.SubscribedTopics[topic].id, { val: message, c: 'from bridge', }); } // Foreign States } else { // Assignable Topics => id & val if (this.SubscribedTopics[topic].messageAssign) { await this.adapter.setForeignStateAsync(message.id, { val: message.val, c: 'from bridge' }); // Write in the desired id } else { await this.adapter.setForeignStateAsync(this.SubscribedTopics[topic].id, { val: message, c: 'from bridge', }); } } await this.adapter.setState( 'info.subscribedTopics', { val: JSON.stringify(this.SubscribedTopics), c: 'from bridge' }, true, ); } else { this.adapter.log.debug(`The received Topic ${topic} is not subscribed`); } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /** * @param {string} hex value of the color */ hexToRgb(hex) { const activeFunction = 'bridge.js - hexToRgb'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { hex = hex.replace('#', '').trim(); if (hex.length === 3) { // Kurzform #FFF → #FFFFFF hex = hex .split('') .map(c => c + c) .join(''); } return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16), }; } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); return { r: 255, g: 255, b: 255, }; } } /** * Converts an RGB color object to a HEX string. * * @param {object} colorObject - RGB color object * @param {number} colorObject.r - Red component (0–255) * @param {number} colorObject.g - Green component (0–255) * @param {number} colorObject.b - Blue component (0–255) * @returns {string} HEX color string (#RRGGBB) */ rgbToHex(colorObject) { const activeFunction = 'bridge.js - rgbToHex'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { const { r, g, b } = colorObject; /** * @param {number} c - Blue component (0–255) */ const toHex = c => { const hex = c.toString(16); return hex.length === 1 ? `0${hex}` : hex; }; return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); return `#FFFFFF`; } } /** * @param id Id of actual element, handled in the bridge * @param Stateval Value of the used Id * @param options Options for using spezial fuctions */ async work(id, Stateval, options) { const activeFunction = 'bridge.js - work'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (this.bridgeMqttClient.internalConnectionstate) { const discovered = await this.discovery(id, options); // only publish if no new id is discovered, because the newId will be published 1s later if (!discovered || !discovered.newId) { await this.publishId(id, Stateval, {}); } } else { this.adapter.log.debug(`work called with id ${id}, but Bridge is not connected yet.`); } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ********************* Discover zur Bridge ************************* * ******************************************************************/ /** * @param id Id, wich is to discover * @param options Options for using spezial fuctions */ async discovery(id, options) { const activeFunction = 'bridge.js - discovery'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (!this.CheckedIds[id] || (options && options.forceDiscovery)) { this.CheckedIds[id] = {}; this.adapter.log.debug(`discover the id ${id}`); return await this.buildDiscovery(id, options); } this.adapter.log.debug(`${id} allready checked for discovery`); return false; } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ******************** Discover Notification ************************ * ******************************************************************/ /** * Discover Notifications to Bridge */ async discoverGeneralNotification() { const activeFunction = 'discoverGeneralNotification'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { const notificationId = `${this.adapter.namespace}.${this.Words.notification}${this.GeneralId}`; if (!this.Notifications[notificationId]) { const discoveryobject = this.getNotificationDiscoveryObject(this.adapter.namespace, this.Words.general); this.Notifications[notificationId] = {}; this.assignIdStructure( this.PublishedIds, notificationId, { usedDeviceId: this.adapter.namespace, }, discoveryobject?.topic, discoveryobject?.payload, discoveryobject?.payload.topic, undefined, ); await this.publishDiscovery(notificationId, { topic: discoveryobject?.topic, payload: structuredClone(discoveryobject?.payload), informations: { usedDeviceId: this.adapter.namespace, }, }); } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * **************** Get Notification Discovery Object **************** * ******************************************************************/ /** * @param deviceIdentifier deviceidentifiere for the desired device * @param notificationType notificationtype for the discoveryobject */ getNotificationDiscoveryObject(deviceIdentifier, notificationType) { const activeFunction = 'getNotificationDiscoveryObject'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { const normalizedDeviceIdentifier = this.normalizeString(deviceIdentifier); const discoveryobject = { topic: `${this.BridgeDiscoveryPrefix[this.adapter.config.BridgeType]}${this.NotificationEntityType}/${normalizedDeviceIdentifier}/${this.Words.notification}_${notificationType}/config`.toLowerCase(), payload: { automation_type: 'trigger', topic: `${this.bridgeMqttClient.BridgePrefix}${normalizedDeviceIdentifier}/${this.Words.notification}_${notificationType}${this.EndingState}`.toLowerCase(), type: 'notification', subtype: notificationType, device: { identifiers: [normalizedDeviceIdentifier.toLowerCase()], name: deviceIdentifier }, }, }; return discoveryobject; } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ********************** Discover Climate *************************** * ******************************************************************/ /** * Discover Configed Climate Entities */ async discoverClimate() { const activeFunction = 'discoverClimate'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (this.adapter.config.ClimateConfig) { for (const config of this.adapter.config.ClimateConfig) { if (!(await this.generateClimateIds(config))) { continue; } // All Ids ok // Target const target = {}; target.changeInfo = await this.adapter.getChangeInfo(config.climateIds.target); target.DeviceIdentifier = this.getDeviceIdentifier( target.changeInfo, this.adapter.config.DeviceIdentifiers, ); target.normalizedDeficeIdentifier = this.normalizeString(target.DeviceIdentifier); target.uniqueString = await this.getUniqueString(config.climateIds.target, target.DeviceIdentifier); target.Topic = `${this.bridgeMqttClient.BridgePrefix}${target.uniqueString?.path}`.toLowerCase(); //Min und Max holen const targetObject = await this.adapter.getObjectAsync(config.climateIds.target); if (targetObject.common.min) { target.min = targetObject.common.min; } if (targetObject.common.max) { target.max = targetObject.common.max; } // Act const act = {}; act.changeInfo = await this.adapter.getChangeInfo(config.climateIds.act); act.DeviceIdentifier = this.getDeviceIdentifier( act.changeInfo, this.adapter.config.DeviceIdentifiers, ); act.normalizedDeficeIndetifier = this.normalizeString(act.DeviceIdentifier); act.uniqueString = await this.getUniqueString(config.climateIds.act, act.DeviceIdentifier); act.Topic = `${this.bridgeMqttClient.BridgePrefix}${act.uniqueString?.path}`.toLowerCase(); // Mode const mode = {}; mode.changeInfo = await this.adapter.getChangeInfo(config.climateIds.mode); mode.DeviceIdentifier = this.getDeviceIdentifier( mode.changeInfo, this.adapter.config.DeviceIdentifiers, ); mode.normalizedDeviceIdentifier = this.normalizeString(mode.DeviceIdentifier); mode.uniqueString = await this.getUniqueString(config.climateIds.mode, mode.DeviceIdentifier); mode.Topic = `${this.bridgeMqttClient.BridgePrefix}${mode.uniqueString?.path}`.toLowerCase(); const climateUniqueString = await this.getUniqueString( `${this.adapter.namespace}.${config.ClimateName}`, target.DeviceIdentifier, ); const DiscoveryTopic = `${this.BridgeDiscoveryPrefix[this.adapter.config.BridgeType]}${this.ClimateEntityType}/${climateUniqueString?.path}/config`.toLowerCase(); const indexLastDotTarget = config.climateIds.target.lastIndexOf('.'); const Id = config.climateIds.target.substring(0, indexLastDotTarget) + this.EndingVirtualClimate; const DiscoveryPayload = { name: config.ClimateName, unique_id: `${climateUniqueString?.flat}`.toLowerCase(), device: { identifiers: [target.normalizedDeficeIdentifier.toLowerCase()], name: target.DeviceIdentifier, }, mode_state_topic: `${mode.Topic}${this.EndingState}`, mode_command_topic: `${mode.Topic}${this.EndingSet}`, temperature_state_topic: `${target.Topic}${this.EndingState}`, temperature_command_topic: `${target.Topic}${this.EndingSet}`, current_temperature_topic: `${act.Topic}${this.EndingState}`, min_temp: target.min ? target.min : 0, max_temp: target.max ? target.max : 40, modes: ['auto', 'heat', 'off'], precision: 0.1, temp_step: 0.1, }; // Assign Subscribed Topics // Target this.assignTopicStructure( this.SubscribedTopics, `${target.Topic}${this.EndingSet}`, { applicationName: target.changeInfo.applicationName, usedApplicationName: target.changeInfo.usedApplicationName, deviceId: target.changeInfo.deviceId, usedDeviceId: target.changeInfo.usedDeviceId, }, DiscoveryTopic, DiscoveryPayload, config.climateIds.target, undefined, ); // Mode this.assignTopicStructure( this.SubscribedTopics, `${mode.Topic}${this.EndingSet}`, { applicationName: mode.changeInfo.applicationName, usedApplicationName: mode.changeInfo.usedApplicationName, deviceId: mode.changeInfo.deviceId, usedDeviceId: mode.changeInfo.usedDeviceId, }, DiscoveryTopic, DiscoveryPayload, config.climateIds.mode, undefined, ); // Assign published Topics // Target this.assignIdStructure( this.PublishedIds, config.climateIds.target, { applicationName: target.changeInfo.applicationName, usedApplicationName: target.changeInfo.usedApplicationName, deviceId: target.changeInfo.deviceId, usedDeviceId: target.changeInfo.usedDeviceId, }, DiscoveryTopic, DiscoveryPayload, `${target.Topic}${this.EndingState}`, undefined, ); // Act this.assignIdStructure( this.PublishedIds, config.climateIds.act, { applicationName: act.changeInfo.applicationName, usedApplicationName: act.changeInfo.usedApplicationName, deviceId: act.changeInfo.deviceId, usedDeviceId: act.changeInfo.usedDeviceId, }, DiscoveryTopic, DiscoveryPayload, `${act.Topic}${this.EndingState}`, undefined, ); // Mode this.assignIdStructure( this.PublishedIds, config.climateIds.mode, { applicationName: mode.changeInfo.applicationName, usedApplicationName: mode.changeInfo.usedApplicationName, deviceId: mode.changeInfo.deviceId, usedDeviceId: mode.changeInfo.usedDeviceId, }, DiscoveryTopic, DiscoveryPayload, `${mode.Topic}${this.EndingState}`, undefined, ); // State to publish for Mode let modeval = undefined; if (config.climateIds.mode.endsWith(this.EndingVirtualMode)) { modeval = 'auto'; } // Publishing the discover message await this.publishDiscovery(Id, { topic: DiscoveryTopic, payload: structuredClone(DiscoveryPayload), informations: { target: { applicationName: target.changeInfo.applicationName, usedApplicationName: target.changeInfo.usedApplicationName, deviceId: target.changeInfo.deviceId, usedDeviceId: target.changeInfo.usedDeviceId, }, act: { applicationName: act.changeInfo.applicationName, usedApplicationName: act.changeInfo.usedApplicationName, deviceId: act.changeInfo.deviceId, usedDeviceId: act.changeInfo.usedDeviceId, }, mode: { applicationName: mode.changeInfo.applicationName, usedApplicationName: mode.changeInfo.usedApplicationName, deviceId: mode.changeInfo.deviceId, usedDeviceId: mode.changeInfo.usedDeviceId, }, }, }); // Delay for publish new entity setTimeout(async () => { await this.publishId(config.climateIds.target, undefined, {}); await this.publishId(config.climateIds.act, undefined, {}); await this.publishId(config.climateIds.mode, modeval, {}); }, 1000); } } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ****************** generate Climate Ids *************************** * ******************************************************************/ /** * @param config Configuration of the climate entity, wich is to genereate */ async generateClimateIds(config) { const activeFunction = 'generateClimateIds'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { const climateIds = { target: '', act: '', mode: '' }; climateIds.target = `${this.adapter.namespace}.${config.TargetApplication}.devices.${config.TargetDevice}.${config.TargetFolder}.${config.TargetState}`; climateIds.act = `${this.adapter.namespace}.${config.ActApplication}.devices.${config.ActDevice}.${config.ActFolder}.${config.ActState}`; if (config.ModeApplication === 'NotPresent') { climateIds.mode = `${this.adapter.namespace}.${config.TargetApplication}.devices.${config.TargetDevice}.${config.TargetFolder}${this.EndingVirtualMode}`; } else { climateIds.mode = `${this.adapter.namespace}.${config.ModeApplication}.devices.${config.ModeDevice}.${config.ModeFolder}.${config.ModeState}`; } for (const id of Object.values(climateIds)) { if (!(await this.adapter.objectExists(id)) && !id.endsWith(this.EndingVirtualMode)) { return false; } } if (config.ClimateName === '') { return false; } const indexOfSpace = config.ClimateName.indexOf(' -- '); if (indexOfSpace > 0) { config.ClimateName = config.ClimateName.substring(0, indexOfSpace); } config.climateIds = climateIds; return true; } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ********************* Publish zur Bridge ************************** * ******************************************************************/ /** * @param id Id, for notification * @param message message for notification * @param level level, for notification */ async publishNotification(id, message, level) { const activeFunction = 'bridge.js - publishNotification'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (this.adapter.bridge.Notifications[id]) { if (this.adapter.config.BridgenotificationActivation.includes(level)) { await this.publishId(id, message, { retain: false }); } else { this.adapter.log.debug( `the level ${level} is not reached. Actual level: ${this.adapter.config.BridgenotificationActivation}`, ); } } else { this.adapter.log.debug(`the id ${id} is not set for Notifications`); } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /********************************************************************* * ********************* Publish zur Bridge ************************** * ******************************************************************/ /** * @param id Id, wich is to discover * @param val Value of the used Id * @param options options for special values */ async publishId(id, val, options) { const activeFunction = 'bridge.js - publishId'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { if (this.PublishedIds[id]) { if (val === undefined) { const State = await this.adapter.getForeignStateAsync(id); if (State) { val = State.val; } } // Iterate the state_topics for (const element in this.PublishedIds[id].publish) { const topic = element; const publish = this.PublishedIds[id].publish[element]; // Light if (publish.light) { val = {}; val.state = (await this.adapter.getForeignStateAsync(publish.LightIds.onOff)).val; val.state = val.state === true ? 'ON' : 'OFF'; // 16.12. Change: Read and Send always all attributes if (publish.LightIds.brightness) { val.brightness = (await this.adapter.getForeignStateAsync(publish.LightIds.brightness)).val; } if (publish.LightIds.color) { val.color_mode = 'rgb'; val.color = this.hexToRgb( (await this.adapter.getForeignStateAsync(publish.LightIds.color)).val, ); } if (publish.LightIds.effects) { const effect = (await this.adapter.getForeignStateAsync(publish.LightIds.effects)).val; val.effect = ''; if (publish.effects[effect]) { val.effect = publish.effects[effect]; } } } // Cover if (publish.cover) { if (publish.messageAssign) { if (publish.messageAssign[val]) { val = publish.messageAssign[val]; } else { return; } } } // Lock if (publish.lock) { if (publish.messageAssign) { if (publish.messageAssign[val]) { val = publish.messageAssign[val]; } else { return; } } } // safe old values (5 last values) if (publish.values) { if (!publish.oldValues) { publish.oldValues = []; } if (publish.oldValues.length >= this.MaxValueCount) { publish.oldValues.pop(); } publish.oldValues.unshift(structuredClone(publish.values)); } if (!publish.values) { publish.values = {}; } publish.values.val = val; publish.values.ts = Date.now(); publish.values.time = new Date(Date.now()).toLocaleString( this.Timeoutput.Argument, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error this.Timeoutput.Format, ); if (typeof val !== 'string') { val = JSON.stringify(val); } if (!options) { options = { retain: true }; } else if (options.retain === undefined) { options.retain = true; } await this.bridgeMqttClient.publish(topic, val, options); await this.adapter.setState('info.publishedIds', JSON.stringify(this.PublishedIds), true); } /* alt 26.11.2025 if (this.PublishedIds[id].light) { val = {}; val.state = (await this.adapter.getForeignStateAsync(this.PublishedIds[id].LightIds.onOff)).val; val.state = val.state === true ? 'ON' : 'OFF'; if (this.PublishedIds[id].LightIds.brightness) { val.brightness = ( await this.adapter.getForeignStateAsync(this.PublishedIds[id].LightIds.brightness) ).val; } if (this.PublishedIds[id].LightIds.color) { val.color_mode = 'rgb'; val.color = this.hexToRgb( (await this.adapter.getForeignStateAsync(this.PublishedIds[id].LightIds.color)).val, ); } if (this.PublishedIds[id].LightIds.effects) { const effect = (await this.adapter.getForeignStateAsync(this.PublishedIds[id].LightIds.effects)) .val; val.effect = ''; if (this.PublishedIds[id].effects[effect]) { val.effect = this.PublishedIds[id].effects[effect]; } } } if (this.PublishedIds[id].cover) { if (this.PublishedIds[id].message) { if (this.PublishedIds[id].message[val]) { val = this.PublishedIds[id].message[val]; } else { val = ''; } } } // safe old values (5 last values) if (this.PublishedIds[id].values) { if (!this.PublishedIds[id].oldValues) { this.PublishedIds[id].oldValues = []; } if (this.PublishedIds[id].oldValues.length >= this.MaxValueCount) { this.PublishedIds[id].oldValues.pop(); } this.PublishedIds[id].oldValues.unshift(structuredClone(this.PublishedIds[id].values)); } if (!this.PublishedIds[id].values) { this.PublishedIds[id].values = {}; } this.PublishedIds[id].values.val = val; this.PublishedIds[id].values.ts = Date.now(); this.PublishedIds[id].values.time = new Date(Date.now()).toLocaleString( this.Timeoutput.Argument, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error this.Timeoutput.Format, ); if (typeof val !== 'string') { val = JSON.stringify(val); } if (!options) { options = { retain: true }; } else if (options.retain === undefined) { options.retain = true; } await this.bridgeMqttClient.publish(this.PublishedIds[id].state_topic, val, options); await this.adapter.setState('info.publishedIds', JSON.stringify(this.PublishedIds), true); */ } else { this.adapter.log.debug(`Id ${id} is not set for publish.`); } } catch (error) { this.adapter.log.error(`error at ${activeFunction}: ${error}`); } } /** * @param id Id of actual element * @param options Options for using spezial fuctions */ async buildDiscovery(id, options) { const activeFunction = 'bridge.js - buildDiscovery'; this.adapter.log.debug(`Function ${activeFunction} started.`); try { // Defaultvalue for discover let returnValue = { newDevice: undefined, newId: undefined }; // Query for decoded Folder if (id.includes(`${this.adapter.messagehandler.directoryhandler.reachableSubfolders.uplinkDecoded}.`)) { const changeInfo = await this.adapter.getChangeInfo(id); const Bridgestate = { discover: false, publish: false, subscribe: false, }; // Query for Stateconfig if (this.adapter.config.BridgeStateConfig) { for (const config of this.adapter.config.BridgeStateConfig) { if ( (changeInfo.applicationId === config.Application || config.Application === '*') && (changeInfo.deviceEUI === config.Device || config.Device === '*') && (id.includes(`.${config.Folder}.`) || config.Folder === '*') && (id.endsWith(`.decoded.${config.State}`) || config.State === '*') ) { Bridgestate.discover = !config.exclude; Bridgestate.publish = true; if (config.exclude) { this.adapter.log.debug( `The Id: ${id} matches the exclude of the config: ${JSON.stringify(config)}`, ); break; } else { this.adapter.log.debug( `The Id: ${id} matches the discovery of the config: ${JSON.stringify(config)}`, ); } } } if (Bridgestate.discover) { options.Bridgestate = Bridgestate; const DiscoveryObject = await this.getDiscoveryObject(changeInfo, options); if (Bridgestate.publish) { this.assignIdStructure( this.PublishedIds, id, { applicationName: changeInfo.applicationName, usedApplicationName: changeInfo.usedApplicationName, deviceId: changeInfo.deviceId, usedDeviceId: changeInfo.usedDeviceId, }, DiscoveryObject?.topic, DiscoveryObject?.payload, DiscoveryObject?.payload.state_topic, undefined, ); } returnValue = await this.publishDiscovery(id, { topic: DiscoveryObject?.topic, payload: structuredClone(DiscoveryObject?.payload), informations: { applicationName: changeInfo.applicationName, usedApplicationName: changeInfo.usedApplicationName, deviceId: changeInfo.deviceId, usedDeviceId: changeInfo.usedDeviceId, }, }); // Delay for publish new entity setTimeout(async () => { await this.publishId(id, undefined, {}); }, 1000); } } // Query for Control Folder } else if ( id.includes(`${this.adapter.messagehandler.directoryhandler.reachableSubfolders.downlinkControl}.`) ) { const changeInfo = await this.adapter.getChangeInfo(id); const Bridgestate = { discover: false, publish: false, subscribe: false, }; // Query for Stateconfig if (this.adapter.config.BridgeStateConfig) { for (const config of this.adapter.config.BridgeStateConfig) { if ( (changeInfo.applicationId === config.Application || config.Application === '*') && (changeInfo.deviceEUI === config.Device || config.Device === '*') && (id.includes(`.${config.Folder}.`) || config.Folder === '*') && (id.endsWith(`.control.${config.State}`) || config.State === '*') ) { Bridgestate.discover = !config.exclude; Bridgestate.publish = config.publish; Bridgestate.subscribe = config.subscribe; if (config.exclude) { break; } } } if (Bridgestate.discover) { options.Bridgestate = Bridgestate; const DiscoveryObject = await this.getDiscoveryObject(changeInfo, options); if (Bridgestate.publish) {