UNPKG

iobroker.wireless-mbus

Version:

Receive data from Wireless Meter-Bus (wM-Bus) devices like gas or electricity meters

463 lines (390 loc) 15 kB
/* # vim: tabstop=4 shiftwidth=4 expandtab * * ioBroker wmbus adapter * * Copyright (c) 2019 ISFH * This work is licensed under the terms of the GPL2 license. * See NOTICE for detailed listing of other contributors * * This file contains large portions from the ioBroker mbus adapter * by Apollon77 which is originally published under the MIT License. * * Adapter loading data from an wM-Bus devices * */ 'use strict'; const utils = require('@iobroker/adapter-core'); const fs = require('fs'); const WMBusDecoder = require('./lib/wmbus_decoder.js'); const ObjectHelper = require('./lib/ObjectHelper.js'); const { SerialPort } = require('serialport'); let ReceiverModule; const receiverPath = '/lib/receiver/'; class WirelessMbus extends utils.Adapter { constructor(options) { super({ ...options, name: 'wireless-mbus', }); this.on('ready', this.onReady.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); this.objectHelper = new ObjectHelper(this); this.receivers = {}; this.connected = false; this.receiver = null; this.decoder = null; this.failedDevices = []; this.needsKey = []; this.createdDevices = []; this.stateValues = {}; } onUnload(callback) { try { this.receiver.port.close(); this.receiver = undefined; this.decoder = undefined; callback && callback(); } catch (e) { callback && callback(); } } async onReady() { const objConnection = { '_id': 'info.connection', 'type': 'state', 'common': { 'role': 'indicator.connected', 'name': 'If connected to wM-Bus receiver', 'type': 'boolean', 'read': true, 'write': false, 'def': false }, 'native': {} }; await this.objectHelper.createObject(objConnection._id, objConnection); const objRaw = { '_id': 'info.rawdata', 'type': 'state', 'common': { 'role': 'value', 'name': 'Telegram raw data if parser failed', 'type': 'string', 'read': true, 'write': false, 'def': false }, 'native': {} }; await this.objectHelper.createObject(objRaw._id, objRaw); if (typeof this.config.aeskeys !== 'undefined') { this.config.aeskeys.forEach((item) => { if (item.key === 'UNKNOWN') { this.needsKey.push(item.id); } }); } this.receivers = this.getReceivers(); this.setConnected(false); const port = (typeof this.config.serialPort !== 'undefined' ? this.config.serialPort : '/dev/ttyWMBUS'); // @ts-ignore const baud = (typeof this.config.serialBaudRate !== 'undefined' ? parseInt(this.config.serialBaudRate) : 9600); const mode = (typeof this.config.wmbusMode !== 'undefined' ? this.config.wmbusMode : 'T'); const options = this.createOptions(port, baud); const receiverClass = this.getReceiverClass(this.config.deviceType); const receiverName = this.getReceiverName(this.config.deviceType); const receiverJs = `.${receiverPath}${receiverClass}`; try { if (fs.existsSync(receiverJs)) { ReceiverModule = require(receiverJs); this.receiver = new ReceiverModule(options, mode, this.dataReceived.bind(this), this.serialError.bind(this), { debug: this.log.debug, info: this.log.info, error: this.log.error }); this.log.debug(`Created device of type: ${receiverName}`); this.decoder = new WMBusDecoder({ debug: this.log.debug, error: this.log.error }, this.config.drCacheEnabled); await this.receiver.init(); this.setConnected(true); } else { this.log.error(`No or unknown adapter type selected! ${receiverClass}`); } } catch (e) { this.log.error(`Error opening serial port ${port} with baudrate ${baud}`); // @ts-ignore this.log.error(e); this.setConnected(false); return; } } createOptions(port, baud) { const matches = String(port).match(/tcp:\/\/([^:]+):(\d+)/); if (matches) { return { isTcp: true, host: matches[1], port: parseInt(matches[2]) }; } else { return { path: port, baudRate: baud }; } } getReceivers() { const receivers = {}; const json = JSON.parse(fs.readFileSync(`${this.adapterDir}${receiverPath}receiver.json`, 'utf8')); Object.keys(json).forEach((item) => { if (fs.existsSync(this.adapterDir + receiverPath + json[item].js)) { receivers[item] = json[item]; } }); return receivers; } getReceiverClass(type) { if (type in this.receivers) { return this.receivers[type].js; } return type; } getReceiverName(type) { if (type in this.receivers) { return this.receivers[type].name; } return type; } getReceiverJs(type) { if (type in this.receivers) { return `.${receiverPath}${this.receivers[type].js}`; } return `.${receiverPath}${type}`; } serialError(err) { this.log.error(`Serialport error: ${err.message}`); this.setConnected(false); this.onUnload(); } setConnected(isConnected) { if (this.connected !== isConnected) { this.connected = isConnected; this.setState('info.connection', this.connected, true, err => { if (err) { this.log.error(`Can not update connected state: ${err}`); } else { this.log.debug(`connected set to ${this.connected}`); } }); } } async dataReceived(data) { this.setConnected(true); const id = this.parseID(data.rawData); if (data.rawData.length < 11) { if (id == 'ERR-XXXXXXXX') { this.log.info(`Invalid telegram received? ${data.rawData.toString('hex')}`); } else { this.log.debug(`Beacon of device: ${id}`); } return; } // check block list if (this.isDeviceBlocked(id)) { this.log.debug(`Device is blocked: ${id}`); return; } // look for AES key let key = this.getAesKey(id); if (typeof key !== 'undefined') { if (key === 'UNKNOWN') { key = undefined; } else { this.log.debug(`Found AES key: ${key}`); } } if (!this.decoder) { this.log.error('wmbus decoder has not be initialized!'); return; } this.decoder.parse(data.rawData, data.containsCrc, key, data.frameType, (err, result) => { if (err) { this.log.debug(`Parser failed to parse telegram from device ${id}`); if (this.config.autoBlocklist) { this.checkAutoBlocklist(id); } this.setState('info.rawdata', data.rawData.toString('hex'), true); this.checkWrongKey(id, err.code); return; } this.resetAutoBlocklist(id); const deviceId = `${result.deviceInformation.Manufacturer}-${result.deviceInformation.Id}`; this.updateDevice(deviceId, result); }); } parseID(data) { if (data.length < 8) { return 'ERR-XXXXXXXX'; } const hexId = data.readUInt16LE(2); const manufacturer = String.fromCharCode((hexId >> 10) + 64) + String.fromCharCode(((hexId >> 5) & 0x1f) + 64) + String.fromCharCode((hexId & 0x1f) + 64); return `${manufacturer}-${data.readUInt32LE(4).toString(16).padStart(8, '0')}`; } isDeviceBlocked(id) { if ((typeof this.config.blacklist === 'undefined') || !this.config.blacklist.length) { return false; } const found = this.config.blacklist.find((item) => { if (typeof item.id === 'undefined') { return false; } else { return item.id == id; } }); if (typeof found !== 'undefined') { // found return true; } return false; } checkAutoBlocklist(id) { const i = this.failedDevices.findIndex((dev) => dev.id == id); if (i === -1) { this.failedDevices.push({ id: id, count: 1 }); } else { this.failedDevices[i].count++; if (this.failedDevices[i].count >= 10) { this.config.blacklist.push({ id: id }); this.log.warn(`Device ${id} is now blocked until adapter restart!`); } } } resetAutoBlocklist(id) { const i = this.failedDevices.findIndex((dev) => dev.id == id); if ((i !== -1) && (this.failedDevices[i].count)) { this.failedDevices[i].count = 0; } } checkWrongKey(id, code) { if (code == 9) { // ERR_NO_AESKEY if (typeof this.needsKey.find((el) => el == id) === 'undefined') { this.needsKey.push(id); } } } getAesKey(id) { if ((typeof this.config.aeskeys === 'undefined') || !this.config.aeskeys.length) { return undefined; } // look for perfect match const perfectMatch = this.config.aeskeys.find((item) => { if (typeof item.id === 'undefined') { return false; } else { return item.id == id; } }); if (typeof perfectMatch !== 'undefined') { // found return perfectMatch.key; } // which device names start with our id const candidates = this.config.aeskeys.filter((item) => { if (typeof item.id === 'undefined') { return false; } else { return id.startsWith(item.id); } }); if (candidates.length == 1) { // only 1 match - take it return candidates[0].key; } if (candidates.length > 1) { // more than one, find the best let len = candidates[0].id.length; let pos = 0; for (let i = 1; i < candidates.length; i++) { if (candidates[i].id.length > len) { len = candidates[i].id.length; pos = i; } } return candidates[pos].key; } return undefined; } async updateDevice(deviceId, result) { if (this.createdDevices.indexOf(deviceId) == -1) { await this.createDeviceObjects(deviceId, result); } this.updateDeviceStates(deviceId, result); } async createDeviceObjects(deviceId, data) { this.log.debug(`Creating device: ${deviceId}`); await this.objectHelper.createDeviceOrChannel('device', deviceId); await this.objectHelper.createDeviceOrChannel('channel', `${deviceId}.data`); await this.objectHelper.createDeviceOrChannel('channel', `${deviceId}.info`); for (const key of Object.keys(data.deviceInformation)) { await this.objectHelper.createInfoState(deviceId, key); } await this.objectHelper.createInfoState(deviceId, 'Updated'); for (const item of data.dataRecord) { await this.objectHelper.createDataState(deviceId, item); } this.createdDevices.push(deviceId); } async updateDeviceStates(deviceId, data) { this.log.debug(`Updating device: ${deviceId}`); for (const key of Object.keys(data.deviceInformation)) { const name = `${deviceId}.info.${key}`; if ((typeof this.stateValues[name] === 'undefined') || (this.stateValues[name] !== data.deviceInformation[key])) { this.stateValues[name] = data.deviceInformation[key]; await this.objectHelper.updateState(name, data.deviceInformation[key]); } } await this.objectHelper.updateState(`${deviceId}.info.Updated`, Math.floor(Date.now() / 1000)); for (const item of data.dataRecord) { const name = `${deviceId}.data.${item.number}-${item.storageNo}-${item.type}`; if (this.config.alwaysUpdate || (typeof this.stateValues[name] === 'undefined') || (this.stateValues[name] !== item.value)) { this.stateValues[name] = item.value; let val = item.value; if (this.config.forcekWh) { if (item.unit == 'Wh') { val = val / 1000; } else if (item.unit == 'J') { val = val / 3600000; } } this.log.debug(`Value ${name}: ${val}`); await this.objectHelper.updateState(name, val); } } } onMessage(obj) { if (typeof obj === 'object' && obj.callback) { switch (obj.command) { case 'listUart': if (SerialPort) { SerialPort.list().then( ports => { this.log.info('List of port: ' + JSON.stringify(ports)); this.sendTo(obj.from, obj.command, ports, obj.callback); }, err => this.log.error(JSON.stringify(err)) ); } else { this.log.warn('Module serialport is not available'); this.sendTo(obj.from, obj.command, [{ comName: 'Not available' }], obj.callback); } break; case 'listReceiver': this.sendTo(obj.from, obj.command, this.receivers, obj.callback); break; case 'needsKey': this.sendTo(obj.from, obj.command, this.needsKey, obj.callback); break; } } } } if (require.main !== module) { module.exports = (options) => new WirelessMbus(options); } else { new WirelessMbus(); }