7 Commits

Author SHA1 Message Date
5fe96315d0 add device name 2024-08-18 11:22:08 +02:00
1118c308ee added model and manufacturer 2024-08-17 13:45:26 +02:00
0f60f9a4c5 added schema file 2024-08-17 13:44:51 +02:00
9b94c01dfe changed loglevel 2024-08-17 11:18:23 +02:00
cb2777de84 Merge branch 'master' of https://github.com/dennis9819/luminea2mqtt 2024-08-17 11:17:55 +02:00
891c326195 Added Autodiscovery and refactoring 2024-08-17 11:16:31 +02:00
6a2c15500d fix topic filter 2024-08-14 17:44:12 +02:00
9 changed files with 408 additions and 76 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

66
src/autodiscover.js Normal file
View File

@@ -0,0 +1,66 @@
const log4js = require('log4js');
const logger = log4js.getLogger("autodiscover");
const configldr = require('./config')
logger.level = 'info';
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 (device,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_${device.deviceid}`
Object.keys(config).forEach(component =>{
let items = config[component]
Object.keys(items).forEach((item,ix) =>{
let mqtt_topic = `${configldr.config.autodiscover.topic}/${component}/${device.deviceid}/${item}/config`
let temp_data = JSON.parse(JSON.stringify(config[component][item]))
temp_data.unique_id = `${device.deviceid}_${item}_luminea2mqtt`
temp_data.object_id = `${device.friendlyname}_${item}`
if (component == "switch"){
if (items.length > 1){
temp_data.name = `${device.friendlyname}_${ix}`
}else{
temp_data.name = `${device.friendlyname}`
}
}
temp_data.origin = {
name : "luminea2mqtt",
support_url: "https://github.com/dennis9819/luminea2mqtt"
}
temp_data.device = {
identifiers : [
unique_identifier
],
name: device.friendlyname,
manufacturer: device.manufacturer ? device.manufacturer : "Unkown",
model: device.model ? device.model : "Unkown",
//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,6 +94,12 @@ class DeviceBase {
}); });
} }
pushAutodiscover(config){
if (configldr.config.autodiscover.enabled){
autodiscover.publishDevice(this,config)
}
}
reconnect() { reconnect() {
this.device.find().then(el => { this.device.find().then(el => {
if (el) { if (el) {

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

@@ -9,6 +9,8 @@ const DeviceBase = require('../devicebase')
class Lineplug extends DeviceBase { class Lineplug extends DeviceBase {
init() { init() {
this.manufacturer = "Luminea"
this.model = "NX-4458"
this.lastdata = { this.lastdata = {
voltage: 0, voltage: 0,
current: 0, current: 0,
@@ -19,6 +21,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 +86,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)
} }
@@ -82,7 +131,7 @@ class Lineplug extends DeviceBase {
changed = true changed = true
} }
if (updatedValues.includes('17')) { if (updatedValues.includes('17')) {
this.lastdata.power = dps['17'] / 100 this.lastdata.work = dps['17'] / 100
changed = true changed = true
} }
if (updatedValues.includes('9')) { if (updatedValues.includes('9')) {

View File

@@ -0,0 +1,128 @@
{
"1": {
"code": "switch_1",
"id": "1",
"mode": "rw",
"name": "开关1",
"property": "{\"type\":\"bool\"}",
"schemaType": "bool",
"type": "obj"
},
"17": {
"code": "add_ele",
"id": "17",
"mode": "rw",
"name": "增加电量",
"property": "{\"max\":50000,\"min\":0,\"scale\":3,\"step\":100,\"type\":\"value\",\"unit\":\"kwh\"}",
"schemaType": "value",
"type": "obj"
},
"18": {
"code": "cur_current",
"id": "18",
"mode": "ro",
"name": "当前电流",
"property": "{\"max\":30000,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\",\"unit\":\"mA\"}",
"schemaType": "value",
"type": "obj"
},
"19": {
"code": "cur_power",
"id": "19",
"mode": "ro",
"name": "当前功率",
"property": "{\"max\":50000,\"min\":0,\"scale\":1,\"step\":1,\"type\":\"value\",\"unit\":\"W\"}",
"schemaType": "value",
"type": "obj"
},
"20": {
"code": "cur_voltage",
"id": "20",
"mode": "ro",
"name": "当前电压",
"property": "{\"max\":5000,\"min\":0,\"scale\":1,\"step\":1,\"type\":\"value\",\"unit\":\"V\"}",
"schemaType": "value",
"type": "obj"
},
"21": {
"code": "test_bit",
"id": "21",
"mode": "ro",
"name": "产测结果位",
"property": "{\"max\":5,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\"}",
"schemaType": "value",
"type": "obj"
},
"22": {
"code": "voltage_coe",
"id": "22",
"mode": "ro",
"name": "电压校准系数",
"property": "{\"max\":1000000,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\"}",
"schemaType": "value",
"type": "obj"
},
"23": {
"code": "electric_coe",
"id": "23",
"mode": "ro",
"name": "电流校准系数",
"property": "{\"max\":1000000,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\"}",
"schemaType": "value",
"type": "obj"
},
"24": {
"code": "power_coe",
"id": "24",
"mode": "ro",
"name": "功率校准系数",
"property": "{\"max\":1000000,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\"}",
"schemaType": "value",
"type": "obj"
},
"25": {
"code": "electricity_coe",
"id": "25",
"mode": "ro",
"name": "电量校准系数",
"property": "{\"max\":1000000,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\"}",
"schemaType": "value",
"type": "obj"
},
"26": {
"code": "fault",
"id": "26",
"mode": "ro",
"name": "故障告警",
"property": "{\"label\":[\"ov_cr\",\"ov_vol\",\"ov_pwr\",\"ls_cr\",\"ls_vol\",\"ls_pow\"],\"maxlen\":6,\"type\":\"bitmap\"}",
"schemaType": "bitmap",
"type": "obj"
},
"41": {
"code": "cycle_time",
"id": "41",
"mode": "rw",
"name": "循环定时",
"property": "{\"maxlen\":255,\"type\":\"string\"}",
"schemaType": "string",
"type": "obj"
},
"42": {
"code": "random_time",
"id": "42",
"mode": "rw",
"name": "随机定时",
"property": "{\"maxlen\":255,\"type\":\"string\"}",
"schemaType": "string",
"type": "obj"
},
"9": {
"code": "countdown_1",
"id": "9",
"mode": "rw",
"name": "开关1倒计时",
"property": "{\"max\":86400,\"min\":0,\"scale\":0,\"step\":1,\"type\":\"value\",\"unit\":\"s\"}",
"schemaType": "value",
"type": "obj"
}
}