diff --git a/lib/extension/bridge.js b/lib/extension/bridge.js index 7ecb2930..da29fd9e 100644 --- a/lib/extension/bridge.js +++ b/lib/extension/bridge.js @@ -3,6 +3,7 @@ const utils = require('../util/utils'); const Extension = require('./extension'); const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const settings = require('../util/settings'); +const Transport = require('winston-transport'); const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); @@ -10,6 +11,7 @@ class Bridge extends Extension { constructor(zigbee, mqtt, state, publishEntityState, eventBus) { super(zigbee, mqtt, state, publishEntityState, eventBus); this.lastJoinedDeviceIeeeAddr = null; + this.setupMQTTLogging(); this.requestLookup = { 'device/options': this.deviceOptions.bind(this), @@ -39,6 +41,19 @@ class Bridge extends Extension { await this.publishGroups(); } + setupMQTTLogging() { + const mqtt = this.mqtt; + class EventTransport extends Transport { + log(info, callback) { + const payload = JSON.stringify({message: info.message, level: info.level}); + mqtt.publish(`bridge/logging`, payload, {}, settings.get().mqtt.base_topic, true); + callback(); + } + } + + logger.add(new EventTransport()); + } + async onMQTTMessage(topic, message) { const match = topic.match(requestRegex); if (match && this.requestLookup[match[1].toLowerCase()]) { diff --git a/lib/mqtt.js b/lib/mqtt.js index 1294eda7..920c3747 100644 --- a/lib/mqtt.js +++ b/lib/mqtt.js @@ -99,17 +99,21 @@ class MQTT extends events.EventEmitter { return this.client && !this.client.reconnecting; } - async publish(topic, payload, options, base=settings.get().mqtt.base_topic) { + async publish(topic, payload, options, base=settings.get().mqtt.base_topic, skipLog=false) { topic = `${base}/${topic}`; options = {qos: 0, retain: false, ...options}; if (!this.isConnected()) { - logger.error(`Not connected to MQTT server!`); - logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`); + if (!skipLog) { + logger.error(`Not connected to MQTT server!`); + logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`); + } return; } - logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`); + if (!skipLog) { + logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`); + } return new Promise((resolve) => { this.client.publish(topic, payload, options, () => resolve()); diff --git a/test/bridge.test.js b/test/bridge.test.js index a4d1338c..aaf7f0aa 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -5,7 +5,6 @@ const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); const flushPromises = () => new Promise(setImmediate); -const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const {coordinator, bulb, unsupported} = zigbeeHerdsman.devices; zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); @@ -16,6 +15,7 @@ describe('Bridge', () => { let controller; beforeEach(async () => { + MQTT.mock.reconnecting = false; data.writeDefaultConfiguration(); settings._reRead(); settings.set(['advanced', 'legacy_api'], false); @@ -23,6 +23,7 @@ describe('Bridge', () => { data.writeDefaultState(); logger.info.mockClear(); logger.warn.mockClear(); + logger.setTransportsEnabled(false); MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.bulb; device.removeFromDatabase.mockClear(); @@ -51,7 +52,34 @@ describe('Bridge', () => { ); }); + it('Should log to MQTT', async () => { + logger.setTransportsEnabled(true); + MQTT.publish.mockClear(); + logger.info.mockClear(); + logger.info("this is a test"); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/logging', + JSON.stringify({message: 'this is a test', level: 'info'}), + { retain: false, qos: 0 }, + expect.any(Function) + ); + expect(logger.info).toHaveBeenCalledTimes(1); + }); + + it('Shouldnt log to MQTT when not connected', async () => { + logger.setTransportsEnabled(true); + MQTT.mock.reconnecting = true; + MQTT.publish.mockClear(); + logger.info.mockClear(); + logger.error.mockClear(); + logger.info("this is a test"); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledTimes(0); + }); + it('Should publish devices on startup', async () => { + logger.setTransportsEnabled(true); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', JSON.stringify([{"id":1,"friendly_name":"group_1","members":[]},{"id":15071,"friendly_name":"group_tradfri_remote","members":[]},{"id":99,"friendly_name":99,"members":[]},{"id":11,"friendly_name":"group_with_tradfri","members":[]},{"id":2,"friendly_name":"group_2","members":[]}]), diff --git a/test/stub/logger.js b/test/stub/logger.js index 8709b27e..f1fd9760 100644 --- a/test/stub/logger.js +++ b/test/stub/logger.js @@ -1,14 +1,26 @@ let level = 'info'; +const transports = []; + +let transportsEnabled = false; +const callTransports = (level, message) => { + if (transportsEnabled) { + for (const transport of transports) { + transport.log({level, message}, () => {}); + } + } +} + const mock = { - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - trace: jest.fn(), + info: jest.fn().mockImplementation((msg) => callTransports('info', msg)), + warn: jest.fn().mockImplementation((msg) => callTransports('warn', msg)), + error: jest.fn().mockImplementation((msg) => callTransports('error', msg)), + debug: jest.fn().mockImplementation((msg) => callTransports('debug', msg)), cleanup: jest.fn(), + add: (transport) => transports.push(transport), setLevel: (newLevel) => {level = newLevel}, getLevel: () => level, + setTransportsEnabled: (value) => {transportsEnabled = value}, }; jest.mock('../../lib/util/logger', () => (mock)); diff --git a/test/stub/mqtt.js b/test/stub/mqtt.js index 7588c464..4ecf5e9c 100644 --- a/test/stub/mqtt.js +++ b/test/stub/mqtt.js @@ -4,6 +4,7 @@ const mock = { publish: jest.fn().mockImplementation((topic, payload, options, cb) => cb()), end: jest.fn(), subscribe: jest.fn(), + reconnecting: false, on: (type, handler) => { if (type === 'connect') { handler(); @@ -20,5 +21,5 @@ jest.mock('mqtt', () => { }); module.exports = { - events, ...mock, connect: mockConnect, + events, ...mock, connect: mockConnect, mock, }; \ No newline at end of file