iobroker.lorawan
Version:
converts the desired lora gateway data to a ioBroker structure
855 lines (812 loc) • 53.8 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v2.6.0
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
const mqttClientClass = require('./lib/modules/mqttclient');
const messagehandlerClass = require('./lib/modules/messagehandler');
const downlinkConfighandlerClass = require('./lib/modules/downlinkConfighandler');
class Lorawan extends utils.Adapter {
/**
* @param [options] options of the adapter
*/
constructor(options) {
super({
...options,
name: 'lorawan',
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
// this.on("objectChange", this.onObjectChange.bind(this));
this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
this.on('fileChange', this.onFileChange.bind(this));
this.origin = {
ttn: 'ttn',
chirpstack: 'chirpstack',
};
// Simulation variables
this.simulation = {};
}
onFileChange(_id, _fileName, _size) {
// restart adapter after upload
//this.restart();
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
const activeFunction = 'onReady';
try {
// create downlinkConfigs
this.downlinkConfighandler = new downlinkConfighandlerClass(this);
// Merge the configed and standard profile of downlinks
await this.downlinkConfighandler.addAndMergeDownlinkConfigs();
// create new messagehandler
this.messagehandler = new messagehandlerClass(this);
// Set all mqtt clients
this.mqttClient = new mqttClientClass(this, this.config);
// generate new configed downlinkstates on allready existing devices at adapter startup
await this.messagehandler.generateDownlinksAndRemoveStatesAtStatup();
// generate deviceinfo of all devices in info folder
await this.messagehandler.generateDeviceinfosAtStartup();
// get history instances at Startup
await this.messagehandler.setCustomObjectAtStartup();
//Subscribe all configuration and control states
this.subscribeStatesAsync('*');
this.log.silly(`the adapter starts with downlinkconfigs: ${JSON.stringify(this.config.downlinkConfig)}.`);
this.log.silly(
`the active downlinkconfigs are: ${JSON.stringify(this.downlinkConfighandler.activeDownlinkConfigs)}`,
);
/*
setTimeout(async () => {
this.log.debug('vor Simulation');
await this.startSimulation();
this.log.debug('nach Simulation');
}, 5000);
*/
/*this.simulation.timeout = setTimeout(async () => {
const topic = "application/d63c10b6-9263-4ab3-9299-4308fa19a2ad/device/f1c0ae0e-b4a2-4547-b360-7cfa15e85734/command/down";
const message = {devEui:"f1c0ae0e-b4a2-4547-b360-7cfa15e85734",confirmed:false,fPort:1,data:"AAA"};
await this.mqttClient?.publish(topic,JSON.stringify(message));
}, 5000);*/
} catch (error) {
this.log.error(`error at ${activeFunction}: ${error}`);
}
}
async startSimulation() {
// TTN
//const topic ="v3/hafi-ttn-lorawan@ttn/devices/Meins/up";
//const message = {"end_device_ids":{"device_id":"eui-lobaro-modbus","application_ids":{"application_id":"hafi-ttn-lorawan"},"dev_eui":"70B3D5E050013950","join_eui":"D55B58C0DDC074DE","dev_addr":"260B5972"},"correlation_ids":["gs:uplink:01HMQZVSCX4D7JRDNFA7GJ9D4W"],"received_at":"2024-01-22T07:06:25.260676101Z","uplink_message":{"session_key_id":"AY0v/ZirzRkpNW0Cgjdhig==","f_port":20,"f_cnt":2,"frm_payload":"AA5BAf0AxwIAAQ==","decoded_payload":{"airhumidity":50.9,"airtemperature":19.9,"port":20,"relais1":0,"relais2":1,"relais3":null,"relais5":null,"volt":3.649,"zisternenpegel":2},"rx_metadata":[{"gateway_ids":{"gateway_id":"hafenmeister-port2ttn-ng","eui":"50313953530A4750"},"time":"2024-01-22T07:06:25.013878Z","timestamp":995696116,"rssi":-37,"channel_rssi":-37,"snr":8.5,"location":{"latitude":53.5548443059465,"longitude":9.92155426743724,"altitude":10,"source":"SOURCE_REGISTRY"},"uplink_token":"CiYKJAoYaGFmZW5tZWlzdGVyLXBvcnQydHRuLW5nEghQMTlTUwpHUBD0u+TaAxoLCPGnuK0GEM3uvhkgoIL0oP24Sg==","channel_index":5,"received_at":"2024-01-22T07:06:25.032492359Z"}],"settings":{"data_rate":{"lora":{"bandwidth":125000,"spreading_factor":9,"coding_rate":"4/5"}},"frequency":"867500000","timestamp":995696116,"time":"2024-01-22T07:06:25.013878Z"},"received_at":"2024-01-22T07:06:25.054442349Z","consumed_airtime":"0.205824s","network_ids":{"net_id":"000013","ns_id":"EC656E0000000181","tenant_id":"ttn","cluster_id":"eu1","cluster_address":"eu1.cloud.thethings.network"}}};
/*
const topic = 'v3/hafi-ttn-lorawan@ttn/devices/eui-00137A1000044DF5/up';
const message = {
end_device_ids: {
device_id: 'shlm-luxsensor-001',
application_ids: { application_id: 'shlm-clima' },
dev_eui: '00137A1000044DF5',
join_eui: '00137A1000000002',
dev_addr: '260B60D9',
},
correlation_ids: ['gs:uplink:01JDM7QFYGPF09F2NK2YW5GW78'],
received_at: '2024-11-26T12:39:20.993583474Z',
uplink_message: {
session_key_id: 'AZNi5dvTa96t7M6UDHLcnw==',
f_port: 6,
f_cnt: 107,
frm_payload: 'AR4BJAAAAQIAAAA=',
decoded_payload: { BatV: 3.6, Illuminance: 258 },
rx_metadata: [
{
gateway_ids: { gateway_id: 'shlmgw03', eui: 'A84041FDFE291238' },
time: '2024-11-26T12:39:20.769507Z',
timestamp: 184317371,
rssi: -29,
channel_rssi: -29,
snr: 14,
frequency_offset: '9922',
location: {
latitude: 54.51076410677215,
longitude: 9.540317058563234,
source: 'SOURCE_REGISTRY',
},
uplink_token: 'ChYKFAoIc2hsbWd3MDMSCKhAQf3+KRI4ELvr8VcaDAj4gpe6BhCWuPL1AiD4xK/RrrNE',
channel_index: 1,
received_at: '2024-11-26T12:39:20.784112662Z',
},
{
gateway_ids: { gateway_id: 'shlmgw01', eui: 'A84041FDFE276220' },
time: '2024-11-26T12:39:20.764831Z',
timestamp: 304678548,
rssi: -105,
channel_rssi: -105,
snr: -1.8,
frequency_offset: '10121',
location: {
latitude: 54.5128326197047,
longitude: 9.54245877441114,
source: 'SOURCE_REGISTRY',
},
uplink_token: 'ChYKFAoIc2hsbWd3MDESCKhAQf3+J2IgEJSNpJEBGgwI+IKXugYQ9bnS+QIgoOSGgu/qhgE=',
channel_index: 1,
received_at: '2024-11-26T12:39:20.791977205Z',
},
{
gateway_ids: { gateway_id: 'eui-7076ff0056070bc0', eui: '7076FF0056070BC0' },
time: '2024-11-26T12:39:20.756Z',
timestamp: 2848353700,
rssi: -120,
channel_rssi: -120,
snr: -8.2,
uplink_token:
'CiIKIAoUZXVpLTcwNzZmZjAwNTYwNzBiYzASCHB2/wBWBwvAEKTbmc4KGgwI+IKXugYQiY/egwMgoJHx+PLsngEqDAj4gpe6BhCAyr7oAg==',
channel_index: 6,
gps_time: '2024-11-26T12:39:20.756Z',
received_at: '2024-11-26T12:39:20.813139849Z',
},
],
settings: {
data_rate: { lora: { bandwidth: 125000, spreading_factor: 7, coding_rate: '4/5' } },
frequency: '868300000',
timestamp: 184317371,
time: '2024-11-26T12:39:20.769507Z',
},
received_at: '2024-11-26T12:39:20.785204683Z',
consumed_airtime: '0.061696s',
version_ids: {
brand_id: 'netvox',
model_id: 'r718g',
hardware_version: '2',
firmware_version: '10',
band_id: 'EU_863_870',
},
network_ids: {
net_id: '000013',
ns_id: 'EC656E0000000181',
tenant_id: 'ttn',
cluster_id: 'eu1',
cluster_address: 'eu1.cloud.thethings.network',
},
},
};
*/
// ACK
//const topic = "v3/hafi-ttn-lorawan@ttn/devices/eui-a84041162183f8fb/down/ack";
//const message = {"end_device_ids":{"device_id":"eui-a84041162183f8fb","application_ids":{"application_id":"hafi-ttn-lorawan"},"dev_eui":"A84041162183F8FB","join_eui":"A840410000000101","dev_addr":"260B141A"},"correlation_ids":["as:downlink:01HP6D18MQXJN90J5B07DC11HY","gs:uplink:01HP6D1A9X4WAA3SFMXH4ESSMV"],"received_at":"2024-02-09T07:41:41.776887672Z","downlink_ack":{"session_key_id":"AY2MUrmnuovS8DCZAfYmsA==","f_port":1,"f_cnt":21,"frm_payload":"AQAAeA==","confirmed":true,"priority":"NORMAL","correlation_ids":["as:downlink:01HP6D18MQXJN90J5B07DC11HY"],"confirmed_retry":{"attempt":1}}};
/*
// Chipstack
const topic = 'application/d63c10b6-9263-4ab3-9299-4308fa19a2ad/device/a84041f621857cd2/event/up';
const message = {
deduplicationId: '46fecb8d-d7f1-481e-8b1b-bcaad7e5d1e0',
time: '2025-02-18T14:49:19.475813+00:00',
deviceInfo: {
tenantId: '52f14cd4-c6f1-4fbd-8f87-4025e1d49242',
tenantName: 'ChirpStack',
applicationId: 'bac5ba56-f9c6-4d98-a609-8366e048495d',
applicationName: 'Pool',
deviceProfileId: 'e847fd4b-a87e-452c-91b8-a4fbaa51acfa',
deviceProfileName: 'Dragino Feuchtesenor',
deviceName: 'Skimmer',
devEui: 'a84041f621857cd2',
deviceClassEnabled: 'CLASS_A',
tags: {},
},
devAddr: '01cdf20a',
adr: true,
dr: 5,
fCnt: 170,
fPort: 2,
confirmed: false,
data: 'DPYBAaL//AA9',
object: {
Soilmoisture: 41.8,
Soilconductivity: 61,
devicetype: 'Dragino',
Volt: 3.318,
Soiltemperature: -0.4,
},
rxInfo: [
{
gatewayId: '503035416e314750',
uplinkId: 33537,
gwTime: '2025-02-18T14:49:19.475813+00:00',
rssi: -108,
snr: 2,
channel: 4,
location: { latitude: 50.693467319817266, longitude: 8.47676753997803 },
context: 'mRVIdA==',
crcStatus: 'CRC_OK',
},
],
txInfo: {
frequency: 867300000,
modulation: { lora: { bandwidth: 125000, spreadingFactor: 7, codeRate: 'CR_4_5' } },
},
regionConfigId: 'eu868',
};*/
// Chipstack
const topic = 'application/bbea74d6-1fc5-4238-af20-d2aecdbb4f8e/device/70b3d52dd301b3cc/event/up';
const message = {
deduplicationId: '1d94e6d8-25cc-4099-ab45-4a7f7faa1634',
time: '2025-05-15T13:22:08.891648+00:00',
deviceInfo: {
tenantId: '52f14cd4-c6f1-4fbd-8f87-4025e1d49242',
tenantName: 'ChirpStack',
applicationId: 'bbea74d6-1fc5-4238-af20-d2aecdbb4f8e',
applicationName: 'Heizen',
deviceProfileId: 'aedb4f09-8644-430b-a088-47519f01530b',
deviceProfileName: 'MClimate Vicki',
deviceName: 'ThermostatKinderzimmerRika',
devEui: '70b3d52dd301b3cc',
deviceClassEnabled: 'CLASS_A',
tags: {},
},
devAddr: '01b6c24b',
adr: true,
dr: 5,
fCnt: 38547,
fPort: 2,
confirmed: false,
data: 'RADjUgC5gRKfTt7eEfAw',
object: {
CalibrationFailed: false,
ValveOpenness: 0,
MotorRange: 478,
ChildLock: false,
Reason: 81,
RelativeHumidity: 30.47,
BatteryVoltage: 3.5,
AttachedBackplate: true,
extSensorTemperature: 22.7,
TargetTemperature: 18,
PerceiveAsOnline: true,
BrokenSensor: false,
AntiFreezeProtection: false,
HighMotorConsumption: false,
Device: 'Vicki',
SensorTemperature: 23.06,
targetTemperatureFloat: 18.5,
MotorPosition: 478,
OpenWindow: false,
LowMotorConsumption: false,
},
rxInfo: [
{
gatewayId: '503035416e314750',
uplinkId: 55809,
gwTime: '2025-05-15T13:22:08.891648+00:00',
nsTime: '2025-05-15T13:22:08.927071564+00:00',
rssi: -81,
snr: 7.5,
channel: 2,
rfChain: 1,
location: { latitude: 50.693467319817266, longitude: 8.47676753997803 },
context: 'YOhWvA==',
crcStatus: 'CRC_OK',
},
],
txInfo: {
frequency: 868500000,
modulation: { lora: { bandwidth: 125000, spreadingFactor: 7, codeRate: 'CR_4_5' } },
},
regionConfigId: 'eu868',
};
//const topic = "application/d63c10b6-9263-4ab3-9299-4308fa19a2ad/device/a84041f621857cd2/command/down";
//const message = {"devEui":"a84041f621857cd2","confirmed":false,"fPort":1,"data":"AQAqMA=="};
// Chirpstack LT222222
//const topic = "application/d63c10b6-9263-4ab3-9299-4308fa19a2ad/device/a8404127a188d826/event/up";
//const message = {"deduplicationId":"bd3fdb3b-af86-4617-b9f2-da07075d2bc5","time":"2024-01-24T16:47:01.573381+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"d63c10b6-9263-4ab3-9299-4308fa19a2ad","applicationName":"Benjamin Schmidt","deviceProfileId":"f1c0ae0e-b4a2-4547-b360-7cfa15e85734","deviceProfileName":"Dragino LT22222","deviceName":"Relaistestgerät","devEui":"a8404127a188d826","deviceClassEnabled":"CLASS_C","tags":{}},"devAddr":"01dfbaf2","adr":true,"dr":5,"fCnt":12,"fPort":2,"confirmed":false,"data":"AAAAAAAAAAA8/0E=","object":{"RO1_status":"OFF","DO2_status":"H","ACI2_mA":0.0,"DO1_status":"H","Hardware_mode":"LT22222","RO2_status":"OFF","AVI2_V":0.0,"ACI1_mA":0.0,"DI1_status":"H","DI2_status":"H","Work_mode":"2ACI+2AVI","AVI1_V":0.0},"rxInfo":[{"gatewayId":"50303541b0344750","uplinkId":57857,"gwTime":"2024-01-24T16:47:01.573381+00:00","nsTime":"2024-01-24T16:47:02.370171527+00:00","rssi":-54,"snr":8.5,"channel":6,"location":{"latitude":50.69344693065449,"longitude":8.476783633232118},"context":"2tr9BA==","metadata":{"region_config_id":"eu868","region_common_name":"EU868"},"crcStatus":"CRC_OK"}],"txInfo":{"frequency":867700000,"modulation":{"lora":{"bandwidth":125000,"spreadingFactor":7,"codeRate":"CR_4_5"}}}};
//const topic = "application/d63c10b6-9263-4ab3-9299-4308fa19a2ad/device/a8404127a188d826/command/down";
//const message = {"devEui":"a8404127a188d826","confirmed":false,"fPort":1,"data":"AQACWA=="};
// ACK
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/event/ack";
//const message = {"deduplicationId":"b080c0d8-6151-4675-84b8-74ecf9e33bae","time":"2023-08-15T13:22:27.969901+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"59bcc5a7-59e2-4481-9615-fc4e58791915","applicationName":"Mclimate_Vicki","deviceProfileId":"3a9bc28f-3664-4bdf-b3be-a20d1eb32dc8","deviceProfileName":"Mclimate_Vicki","deviceName":"MClimate_Vicki_Heizkoerperventil_001","devEui":"70b3d52dd300ed31","deviceClassEnabled":"CLASS_A","tags":{}},"queueItemId":"3434298f-2b89-49f8-885e-9fdd9f0892e6","acknowledged":true,"fCntDown":262};
// TXACK
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/event/txack";
//const message = {"downlinkId":2478630510,"time":"2024-01-27T11:50:04.736655452+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"59bcc5a7-59e2-4481-9615-fc4e58791915","applicationName":"Mclimate_Vicki","deviceProfileId":"3a9bc28f-3664-4bdf-b3be-a20d1eb32dc8","deviceProfileName":"Mclimate_Vicki","deviceName":"MClimate_Vicki_Heizkoerperventil_001","devEui":"70b3d52dd300ed31","deviceClassEnabled":"CLASS_A","tags":{}},"queueItemId":"efc2bacf-d5da-48d3-a6ef-2a77fda41bd0","fCntDown":4940,"gatewayId":"50313953530a4750","txInfo":{"frequency":868300000,"power":16,"modulation":{"lora":{"bandwidth":125000,"spreadingFactor":7,"codeRate":"CR_4_5","polarizationInversion":true}},"timing":{"delay":{"delay":"1s"}},"context":"eqFuiw=="}};
// STATUS
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/event/status";
//const message = {"deduplicationId":"4a91b00d-b5e1-4955-b085-ba21b9318213","time":"2024-01-26T20:18:45.299871+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"59bcc5a7-59e2-4481-9615-fc4e58791915","applicationName":"Mclimate_Vicki","deviceProfileId":"3a9bc28f-3664-4bdf-b3be-a20d1eb32dc8","deviceProfileName":"Mclimate_Vicki","deviceName":"MClimate_Vicki_Heizkoerperventil_001","devEui":"70b3d52dd300ed31","deviceClassEnabled":"CLASS_A","tags":{}},"margin":7,"externalPowerSource":false,"batteryLevelUnavailable":false,"batteryLevel":85.826775};
// UP
//const topic = "application/e91e66ba-1aa7-4bdf-af88-f1246e0b8d75/device/a84041263188b787/event/up";
//const message = {"deduplicationId":"ce1ca35d-35c7-4f60-844c-c2b2810fd74b","time":"2024-08-25T07:10:47.758298+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"e91e66ba-1aa7-4bdf-af88-f1246e0b8d75","applicationName":"Türen","deviceProfileId":"431c5895-68e2-478d-945f-f0e9a6f5f9f5","deviceProfileName":"Dragino Türsensoren / Fenstersensoren","deviceName":"Flurtüre","devEui":"a84041263188b787","deviceClassEnabled":"CLASS_A","tags":{}},"devAddr":"0061ebd4","adr":true,"dr":5,"fCnt":8264,"fPort":10,"confirmed":false,"data":"DAYBAA+IAAAAAA==","object":{"ALARM":0.0,"BAT_V":3.078,"CONTACT":true,"OPEN_TIMES":3976.0,"MOD":1.0,"LAST_OPEN_DURATION":0.0,"OPEN":false,"devicetype":"Dragino"},"rxInfo":[{"gatewayId":"503035416e314750","uplinkId":64001,"gwTime":"2024-08-25T07:10:47.758298+00:00","nsTime":"2024-08-25T07:11:29.787667701+00:00","rssi":-68,"snr":9.25,"channel":6,"location":{"latitude":50.69350130173554,"longitude":8.476821184158327},"context":"fp1WbA==","metadata":{"region_common_name":"EU868","region_config_id":"eu868"},"crcStatus":"CRC_OK"}],"txInfo":{"frequency":867700000,"modulation":{"lora":{"bandwidth":125000,"spreadingFactor":7,"codeRate":"CR_4_5"}}}};
// LOG
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/event/up";
//const message = {"time":"2024-01-27T10:29:58.221817559+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"59bcc5a7-59e2-4481-9615-fc4e58791915","applicationName":"Mclimate_Vicki","deviceProfileId":"3a9bc28f-3664-4bdf-b3be-a20d1eb32dc8","deviceProfileName":"Mclimate_Vicki","deviceName":"MClimate_Vicki_Heizkoerperventil_001","devEui":"70b3d52dd300ed31","deviceClassEnabled":"CLASS_A","tags":{}},"level":"ERROR","code":"UPLINK_CODEC","description":"Exception generated by quickjs","context":{"deduplication_id":"c44e7e25-09ce-4c95-b96f-5a298c5c6440"}};
// JOIN
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/event/join";
//const message = {"deduplicationId":"44cef56d-1b8d-45fc-a762-03b98b620db2","time":"2023-12-12T03:13:21.551178+00:00","deviceInfo":{"tenantId":"52f14cd4-c6f1-4fbd-8f87-4025e1d49242","tenantName":"ChirpStack","applicationId":"59bcc5a7-59e2-4481-9615-fc4e58791915","applicationName":"Mclimate_Vicki","deviceProfileId":"3a9bc28f-3664-4bdf-b3be-a20d1eb32dc8","deviceProfileName":"Mclimate_Vicki","deviceName":"MClimate_Vicki_Heizkoerperventil_001","devEui":"70b3d52dd300ed31","deviceClassEnabled":"CLASS_A","tags":{}},"devAddr":"01009400"};
// DOWN
//const topic = "application/59bcc5a7-59e2-4481-9615-fc4e58791915/device/70b3d52dd300ed31/command/down";
//const message = {"devEui": "70b3d52dd300ed31", "confirmed": false,"fPort": 1,"data": "DQEYDQEY"};
this.log.debug(`incomming topic: ${topic}`);
this.log.debug(`incomming message: ${JSON.stringify(message)}`);
await this.messagehandler?.handleMessage(topic, message);
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
*
* @param callback function wich is called after shutdown adapter
*/
onUnload(callback) {
try {
// clear timeout (for simulation)
if (this.simulation.timeout) {
this.clearTimeout(this.simulation.timeout);
delete this.simulation.timeout;
}
// Clear Schedules im directoriehandler
this.messagehandler?.clearAllSchedules();
// Destroy mqtt client
this.mqttClient?.destroy();
callback();
} catch (e) {
this.log.error(e);
callback();
}
}
// If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
// You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
// /**
// * Is called if a subscribed object changes
// * @param {string} id
// * @param {ioBroker.Object | null | undefined} obj
// */
// onObjectChange(id, obj) {
// if (obj) {
// // The object was changed
// this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
// } else {
// // The object was deleted
// this.log.info(`object ${id} deleted`);
// }
// }
/**
* Is called if a subscribed state changes
*
* @param id id of the chaned state
* @param state value and ack of the chanedf state
*/
async onStateChange(id, state) {
const activeFunction = 'onStateChange';
try {
if (state) {
//this.log.silly(`state ${id} changed: val: ${state.val} - ack: ${state.ack}`);
// The state was changed => only states with ack = false will be processed, others will be ignored
if (!state.ack) {
// Check for downlink in id
if (id.indexOf('.downlink.control.') !== -1) {
this.log.silly(`the state ${id} has changed to ${state.val}.`);
// get information of the changing state
const changeInfo = await this.getChangeInfo(id, { withBestMatch: true });
const suffix = this.downlinkConfighandler?.getDownlinkTopicSuffix(changeInfo?.changedState);
if (changeInfo?.changedState === 'push' || changeInfo?.changedState === 'replace') {
const downlinkTopic = this.downlinkConfighandler?.getDownlinkTopic(changeInfo, suffix);
try {
if (JSON.parse(state.val)) {
await this.sendDownlink(downlinkTopic, state.val, changeInfo);
this.setState(id, state.val, true);
}
} catch (error) {
this.log.warn(`Cant send invalid downlinks. Error: ${error}`);
}
} else if (changeInfo?.changedState === 'CustomSend') {
if (state.val !== '') {
const downlinkTopic = this.downlinkConfighandler?.getDownlinkTopic(changeInfo, suffix);
const downlinkConfig =
this.downlinkConfighandler?.activeDownlinkConfigs[
changeInfo.bestMatchForDeviceType
];
const Statevalues = state.val.split(',');
const StateElements = {
PayloadInHex: Statevalues[0].toUpperCase(),
Port: Statevalues[1] ? parseInt(Statevalues[1]) : downlinkConfig.port,
Confirmed: Statevalues[2]
? Statevalues[2] === 'true'
? true
: false
: downlinkConfig.confirmed,
Priority: Statevalues[3] ? Statevalues[3] : downlinkConfig.priority,
};
// Query for righte type
this.log.debug('The following values are detected at input of custom send state');
for (const element of Object.values(StateElements)) {
this.log.debug(typeof element);
this.log.debug(element);
}
// Write into nextSend
await this.writeNextSend(changeInfo, StateElements.PayloadInHex);
if (
!changeInfo?.bestMatchForDeviceType ||
this.downlinkConfighandler?.activeDownlinkConfigs[changeInfo.bestMatchForDeviceType]
.sendWithUplink === 'disabled'
) {
const downlink = this.downlinkConfighandler?.getDownlink(
{
port: StateElements.Port,
confirmed: StateElements.Confirmed,
priority: StateElements.Priority,
},
StateElements.PayloadInHex,
changeInfo,
);
if (downlink !== undefined) {
await this.sendDownlink(downlinkTopic, JSON.stringify(downlink), changeInfo);
}
}
}
this.setState(id, state.val, true);
} else {
const downlinkTopic = this.downlinkConfighandler?.getDownlinkTopic(changeInfo, suffix);
const downlinkParameter = this.downlinkConfighandler?.getDownlinkParameter(changeInfo, {});
if (downlinkParameter !== undefined) {
const payloadInHex = this.downlinkConfighandler?.calculatePayloadInHex(
downlinkParameter,
state,
);
await this.writeNextSend(changeInfo, payloadInHex);
if (
!changeInfo?.bestMatchForDeviceType ||
this.downlinkConfighandler?.activeDownlinkConfigs[changeInfo.bestMatchForDeviceType]
.sendWithUplink === 'disabled'
) {
const downlink = this.downlinkConfighandler?.getDownlink(
downlinkParameter,
payloadInHex,
changeInfo,
);
if (downlink !== undefined) {
await this.sendDownlink(downlinkTopic, JSON.stringify(downlink), changeInfo);
}
}
this.setState(id, state.val, true);
}
}
} else if (id.indexOf('.configuration.') !== -1) {
// State is from configuration path
const changeInfo = await this.getChangeInfo(id, { withBestMatch: true });
this.messagehandler?.fillWithDownlinkConfig(changeInfo?.objectStartDirectory, {});
// remove not configed states
const adapterObjects = await this.getAdapterObjectsAsync();
for (const adapterObject of Object.values(adapterObjects)) {
if (
adapterObject.type === 'state' &&
adapterObject._id.indexOf(`${changeInfo?.objectStartDirectory}.downlink.control`) !== -1
) {
const changeInfo = await this.getChangeInfo(adapterObject._id);
const downlinkParameter = this.downlinkConfighandler?.getDownlinkParameter(changeInfo, {
startupCheck: true,
});
if (!downlinkParameter) {
await this.delObjectAsync(this.removeNamespace(adapterObject._id));
}
}
}
this.setState(id, state.val, true);
}
}
} else {
// The state was deleted
this.log.info(`state ${id} deleted`);
}
} catch (error) {
this.log.error(`error at ${activeFunction}: ${error}`);
}
}
async checkSendDownlinkWithUplink(id) {
const activeFunction = 'checkSendDownlinkWithUplink';
try {
this.log.silly(`Check for send downlink with uplink.`);
const changeInfo = await this.getChangeInfo(id, { withBestMatch: true });
if (
changeInfo &&
changeInfo.bestMatchForDeviceType &&
this.downlinkConfighandler?.activeDownlinkConfigs[changeInfo.bestMatchForDeviceType].sendWithUplink !==
'disabled'
) {
const nextSend = await this.getNextSend(changeInfo?.objectStartDirectory);
if (nextSend?.val !== '0') {
const suffix = this.downlinkConfighandler?.getDownlinkTopicSuffix('push');
const downlinkTopic = this.downlinkConfighandler?.getDownlinkTopic(changeInfo, suffix);
const downlinkConfig =
this.downlinkConfighandler?.activeDownlinkConfigs[changeInfo.bestMatchForDeviceType];
const downlink = this.downlinkConfighandler?.getDownlink(downlinkConfig, nextSend?.val, changeInfo);
if (downlink !== undefined) {
await this.sendDownlink(downlinkTopic, JSON.stringify(downlink), changeInfo);
}
}
}
} catch (error) {
this.log.error(`error at ${activeFunction}: ${error}`);
}
}
async getNextSend(deviceDirectory) {
const idFolder = `${deviceDirectory}.${this.messagehandler?.directoryhandler.reachableSubfolders.downlinkNextSend}`;
return await this.getStateAsync(`${idFolder}.hex`);
}
async writeNextSend(changeInfo, payloadInHex) {
const idFolderNextSend = `${changeInfo.objectStartDirectory}.${this.messagehandler?.directoryhandler.reachableSubfolders.downlinkNextSend}`;
if (
changeInfo.bestMatchForDeviceType &&
this.downlinkConfighandler?.activeDownlinkConfigs[changeInfo.bestMatchForDeviceType].sendWithUplink ===
'enabled & collect'
) {
const nextSend = await this.getStateAsync(`${idFolderNextSend}.hex`);
if (nextSend?.val !== '0') {
payloadInHex = nextSend?.val + payloadInHex;
}
}
await this.setState(`${idFolderNextSend}.hex`, payloadInHex, true);
}
async sendDownlink(topic, message, changeInfo) {
await this.mqttClient?.publish(topic, message, {});
const idFolderNextSend = `${changeInfo.objectStartDirectory}.${this.messagehandler?.directoryhandler.reachableSubfolders.downlinkNextSend}`;
const idFolderLastSend = `${changeInfo.objectStartDirectory}.${this.messagehandler?.directoryhandler.reachableSubfolders.downlinkLastSend}`;
const nextSend = await this.getStateAsync(`${idFolderNextSend}.hex`);
const lastSend = this.getHexpayloadFromDownlink(message);
await this.setState(`${idFolderLastSend}.hex`, lastSend, true);
if (nextSend && lastSend === nextSend?.val) {
await this.setState(`${idFolderNextSend}.hex`, '0', true);
}
}
getHexpayloadFromDownlink(downlinkmessage) {
let downlink = downlinkmessage;
if (typeof downlink === 'string') {
downlink = JSON.parse(downlinkmessage);
} else if (typeof downlink !== 'object') {
return 0;
}
let payload = '';
switch (this.config.origin) {
case this.origin.ttn:
payload = downlink.downlinks[0].frm_payload;
break;
case this.origin.chirpstack:
payload = downlink.data;
break;
}
return Buffer.from(payload, 'base64').toString('hex').toUpperCase();
}
getBaseDeviceInfo(id) {
const activeFunction = 'getBaseDeviceInfo';
try {
id = this.removeNamespace(id);
const idElements = id.split('.');
const deviceInfo = {
id: id,
applicationId: idElements[0],
deviceEUI: idElements[2],
changedState: idElements[idElements.length - 1],
objectStartDirectory: `${idElements[0]}.${idElements[1]}.${idElements[2]}`,
allElements: idElements,
};
return deviceInfo;
} catch (error) {
this.log.error(`error at ${activeFunction}: ${error}`);
}
}
async getChangeInfo(id, options) {
const activeFunction = 'getChangeInfo';
try {
this.log.silly(`changeinfo of id ${id}, will be generated.`);
const changeInfo = this.getBaseDeviceInfo(id);
const myId = `${changeInfo?.objectStartDirectory}.${this.messagehandler?.directoryhandler.reachableSubfolders.configuration}.devicetype`;
// Check for changeInfo
if (changeInfo) {
// Get Obect from startdirectory
const applicationDirectoryObject = await this.getObjectAsync(changeInfo.applicationId);
const startDirectoryObject = await this.getObjectAsync(changeInfo.objectStartDirectory);
if (applicationDirectoryObject && startDirectoryObject) {
changeInfo.applicationName = applicationDirectoryObject.native.applicationName;
changeInfo.usedApplicationName = applicationDirectoryObject.common.name;
changeInfo.deviceId = startDirectoryObject.native.deviceId;
changeInfo.usedDeviceId = startDirectoryObject.common.name;
}
// Get deviceType
const deviceTypeIdState = await this.getStateAsync(myId);
if (deviceTypeIdState) {
changeInfo.deviceType = deviceTypeIdState.val;
if (options && options.withBestMatch) {
// Get best match of expert downlink
const bestMatchForDeviceType =
this.downlinkConfighandler?.getBestMatchForDeviceType(changeInfo);
if (bestMatchForDeviceType) {
changeInfo.bestMatchForDeviceType = bestMatchForDeviceType;
this.log.debug(
`best match for expertconfig of device: ${changeInfo.deviceType ? changeInfo.deviceType : 'empty devicetype'} is: ${bestMatchForDeviceType}`,
);
} else {
this.log.debug(
`no match for expert downlinkconfig found: ${changeInfo.deviceType ? changeInfo.deviceType : 'empty devicetype'}`,
);
}
}
}
}
this.log.silly(`changeinfo is ${JSON.stringify(changeInfo)}.`);
return changeInfo;
} catch (error) {
this.log.error(`error at ${activeFunction}: ${error}`);
}
}
removeNamespace(id) {
if (id.indexOf(this.namespace) !== -1) {
this.log.silly(`namespace will be removed from id ${id}.`);
id = id.substring(this.namespace.length + 1, id.length);
}
return id;
}
// Get Changeinfo in case of device EUI (used more times in onMessage)
async getChangeInfoFromDeviceEUI(deviceUI, subId) {
let changeInfo = undefined;
const adapterObjects = await this.getAdapterObjectsAsync();
for (const adapterObject of Object.values(adapterObjects)) {
if (adapterObject.type === 'device') {
if (adapterObject._id.indexOf(deviceUI) !== -1) {
changeInfo = await this.getChangeInfo(`${adapterObject._id}.${subId}`);
break;
}
}
}
return changeInfo;
}
// If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
// /**
// * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
// * Using this method requires "common.messagebox" property to be set to true in io-package.json
// * @param {ioBroker.Message} obj
// */
async onMessage(obj) {
const activeFunction = 'onMessage';
this.log.debug(`message received: command = ${obj.command} - message = ${JSON.stringify(obj.message)}`);
try {
if (typeof obj === 'object' && obj.message) {
let result = {};
if (obj.command === 'getDeviceInfo') {
if (obj.message.deviceEUI) {
const changeInfo = await this.getChangeInfoFromDeviceEUI(
obj.message.deviceEUI,
`${this.messagehandler?.directoryhandler.reachableSubfolders.configuration}.devicetype`,
);
if (changeInfo) {
result = {
applicationId: changeInfo.applicationId,
applicationName: changeInfo.applicationName,
usedApplicationName: changeInfo.usedApplicationName,
deviceEUI: changeInfo.deviceEUI,
deviceId: changeInfo.deviceId,
usedDeviceId: changeInfo.usedDeviceId,
deviceType: changeInfo.deviceType,
received: obj.message,
};
} else {
result = { error: true, message: 'No device found', received: obj.message };
}
} else {
result = { error: true, message: 'No deviceEUI found', received: obj.message };
}
// Send response
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
} else if (obj.command === 'getUplink') {
if (obj.message.deviceEUI && obj.message.uplink) {
const folderAndUplinkId = obj.message.subfolder
? `${this.messagehandler?.directoryhandler.reachableSubfolders.uplink}.${obj.message.subfolder}.${obj.message.uplink}`
: obj.message.uplink;
const changeInfo = await this.getChangeInfoFromDeviceEUI(
obj.message.deviceEUI,
folderAndUplinkId,
);
if (changeInfo) {
const uplinkId = changeInfo.id;
if (await this.objectExists(uplinkId)) {
const stateResult = await this.getStateAsync(changeInfo.id);
if (stateResult) {
result = {
applicationId: changeInfo.applicationId,
applicationName: changeInfo.applicationName,
usedApplicationName: changeInfo.usedApplicationName,
deviceEUI: changeInfo.deviceEUI,
deviceId: changeInfo.deviceId,
usedDeviceId: changeInfo.usedDeviceId,
deviceType: changeInfo.deviceType,
value: stateResult.val,
received: obj.message,
};
}
} else {
result = { error: true, message: 'No uplink matches', received: obj.message };
}
} else {
result = { error: true, message: 'No device found', received: obj.message };
}
} else {
result = { error: true, message: 'No deviceEUI & uplink found', received: obj.message };
}
// Send response
if (obj.callback) {
this.sendTo(obj.from, obj.command, result, obj.callback);
}
} else if (obj.command === 'setDownlink') {
if (
obj.message.deviceEUI &&
obj.message.downlink &&
(obj.message.value || obj.message.value === false)
) {
const changeInfo = await this.getChangeInfoFromDeviceEUI(
obj.message.deviceEUI,
`${this.messagehandler?.directoryhandler.reachableSubfolders.downlinkControl}.${obj.message.downlink}`,
);
if (changeInfo) {
const downlinkId = changeInfo.id;
if (await this.objectExists(downlinkId)) {
// get Object to decide min and max value
const downlinkObject = await this.getObjectAsync(downlinkId);
if (downlinkObject) {
// check typ number
if (downlinkObject.common.type === 'number') {
if (typeof obj.message.value === 'number') {
// Check limit
if (
(!downlinkObject.common.min ||
obj.message.value >= downlinkObject.common.min) &&
(!downlinkObject.common.max ||
obj.message.value <= downlinkObject.common.max)
) {
await this.setState(downlinkId, obj.message.value);
result = {
applicationId: changeInfo.applicationId,
applicationName: changeInfo.applicationName,
usedApplicationName: changeInfo.usedApplicationName,
deviceEUI: changeInfo.deviceEUI,
deviceId: changeInfo.deviceId,
deviceType: changeInfo.deviceType,
downlink: obj.message.downlink,
value: obj.message.value,
received: obj.message,
};
} else {
result = {
error: true,
message: 'value is not in valid range',
received: obj.message,
};
}
} else {
result = {
error: true,
message: `downlink is type number, but received ${typeof obj.message.value}`,
received: obj.message,
};
}
} else {
// downlinkobject is not a number
if (downlinkObject.common.type !== typeof obj.message.value) {
result = {
error: true,
message: `downlink is type ${downlinkObject.common.type}, but received ${typeof obj.message.value}`,
received: obj.message,
};
} else {
await this.setState(downlinkId, obj.message.value);
result = {
applicationId: changeInfo.applicationId,
applicationName: changeInfo.applicationName,
deviceEUI: changeInfo.deviceEUI,
deviceId: changeInfo.deviceId,
deviceType: changeInfo.deviceType,
downlink: obj.message.downlink,
value: obj.message.value,
received: obj.message,
};
}
}
}
} else {
result = { error: true, message: 'No downlink matches', received: obj.message };
}
} else {
result = { error: true, message: 'No device found', received: obj.me