iobroker.lorawan
Version:
converts the desired lora gateway data to a ioBroker structure
1,079 lines (1,003 loc) • 154 kB
JavaScript
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,
};
let deviceSuffix = '';
// 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;
deviceSuffix = config.deviceSuffix;
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.deviceSuffix = deviceSuffix;
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,
};
let deviceSuffix = '';
// 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) {