3 Commits
1.0.0 ... 1.1.0

8 changed files with 271 additions and 75 deletions

View File

@@ -4,10 +4,14 @@ mqtt:
username: "username" username: "username"
password: "password" password: "password"
clientid: "test" clientid: "test"
prefix: "luminea2mqtt"
autodiscover:
enabled: true
topic: homeassistant
devices: devices:
- id: "sqy709956ply4inkx6ac87" - id: "sqy709956ply4inkx6ac87"
key: "xxxxxxxxxxxxxxxx" key: "xxxxxxxxxxxxxxxx"
topic: "tuya/device1" friendlyname: "device1"
refresh: 30 refresh: 30
reconnect: 10 reconnect: 10
type: luminea_nx_4458 type: luminea_nx_4458

View File

@@ -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 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 ## Supported devices
At the moment: At the moment:
* luminea nx-4458 * luminea nx-4458
@@ -66,20 +72,31 @@ mqtt:
username: "username" username: "username"
password: "password" password: "password"
clientid: "test" 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. 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 ### Device config
``` ```
devices: devices:
- id: "sqy709956ply4inkx6ac87" - id: "sqy709956ply4inkx6ac87"
key: "xxxxxxxxxxxxxxxx" key: "xxxxxxxxxxxxxxxx"
topic: "tuya/device1" friendlyname: "device1"
refresh: 30 #refresh intervall in s refresh: 30 #refresh intervall in s
type: luminea_nx_4458 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. * `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 * `refresh`: refresh intervall in s
* `type`: name of device class. Class files are located in `./src/modules` * `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 * `startWatcher` is called after the connection is established
* `stopWatcher` is called after disconnect (stop timers, unsubscribe mqtt,...) * `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: These variables are available:
* `this.mqtt` reference to the mqtt client * `this.mqtt` reference to the mqtt client
* `this.topicname` String: topic prefix * `this.topicname` String: topic prefix

59
src/autodiscover.js Normal file
View File

@@ -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)
})
})
}

20
src/config.js Normal file
View File

@@ -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

View File

@@ -1,6 +1,8 @@
const TuyaDevice = require('tuyapi'); const TuyaDevice = require('tuyapi');
const log4js = require('log4js'); const log4js = require('log4js');
const autodiscover = require('./autodiscover')
const configldr = require('./config')
class DeviceBase { class DeviceBase {
constructor(deviceconfig, mqtt) { constructor(deviceconfig, mqtt) {
@@ -17,19 +19,19 @@ class DeviceBase {
this.logger.error("missing attribute 'key' in device config") this.logger.error("missing attribute 'key' in device config")
return return
} }
if (!deviceconfig.topic) {
this.logger.error("missing attribute 'topic' in device config")
return
}
// define device vars // define device vars
this.mqtt = mqtt this.deviceid = deviceconfig.id // tuya device id
this.topicname = deviceconfig.topic this.devicekey = deviceconfig.key // tuya device key
this.deviceid = deviceconfig.id
this.devicekey = deviceconfig.key
this.topic_get = `${deviceconfig.topic}/get` this.mqtt = mqtt // pointer to mqtt client
this.topic_set = `${deviceconfig.topic}/set` // name to be displayed. If not set, default to prefixed tuya client id
this.topic_state = `${deviceconfig.topic}/state` 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 (all) : ${this.topicname}`)
this.logger.debug(`use topic (get) : ${this.topic_get}`) 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() { reconnect() {
this.device.find().then(el => { this.device.find().then(el => {
if (el) { if (el) {
this.device.connect().catch(el => { this.device.connect().catch(el => {
console.log(this.device)
this.logger.debug("Reconnect failed: device offline") this.logger.debug("Reconnect failed: device offline")
}) })
} else { } else {

64
src/devicemanager.js Normal file
View File

@@ -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

View File

@@ -3,10 +3,15 @@ const logger = log4js.getLogger();
const loggerInit = log4js.getLogger("initializer"); const loggerInit = log4js.getLogger("initializer");
const { Command } = require('commander'); const { Command } = require('commander');
const program = new Command(); const program = new Command();
const configldr = require('./config')
const autodiscover = require('./autodiscover')
const mqtt = require("mqtt");
const DeviceManager = require('./devicemanager')
logger.level = 'info'; logger.level = 'info';
loggerInit.level = 'info'; loggerInit.level = 'info';
program program
.name('luminea2mqtt') .name('luminea2mqtt')
.version('1.0.0') .version('1.0.0')
@@ -14,39 +19,31 @@ program
.parse(process.argv); .parse(process.argv);
async function main() { async function main() {
const mqtt = require("mqtt");
const YAML = require('yaml')
const fs = require('fs')
loggerInit.info("Read configfile")
const options = program.opts(); const options = program.opts();
loggerInit.info(`User config from ${options.config}`) 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)
let deviceManager = new DeviceManager()
const mqttserver = `mqtt://${config.mqtt.host}:${config.mqtt.port}`
const mqttserver = `mqtt://${configldr.config.mqtt.host}:${configldr.config.mqtt.port}`
let client = mqtt.connect(mqttserver, { let client = mqtt.connect(mqttserver, {
// Clean session // Clean session
connectTimeout: 1000, connectTimeout: 1000,
// Authentication // Authentication
username: config.mqtt.username, username: configldr.config.mqtt.username,
password: config.mqtt.password, password: configldr.config.mqtt.password,
clientId: config.mqtt.clientid, clientId: configldr.config.mqtt.clientid,
debug: true, 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 () { client.on('connect', function () {
loggerInit.info(`Connected to ${mqttserver}`) loggerInit.info(`Connected to ${mqttserver}`)
// Subscribe to a topic deviceManager.connect()
}) })
client.on("reconnect", () => { client.on("reconnect", () => {
loggerInit.info(`Try reconnect to ${mqttserver}`) loggerInit.info(`Try reconnect to ${mqttserver}`)
@@ -57,32 +54,8 @@ async function main() {
client.end() 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', () => { process.on('SIGINT', () => {
for (let device of devices) { deviceManager.disconnect()
device.disconnect()
}
process.exit(2); process.exit(2);
}); });

View File

@@ -19,6 +19,52 @@ class Lineplug extends DeviceBase {
countdown_1: 0, countdown_1: 0,
random_time: 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() { startWatcher() {
@@ -38,26 +84,27 @@ class Lineplug extends DeviceBase {
// monitor queue // monitor queue
this.mqtt.on('message', (topic, message) => { this.mqtt.on('message', (topic, message) => {
// message is Buffer // message is Buffer
let payload = message.toString() if (topic == this.topic_set) { // verify that the topic is correct
this.logger.debug(`input ${topic}: ${payload}`) let payload = message.toString()
this.logger.debug(`input ${topic}: ${payload}`)
try { try {
const jsonpayload = JSON.parse(payload) const jsonpayload = JSON.parse(payload)
if (jsonpayload.value != undefined) { if (jsonpayload.value != undefined) {
this.logger.info(`Change status to ${jsonpayload.value}`) this.logger.info(`Change status to ${jsonpayload.value}`)
this.device.set({ set: jsonpayload.value }).then(el => { this.device.set({ set: jsonpayload.value }).then(el => {
this.device.refresh() this.device.refresh()
}) })
}
} catch (error) {
this.logger.warn(`Error parsing malformatted JSON message via mqtt`)
this.logger.trace(payload)
this.logger.trace(error)
} }
} catch (error) {
this.logger.warn(`Error parsing malformatted JSON message via mqtt`)
this.logger.trace(payload)
this.logger.trace(error)
} }
}) })
} }
stopWatcher(){ stopWatcher() {
clearInterval(this.timer) clearInterval(this.timer)
} }