This commit is contained in:
Dennis Gunia
2024-08-03 11:53:00 +02:00
commit bffa7df214
9 changed files with 292 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
config.yaml
node_modules/
package-lock.json

10
config-example.yaml Normal file
View File

@@ -0,0 +1,10 @@
mqtt:
host: "127.0.0.1"
port: 1883
username: "username"
password: "password"
lineaplug:
- id: "sqy709956ply4inkx6ac87"
key: "xxxxxxxxxxxxxxxx"
topic: "tuya/device1"
refresh: 30 #refresh intervall in s

31
extractkeys.md Normal file
View File

@@ -0,0 +1,31 @@
# Extract Keys from iO.e app
## Install iO.e in an android emulator
I've used waydroid (https://waydro.id/#install). I've installed the iO.e app and signed in with my account. The app downloaded all relevant data and stored it in its application data directory on the android device or in this case: the emulator.
## Extract the relevant file
Next step is finding the relevant file.
```
WAYDROID_ROOT_FS="$HOME/.local/share/waydroid/data/data/com.iosmart.app/shared_prefs"
sudo bash -c "grep s_home_data $WAYDROID_ROOT_FS/preferences_global_key* > /tmp/dump.raw"
sudo chown $USER /tmp/dump.raw
ls -l /tmp/dump.raw
```
Now we have the raw data extracted. Next step is converting this to a readable json file.
First we have to remove the url encoding for quotes
```
sed -i 's/\&quot\;/\"/gm' /tmp/dump.raw
```
Next, remove the string at the beginning and end of the file:
```
sed -i 's/^.*deviceRespBeen/\{\"data/' /tmp/dump.raw
sed -i 's/<\/string>$//' /tmp/dump.raw
```
Now we can format the file with `jq`
```
cat /tmp/dump.raw | jq
```
This file contains an array of all configured tuya devices including `id`,`key`and `schema`information for decoding dps values. Use it as you wish.

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "luminea2mqtt",
"version": "1.0.0",
"description": "Luminea to mqtt bridge",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Dennis Gunia",
"license": "ISC",
"dependencies": {
"log4js": "^6.9.1",
"mqtt": "^5.9.1",
"tuyapi": "github:codetheweb/tuyapi",
"yaml": "^2.5.0"
}
}

0
readme.md Normal file
View File

3
src/discovery.js Normal file
View File

@@ -0,0 +1,3 @@
export function scanLumineaDevice(){
}

61
src/index.js Normal file
View File

@@ -0,0 +1,61 @@
const log4js = require('log4js');
const logger = log4js.getLogger();
const loggerInit = log4js.getLogger("initializer");
logger.level = 'info';
loggerInit.level = 'info';
async function main() {
const mqtt = require("mqtt");
const YAML = require('yaml')
const fs = require('fs')
const Lineplug = require('./lineaplug')
loggerInit.info("Read configfile")
const file = fs.readFileSync('./config.yaml', 'utf8')
const config = YAML.parse(file)
const mqttserver = `mqtt://${config.mqtt.host}:${config.mqtt.port}`
let client = mqtt.connect(mqttserver, {
// Clean session
connectTimeout: 1000,
// Authentication
username: config.mqtt.username,
password: config.mqtt.password,
clientId: "test",
debug: true,
});
client.on('connect', function () {
loggerInit.info(`Connected to ${mqttserver}`)
// Subscribe to a topic
})
client.on("reconnect", () => {
loggerInit.info("reconnecting!")
})
client.stream.on('error', (err) => {
loggerInit.error('error', err);
client.end()
});
let devices = []
config.lineaplug.forEach((device) => {
loggerInit.info(`Setup device ${device.id} Type: Lineplug`)
const newdev = new Lineplug(device,client)
devices.push(newdev)
})
process.on('SIGINT',() =>{
for (let device of devices){
device.disconnect()
}
process.exit(2);
});
}
main()

162
src/lineaplug.js Normal file
View File

@@ -0,0 +1,162 @@
const TuyaDevice = require('tuyapi');
const log4js = require('log4js');
class Lineplug {
constructor(deviceconfig, mqtt) {
const loggername = deviceconfig.id ? deviceconfig.id : "undef device"
this.logger = log4js.getLogger(loggername);
if (!deviceconfig.id) {
this.logger.error("missing attribute 'id' in device config")
return
}
if (!deviceconfig.key) {
this.logger.error("missing attribute 'key' in device config")
return
}
if (!deviceconfig.topic) {
this.logger.error("missing attribute 'topic' in device config")
return
}
this.mqtt = mqtt
this.topicname = deviceconfig.topic
this.deviceid = deviceconfig.id
this.topic_get = `${deviceconfig.topic}/get`
this.topic_set = `${deviceconfig.topic}/set`
this.intervall = deviceconfig.refresh ? deviceconfig.refresh * 1000 : 10000
this.lastdata = {
voltage: 0,
current: 0,
power: 0,
work: 0,
status: false,
cycle_time: 0,
countdown_1: 0,
random_time: 0,
}
try {
this.device = new TuyaDevice({
//ip: "10.110.0.126",
id: deviceconfig.id,
key: deviceconfig.key,
issueRefreshOnConnect: true,
ip: deviceconfig.ip,
})
this.device.find().then(el => {
this.device.connect().then(el => {
this.logger.info(`Connected to tuya id: ${this.deviceid} @ ${this.topicname}`)
this.startWatcher()
this.mqtt.subscribe(this.topic_set, (err) => {
if (err) {
this.logger.error(`Cannot subscribe to ${this.topic_set}`)
} else {
this.logger.info(`Subscribed to ${this.topic_set}`)
}
});
}).catch(error => {
this.logger.error(`Cannot connect to ${this.deviceid}`)
this.logger.error(error.message)
});
})
} catch (error) {
this.logger.error(`Cannot connect to ${this.deviceid}`)
this.logger.error(error.message)
}
}
startWatcher() {
// monitoring loop
this.timer = setInterval(() => this.device.refresh(), 10000)
this.logger.info(`Started watcher for id: ${this.deviceid}`)
this.device.on('data', data => {
this.processData(data)
});
this.device.on('dp-refresh', data => {
this.processData(data)
});
// monitor queue
this.mqtt.on('message', (topic, message) => {
// message is Buffer
//if (topic == this.topicname){
let payload = message.toString()
try {
const jsonpayload = JSON.parse(payload)
if (jsonpayload.value != undefined) {
this.logger.info(`Change status to ${jsonpayload.value}`)
this.device.set({ set: jsonpayload.value }).then(el => {
this.device.refresh()
})
}
} catch (error) {
this.logger.warn(`Error parsing malformatted JSON message via mqtt`)
this.logger.trace(payload)
this.logger.trace(error)
}
// }
})
}
processData(data) {
//console.log(data)
if (!data.dps) {
return
}
const dps = data.dps
const updatedValues = Object.keys(dps)
let changed = false
if (updatedValues.includes('20')) {
this.lastdata.voltage = dps['20'] / 10
changed = true
}
if (updatedValues.includes('18')) {
this.lastdata.current = dps['18'] / 1000
changed = true
}
if (updatedValues.includes('19')) {
this.lastdata.power = dps['19'] / 10
changed = true
}
if (updatedValues.includes('17')) {
this.lastdata.power = dps['17'] / 100
changed = true
}
if (updatedValues.includes('9')) {
this.lastdata.countdown_1 = dps['9']
changed = true
}
if (updatedValues.includes('41')) {
this.lastdata.cycle_time = dps['41']
changed = true
}
if (updatedValues.includes('42')) {
this.lastdata.random_time = dps['42']
changed = true
}
if (updatedValues.includes('1')) {
this.lastdata.status = dps['1']
changed = true
this.mqtt.publish(this.topic_get, JSON.stringify({
value: this.lastdata.status
}))
}
if (changed) {
this.mqtt.publish(this.topicname, JSON.stringify(this.lastdata))
}
}
disconnect() {
clearInterval(this.timer)
this.device.disconnect()
this.logger.info(`Disconnected for id: ${this.deviceid}`)
}
}
module.exports = Lineplug

5
src/logging.js Normal file
View File

@@ -0,0 +1,5 @@
const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = 'info';
module.exports.def = logger