Added Autodiscovery and refactoring

This commit is contained in:
2024-08-17 11:16:31 +02:00
parent 06da8f5b97
commit 891c326195
8 changed files with 256 additions and 61 deletions

View File

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

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

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

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 { 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);
});

View File

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