diff --git a/config-example.yaml b/config-example.yaml index b095b06..973f1b3 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -4,10 +4,14 @@ mqtt: username: "username" password: "password" clientid: "test" + prefix: "luminea2mqtt" +autodiscover: + enabled: true + topic: homeassistant devices: - id: "sqy709956ply4inkx6ac87" key: "xxxxxxxxxxxxxxxx" - topic: "tuya/device1" + friendlyname: "device1" refresh: 30 reconnect: 10 type: luminea_nx_4458 diff --git a/readme.md b/readme.md index 240f494..0759379 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,12 @@ This should also work with almost all other tuya devices, provided therre is a f This bridge is mostly based on the work of: https://github.com/codetheweb/tuyapi +## Features +* Easy intigration of luminea (tuya) device into HomeAssistant +* Auto reconnect to tuya device +* Supports Homeassistant Auto-Discovery +* Easily extendable + ## Supported devices At the moment: * luminea nx-4458 @@ -66,20 +72,31 @@ mqtt: username: "username" password: "password" clientid: "test" + prefix: "luminea2mqtt" ``` These values define the port and ip of the server, as well as the credentials for this client. All values must be specified. Make sure that the `clientid` is unique. +The `prefix` is the prefix of all mqtt topics. +### Autodiscover +``` +autodiscover: + enabled: true + topic: homeassistant +``` +This enables auto discovery for home assistant. (See https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery) +* `enabled`: enables autodiscovery. +* `topic`: topic prefix for discovery messages ### Device config ``` devices: - id: "sqy709956ply4inkx6ac87" key: "xxxxxxxxxxxxxxxx" - topic: "tuya/device1" + friendlyname: "device1" refresh: 30 #refresh intervall in s type: luminea_nx_4458 ``` * `id` and `key`: Specify id an local key of tuya device. There are severeal tutorials avaliable online on how to get these keys. This depends on the app you have used to register your devices if you don't want to reset them. If you use iO.e, see `extractkeys.md` on how to get extract these values. -* `topic`: base topic for mqtt. `/get` and `/set` topics are also used for turning the switch on or off. +* `friendlyname`: The displayname of the device. Also sets the base topic for this device as following: `/{mqtt.prefix}/{friendlyname}` If this property is not set, friendlyname defaults to the `id`. `/get` and `/set` topics are also used for turning the switch on or off. * `refresh`: refresh intervall in s * `type`: name of device class. Class files are located in `./src/modules` @@ -112,6 +129,9 @@ These functions must be implementd: * `startWatcher` is called after the connection is established * `stopWatcher` is called after disconnect (stop timers, unsubscribe mqtt,...) +These functions can be implemented: +* `pushAutodiscover(deviceConfig)` is called to publish the device config + These variables are available: * `this.mqtt` reference to the mqtt client * `this.topicname` String: topic prefix diff --git a/src/autodiscover.js b/src/autodiscover.js new file mode 100644 index 0000000..7788c85 --- /dev/null +++ b/src/autodiscover.js @@ -0,0 +1,59 @@ +const log4js = require('log4js'); +const logger = log4js.getLogger("autodiscover"); +const configldr = require('./config') + +logger.level = 'debug'; +let discover_mqtt = undefined + +module.exports.setup = async (mqtt) => { + discover_mqtt = mqtt + if (!configldr.config.autodiscover.enabled) { + logger.info(`Autodiscover disabled`) + } else { + const mqtt_prefix = `${configldr.config.autodiscover.topic}` + logger.info(`Setting up autodiscover with prefix ${mqtt_prefix}`) + } +} + +module.exports.publishDevice = async (deviceid,device_name,config) => { + // deviceid: unique device id, ideally based on the tuya device id + // device_name: firendlyname displayed in the gui + // config: device config + + let unique_identifier = `luminea2mqtt_${deviceid}` + Object.keys(config).forEach(component =>{ + + let items = config[component] + Object.keys(items).forEach(item =>{ + let mqtt_topic = `${configldr.config.autodiscover.topic}/${component}/${deviceid}/${item}/config` + let temp_data = JSON.parse(JSON.stringify(config[component][item])) + temp_data.unique_id = `${deviceid}_${item}_luminea2mqtt` + temp_data.object_id = `${device_name}_${item}` + temp_data.origin = { + name : "luminea2mqtt", + support_url: "https://github.com/dennis9819/luminea2mqtt" + } + temp_data.device = { + identifiers : [ + unique_identifier + ], + name: device_name, + manufacturer: "dennisgunia", + model: "Unknown", + + //via_device: `luminea2mqtt_bridge_${configldr.config.mqtt.devenv1}` + } + + const payload_str = JSON.stringify(temp_data) + logger.debug(`publish ${mqtt_topic}: ${payload_str}`) + discover_mqtt.publish(mqtt_topic, payload_str) + }) + + + + + + }) + +} + diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..31b545c --- /dev/null +++ b/src/config.js @@ -0,0 +1,20 @@ +const log4js = require('log4js'); +const YAML = require('yaml') +const fs = require('fs') + +const loggerInit = log4js.getLogger("initializer"); +loggerInit.level = 'info'; + +module.exports.config = {} +module.exports.loadConfig = async (configfile) => { + loggerInit.info(`Read configfile ${configfile}`) + try { + const file = fs.readFileSync(configfile, 'utf8') + module.exports.config = YAML.parse(file) + } catch (error) { + loggerInit.error(`error reading config: ${error.message}`) + process.exit(10) + } +} + +module.exports.config \ No newline at end of file diff --git a/src/devicebase.js b/src/devicebase.js index 5e04740..43535a1 100644 --- a/src/devicebase.js +++ b/src/devicebase.js @@ -1,6 +1,8 @@ const TuyaDevice = require('tuyapi'); const log4js = require('log4js'); +const autodiscover = require('./autodiscover') +const configldr = require('./config') class DeviceBase { constructor(deviceconfig, mqtt) { @@ -17,19 +19,19 @@ class DeviceBase { this.logger.error("missing attribute 'key' in device config") return } - if (!deviceconfig.topic) { - this.logger.error("missing attribute 'topic' in device config") - return - } // define device vars - this.mqtt = mqtt - this.topicname = deviceconfig.topic - this.deviceid = deviceconfig.id - this.devicekey = deviceconfig.key + this.deviceid = deviceconfig.id // tuya device id + this.devicekey = deviceconfig.key // tuya device key - this.topic_get = `${deviceconfig.topic}/get` - this.topic_set = `${deviceconfig.topic}/set` - this.topic_state = `${deviceconfig.topic}/state` + this.mqtt = mqtt // pointer to mqtt client + // name to be displayed. If not set, default to prefixed tuya client id + this.friendlyname = deviceconfig.friendlyname ? deviceconfig.friendlyname : `luminea2mqtt_${this.deviceid}` + // define queue names. Prefix is set globally. Prefer friendly name. If not set, use client id + this.queue_name = deviceconfig.friendlyname ? deviceconfig.friendlyname : this.deviceid + this.topicname = `${configldr.config.mqtt.prefix}/${this.queue_name}` + this.topic_get = `${configldr.config.mqtt.prefix}/${this.queue_name}/get` + this.topic_set = `${configldr.config.mqtt.prefix}/${this.queue_name}/set` + this.topic_state = `${configldr.config.mqtt.prefix}/${this.queue_name}/state` this.logger.debug(`use topic (all) : ${this.topicname}`) this.logger.debug(`use topic (get) : ${this.topic_get}`) @@ -92,10 +94,17 @@ class DeviceBase { }); } + pushAutodiscover(config){ + if (configldr.config.autodiscover.enabled){ + autodiscover.publishDevice(this.deviceid,this.friendlyname,config) + } + } + reconnect() { this.device.find().then(el => { if (el) { this.device.connect().catch(el => { + console.log(this.device) this.logger.debug("Reconnect failed: device offline") }) } else { diff --git a/src/devicemanager.js b/src/devicemanager.js new file mode 100644 index 0000000..65e63b8 --- /dev/null +++ b/src/devicemanager.js @@ -0,0 +1,64 @@ + +const TuyaDevice = require('tuyapi'); +const log4js = require('log4js'); +const autodiscover = require('./autodiscover') +const configldr = require('./config') + +class DeviceManager { + constructor() { + // setup logger + this.devices = [] + this.logger = log4js.getLogger("devicemanager"); + this.logger.level = configldr.config.loglevel; + this.config = configldr.config + this.connected = false + + + } + + setClient(mqtt){ + this.mqtt = mqtt + } + + async connect() { + if (!this.connected) { + this.logger.info(`Setup all devices...`) + this.connected = true + if (this.config.devices) { + this.config.devices.forEach((device) => { + device.loglevel = configldr.config.loglevel; + const deviceClassFile = `./modules/${device.type}` + this.logger.info(`Setup device ${device.id}, type: ${device.type}, class:'${deviceClassFile}.js'`) + try { + const DeviceClass = require(deviceClassFile) + const newdev = new DeviceClass(device, this.mqtt) + this.devices.push(newdev) + } catch (error) { + this.logger.error(`Error initializing device class ${deviceClassFile}`); + this.logger.error(error.message); + } + + }) + } else { + this.logger.error(`Missing 'devices' in config.`) + process.exit(10) + } + } else { + this.logger.debug("Devices already connected") + } + } + + async disconnect() { + if (this.connected) { + this.connected = false + for (let device of devices) { + await device.disconnect() + } + } else { + this.logger.debug("Devices already disconnected") + } + } + +} + +module.exports = DeviceManager \ No newline at end of file diff --git a/src/index.js b/src/index.js index cc889d1..e7c12f0 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,15 @@ const logger = log4js.getLogger(); const loggerInit = log4js.getLogger("initializer"); const { Command } = require('commander'); const program = new Command(); +const configldr = require('./config') +const autodiscover = require('./autodiscover') +const mqtt = require("mqtt"); +const DeviceManager = require('./devicemanager') logger.level = 'info'; loggerInit.level = 'info'; + program .name('luminea2mqtt') .version('1.0.0') @@ -14,39 +19,31 @@ program .parse(process.argv); async function main() { - const mqtt = require("mqtt"); - const YAML = require('yaml') - const fs = require('fs') - - loggerInit.info("Read configfile") - const options = program.opts(); loggerInit.info(`User config from ${options.config}`) - let config = {} - try { - const file = fs.readFileSync(options.config, 'utf8') - config = YAML.parse(file) - } catch (error) { - loggerInit.error(`error reading config: ${error.message}`) - process.exit(10) - } + configldr.loadConfig(options.config) - - const mqttserver = `mqtt://${config.mqtt.host}:${config.mqtt.port}` + let deviceManager = new DeviceManager() + + const mqttserver = `mqtt://${configldr.config.mqtt.host}:${configldr.config.mqtt.port}` let client = mqtt.connect(mqttserver, { // Clean session connectTimeout: 1000, // Authentication - username: config.mqtt.username, - password: config.mqtt.password, - clientId: config.mqtt.clientid, + username: configldr.config.mqtt.username, + password: configldr.config.mqtt.password, + clientId: configldr.config.mqtt.clientid, debug: true, }); - loggerInit.info(`Connect to ${mqttserver}, user: ${config.mqtt.username}, clientid: ${config.mqtt.clientid}`) + + loggerInit.info(`Connect to ${mqttserver}, user: ${configldr.config.mqtt.username}, clientid: ${configldr.config.mqtt.clientid}`) + autodiscover.setup(client) + deviceManager.setClient(client) + client.on('connect', function () { loggerInit.info(`Connected to ${mqttserver}`) - // Subscribe to a topic + deviceManager.connect() }) client.on("reconnect", () => { loggerInit.info(`Try reconnect to ${mqttserver}`) @@ -57,32 +54,8 @@ async function main() { client.end() }); - let devices = [] - - if (config.devices){ - config.devices.forEach((device) => { - device.loglevel = config.loglevel - const deviceClassFile = `./modules/${device.type}` - loggerInit.info(`Setup device ${device.id}, type: ${device.type}, class:'${deviceClassFile}.js'`) - try { - const DeviceClass = require(deviceClassFile) - const newdev = new DeviceClass(device, client) - devices.push(newdev) - } catch (error) { - loggerInit.error(`Error initializing device class ${deviceClassFile}`); - loggerInit.error(error.message); - } - - }) - }else{ - loggerInit.error(`Missing 'devices' in config.`) - process.exit(10) - } - process.on('SIGINT', () => { - for (let device of devices) { - device.disconnect() - } + deviceManager.disconnect() process.exit(2); }); diff --git a/src/modules/luminea_nx_4458.js b/src/modules/luminea_nx_4458.js index 64e02ce..73e6e8d 100644 --- a/src/modules/luminea_nx_4458.js +++ b/src/modules/luminea_nx_4458.js @@ -19,6 +19,52 @@ class Lineplug extends DeviceBase { countdown_1: 0, random_time: 0, } + + this.deviceConfig = { + switch: { + switch: { + component: "switch", + command_topic: this.topic_set, + state_topic: this.topicname, + payload_on: "{\"value\": true}", + payload_off: "{\"value\": false}", + optimistic: false, + device_class: "outlet", + value_template: "{{ value_json.status }}", + state_off: false, + state_on: true, + enabled_by_default: true + }, + }, + sensor: { + voltage: { + state_topic: this.topicname, + device_class: "voltage", + state_class: "measurement", + value_template: "{{ value_json.voltage }}", + unit_of_measurement: "V", + enabled_by_default: true + }, + current: { + state_topic: this.topicname, + device_class: "current", + state_class: "measurement", + value_template: "{{ value_json.current }}", + unit_of_measurement: "A", + enabled_by_default: true + }, + power: { + state_topic: this.topicname, + device_class: "power", + state_class: "measurement", + value_template: "{{ value_json.power }}", + unit_of_measurement: "W", + enabled_by_default: true + } + } + + } + this.pushAutodiscover(this.deviceConfig) } startWatcher() { @@ -57,7 +103,7 @@ class Lineplug extends DeviceBase { }) } - stopWatcher(){ + stopWatcher() { clearInterval(this.timer) }