mirror of
https://github.com/lehanspb/tuya-mqtt.git
synced 2025-12-16 17:54:36 +00:00
Use devices.json for discovery
* Usese devices.json from "tuya-cli wizard" for device discovery, no topic config required * Uses device discovery by default, but can manually edit devices.json to add IP address, protocol version, or change * Creates short topic name using just "friendly name" or "id".
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuya-mqtt",
|
||||
"version": "2.1.0",
|
||||
"version": "3.0.0-beta1",
|
||||
"description": "Control Tuya devices locally via MQTT",
|
||||
"homepage": "https://github.com/TheAgentK/tuya-mqtt#readme",
|
||||
"main": "tuya-mqtt.js",
|
||||
@@ -16,7 +16,8 @@
|
||||
"color-convert": "^2.0.1",
|
||||
"debug": "^4.1.1",
|
||||
"mqtt": "^4.2.1",
|
||||
"tuyapi": "^5.3.1"
|
||||
"tuyapi": "^5.3.1",
|
||||
"json5": "^2.1.3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -18,14 +18,12 @@ var TuyaDevice = (function () {
|
||||
var devices = [];
|
||||
var events = {};
|
||||
|
||||
function checkExisiting(id) {
|
||||
function checkExisiting(options) {
|
||||
var existing = false;
|
||||
// Check for existing instance
|
||||
devices.forEach(device => {
|
||||
if (device.hasOwnProperty("options")) {
|
||||
if (id === device.options.id) {
|
||||
existing = device;
|
||||
}
|
||||
if (device.topicLevel == options.topicLevel) {
|
||||
existing = device;
|
||||
}
|
||||
});
|
||||
return existing;
|
||||
@@ -42,10 +40,11 @@ var TuyaDevice = (function () {
|
||||
});
|
||||
}
|
||||
|
||||
function TuyaDevice(options, callback) {
|
||||
function TuyaDevice(options) {
|
||||
var device = this;
|
||||
// Check for existing instance
|
||||
if (existing = checkExisiting(options.id)) {
|
||||
|
||||
// Check for existing instance by matching topicLevel value
|
||||
if (existing = checkExisiting(options)) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
status: "connected",
|
||||
@@ -58,32 +57,70 @@ var TuyaDevice = (function () {
|
||||
return new TuyaDevice(options);
|
||||
}
|
||||
|
||||
options.type = options.type || undefined;
|
||||
|
||||
this.type = options.type;
|
||||
this.options = options;
|
||||
|
||||
Object.defineProperty(this, 'device', {
|
||||
value: new TuyAPI(JSON.parse(JSON.stringify(this.options)))
|
||||
});
|
||||
if (this.options.name) {
|
||||
this.topicLevel = this.options.name.toLowerCase().replace(/ /g,"_");
|
||||
} else {
|
||||
this.topicLevel = this.options.id;
|
||||
}
|
||||
|
||||
this.device.on('data', data => {
|
||||
if (typeof data == "string") {
|
||||
debugError('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ""));
|
||||
} else {
|
||||
debug('Data from device:', data);
|
||||
device.triggerAll('data', data);
|
||||
if (!this.options.ip) {
|
||||
const findOptions = {
|
||||
id: this.options.id,
|
||||
key: "yGAdlopoPVldABfn"
|
||||
}
|
||||
});
|
||||
findDevice = new TuyAPI(JSON.parse(JSON.stringify(findOptions)))
|
||||
findDevice.find().then(() => {
|
||||
this.options.ip = findDevice.device.ip
|
||||
this.options.version = findDevice.device.version
|
||||
Object.defineProperty(this, 'device', {
|
||||
value: new TuyAPI(JSON.parse(JSON.stringify(this.options)))
|
||||
});
|
||||
|
||||
this.device.on('data', data => {
|
||||
if (typeof data == "string") {
|
||||
debugError('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ""));
|
||||
} else {
|
||||
debug('Data from device:', data);
|
||||
device.triggerAll('data', data);
|
||||
}
|
||||
});
|
||||
|
||||
devices.push(this);
|
||||
|
||||
// Find device on network
|
||||
debug("Search device in network");
|
||||
this.find().then(() => {
|
||||
debug("Device found in network");
|
||||
// Connect to device
|
||||
this.device.connect();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.defineProperty(this, 'device', {
|
||||
value: new TuyAPI(JSON.parse(JSON.stringify(this.options)))
|
||||
});
|
||||
|
||||
devices.push(this);
|
||||
// Find device on network
|
||||
debug("Search device in network");
|
||||
this.find().then(() => {
|
||||
debug("Device found in network");
|
||||
// Connect to device
|
||||
this.device.connect();
|
||||
});
|
||||
this.device.on('data', data => {
|
||||
if (typeof data == "string") {
|
||||
debugError('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, ""));
|
||||
} else {
|
||||
debug('Data from device:', data);
|
||||
device.triggerAll('data', data);
|
||||
}
|
||||
});
|
||||
|
||||
devices.push(this);
|
||||
|
||||
// Find device on network
|
||||
debug("Search device in network");
|
||||
this.find().then(() => {
|
||||
debug("Device found in network");
|
||||
// Connect to device
|
||||
this.device.connect();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return promis to wait for connection
|
||||
|
||||
376
tuya-mqtt.js
376
tuya-mqtt.js
@@ -1,4 +1,6 @@
|
||||
const fs = require('fs')
|
||||
const mqtt = require('mqtt');
|
||||
const json5 = require('json5');
|
||||
const TuyaDevice = require('./tuya-device');
|
||||
const debug = require('debug')('TuyAPI:mqtt');
|
||||
const debugColor = require('debug')('TuyAPI:mqtt:color');
|
||||
@@ -13,10 +15,6 @@ function bmap(istate) {
|
||||
return istate ? 'ON' : "OFF";
|
||||
}
|
||||
|
||||
function boolToString(istate) {
|
||||
return istate ? 'true' : "false";
|
||||
}
|
||||
|
||||
/*
|
||||
* execute function on topic message
|
||||
*/
|
||||
@@ -30,103 +28,13 @@ function IsJsonString(text) {
|
||||
}
|
||||
|
||||
/**
|
||||
* check mqtt-topic string for old notation with included device type
|
||||
* @param {String} topic
|
||||
*/
|
||||
function checkTopicNotation(_topic) {
|
||||
var topic = _topic.split("/");
|
||||
var type = topic[1];
|
||||
var result = (type == "socket" || type == "lightbulb" || type == "ver3.1" || type == "ver3.3");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* get action from mqtt-topic string
|
||||
* @param {String} topic
|
||||
* @returns {String} action type
|
||||
*/
|
||||
function getActionFromTopic(_topic) {
|
||||
var topic = _topic.split("/");
|
||||
|
||||
if (checkTopicNotation(_topic)) {
|
||||
return topic[5];
|
||||
} else {
|
||||
return topic[4];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get device informations from mqtt-topic string
|
||||
* @param {String} topic
|
||||
* @returns {String} object.id
|
||||
* @returns {String} object.key
|
||||
* @returns {String} object.ip
|
||||
*/
|
||||
function getDeviceFromTopic(_topic) {
|
||||
var topic = _topic.split("/");
|
||||
|
||||
if (checkTopicNotation(_topic)) {
|
||||
// When there are 5 topic levels
|
||||
// topic 2 is id, and topic 3 is key
|
||||
var options = {
|
||||
id: topic[2],
|
||||
key: topic[3]
|
||||
};
|
||||
|
||||
// 4th topic is IP address or "discover" keyword
|
||||
if (topic[4] !== "discover") {
|
||||
options.ip = topic[4]
|
||||
// If IP is manually specified check if topic 1
|
||||
// is protocol version and set accordingly
|
||||
if (topic[1] == "ver3.3") {
|
||||
options.version = "3.3"
|
||||
} else if (topic[1] == "ver3.1") {
|
||||
options.version = "3.1"
|
||||
} else {
|
||||
// If topic is not version then it's device type
|
||||
// Not used anymore but still supported for legacy setups
|
||||
options.type = topic[1]
|
||||
};
|
||||
};
|
||||
|
||||
return options;
|
||||
} else {
|
||||
// When there are 4 topic levels
|
||||
// topic 1 is id, topic 2 is key
|
||||
var options = {
|
||||
id: topic[1],
|
||||
key: topic[2]
|
||||
};
|
||||
|
||||
// If topic 3 is not discover assume it is IP address
|
||||
// Todo: Validate it is an IP address
|
||||
if (topic[3] !== "discover") {
|
||||
options.ip = topic[3]
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get command from mqtt - topic string
|
||||
* converts simple commands to TuyAPI JSON commands
|
||||
* @param {String} topic
|
||||
* get command from mqtt message
|
||||
* converts message to TuyAPI JSON commands
|
||||
* @param {String} message
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getCommandFromTopic(_topic, _message) {
|
||||
var topic = _topic.split("/");
|
||||
var command = null;
|
||||
|
||||
if (checkTopicNotation(_topic)) {
|
||||
command = topic[6];
|
||||
} else {
|
||||
command = topic[5];
|
||||
}
|
||||
|
||||
if (command == null) {
|
||||
command = _message;
|
||||
}
|
||||
function getCommandFromMessage(_message) {
|
||||
let command = _message
|
||||
|
||||
if (command != "1" && command != "0" && IsJsonString(command)) {
|
||||
debug("command is JSON");
|
||||
@@ -142,7 +50,6 @@ function getCommandFromTopic(_topic, _message) {
|
||||
command = command.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -154,30 +61,12 @@ function getCommandFromTopic(_topic, _message) {
|
||||
function publishStatus(device, status) {
|
||||
if (mqtt_client.connected == true) {
|
||||
try {
|
||||
var type = device.type;
|
||||
var tuyaID = device.options.id;
|
||||
var tuyaKey = device.options.key;
|
||||
var tuyaIP = device.options.ip;
|
||||
|
||||
if (typeof tuyaIP == "undefined") {
|
||||
tuyaIP = "discover"
|
||||
}
|
||||
|
||||
if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") {
|
||||
var topic = CONFIG.topic;
|
||||
if (typeof type != "undefined") {
|
||||
topic += type + "/";
|
||||
}
|
||||
topic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state";
|
||||
|
||||
mqtt_client.publish(topic, status, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
debugTuya("mqtt status updated to:" + topic + " -> " + status);
|
||||
} else {
|
||||
debugTuya("mqtt status not updated");
|
||||
}
|
||||
let topic = CONFIG.topic + device.topicLevel + "/state";
|
||||
mqtt_client.publish(topic, status, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
debugTuya("mqtt status updated to:" + topic + " -> " + status);
|
||||
} catch (e) {
|
||||
debugError(e);
|
||||
}
|
||||
@@ -196,42 +85,25 @@ function publishColorState(device, state) {
|
||||
function publishDPS(device, dps) {
|
||||
if (mqtt_client.connected == true) {
|
||||
try {
|
||||
var type = device.type;
|
||||
var tuyaID = device.options.id;
|
||||
var tuyaKey = device.options.key;
|
||||
var tuyaIP = device.options.ip;
|
||||
const baseTopic = CONFIG.topic + device.topicLevel + "/dps";
|
||||
|
||||
if (typeof tuyaIP == "undefined") {
|
||||
tuyaIP = "discover"
|
||||
}
|
||||
const topic = baseTopic;
|
||||
const data = JSON.stringify(dps);
|
||||
debugTuya("mqtt dps updated to:" + topic + " -> ", data);
|
||||
mqtt_client.publish(topic, data, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
|
||||
if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") {
|
||||
var baseTopic = CONFIG.topic;
|
||||
if (typeof type != "undefined") {
|
||||
baseTopic += type + "/";
|
||||
}
|
||||
baseTopic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps";
|
||||
|
||||
var topic = baseTopic;
|
||||
var data = JSON.stringify(dps);
|
||||
debugTuya("mqtt dps updated to:" + topic + " -> ", data);
|
||||
Object.keys(dps).forEach(function (key) {
|
||||
const topic = baseTopic + "/" + key;
|
||||
const data = JSON.stringify(dps[key]);
|
||||
debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data);
|
||||
mqtt_client.publish(topic, data, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
|
||||
Object.keys(dps).forEach(function (key) {
|
||||
var topic = baseTopic + "/" + key;
|
||||
var data = JSON.stringify(dps[key]);
|
||||
debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data);
|
||||
mqtt_client.publish(topic, data, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
});
|
||||
} else {
|
||||
debugTuya("mqtt dps not updated");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugError(e);
|
||||
}
|
||||
@@ -269,78 +141,116 @@ function sleep(sec) {
|
||||
return new Promise(res => setTimeout(res, sec*1000));
|
||||
}
|
||||
|
||||
function initTuyaDevices(tuyaDevices) {
|
||||
for (const tuyaDevice of tuyaDevices) {
|
||||
let options = {
|
||||
id: tuyaDevice.id,
|
||||
key: tuyaDevice.key
|
||||
}
|
||||
if (tuyaDevice.name) { options.name = tuyaDevice.name }
|
||||
if (tuyaDevice.ip) {
|
||||
options.ip = tuyaDevice.ip
|
||||
if (tuyaDevice.version) {
|
||||
options.version = tuyaDevice.version
|
||||
} else {
|
||||
version = "3.1"
|
||||
}
|
||||
}
|
||||
new TuyaDevice(options);
|
||||
}
|
||||
}
|
||||
|
||||
// Main code function
|
||||
const main = async() => {
|
||||
let tuyaDevices
|
||||
|
||||
try {
|
||||
CONFIG = require("./config");
|
||||
} catch (e) {
|
||||
console.error("Configuration file not found")
|
||||
debugError(e)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
CONFIG = require("./config");
|
||||
} catch (e) {
|
||||
console.error("Configuration file not found!")
|
||||
debugError(e)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (typeof CONFIG.qos == "undefined") {
|
||||
CONFIG.qos = 2;
|
||||
}
|
||||
if (typeof CONFIG.retain == "undefined") {
|
||||
CONFIG.retain = false;
|
||||
}
|
||||
if (typeof CONFIG.qos == "undefined") {
|
||||
CONFIG.qos = 2;
|
||||
}
|
||||
if (typeof CONFIG.retain == "undefined") {
|
||||
CONFIG.retain = false;
|
||||
}
|
||||
|
||||
mqtt_client = mqtt.connect({
|
||||
host: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
username: CONFIG.mqtt_user,
|
||||
password: CONFIG.mqtt_pass,
|
||||
});
|
||||
try {
|
||||
tuyaDevices = fs.readFileSync('./devices.json', 'utf8');
|
||||
tuyaDevices = json5.parse(tuyaDevices)
|
||||
} catch (e) {
|
||||
console.error("Devices file not found!")
|
||||
debugError(e)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
mqtt_client.on('connect', function (err) {
|
||||
debug("Connection established to MQTT server");
|
||||
var topic = CONFIG.topic + '#';
|
||||
mqtt_client.subscribe(topic, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
});
|
||||
if (!tuyaDevices.length) {
|
||||
console.error("No devices found in devices file!")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
mqtt_client.on("reconnect", function (error) {
|
||||
if (mqtt_client.connected) {
|
||||
debug("Connection to MQTT server lost. Attempting to reconnect...");
|
||||
} else {
|
||||
debug("Unable to connect to MQTT server");
|
||||
}
|
||||
});
|
||||
mqtt_client = mqtt.connect({
|
||||
host: CONFIG.host,
|
||||
port: CONFIG.port,
|
||||
username: CONFIG.mqtt_user,
|
||||
password: CONFIG.mqtt_pass,
|
||||
});
|
||||
|
||||
mqtt_client.on("error", function (error) {
|
||||
debug("Unable to connect to MQTT server", error);
|
||||
});
|
||||
mqtt_client.on('connect', function (err) {
|
||||
debug("Connection established to MQTT server");
|
||||
let topic = CONFIG.topic + '#';
|
||||
mqtt_client.subscribe(topic, {
|
||||
retain: CONFIG.retain,
|
||||
qos: CONFIG.qos
|
||||
});
|
||||
initTuyaDevices(tuyaDevices)
|
||||
});
|
||||
|
||||
mqtt_client.on('message', function (topic, message) {
|
||||
try {
|
||||
message = message.toString();
|
||||
var action = getActionFromTopic(topic);
|
||||
var options = getDeviceFromTopic(topic);
|
||||
mqtt_client.on("reconnect", function (error) {
|
||||
if (mqtt_client.connected) {
|
||||
debug("Connection to MQTT server lost. Attempting to reconnect...");
|
||||
} else {
|
||||
debug("Unable to connect to MQTT server");
|
||||
}
|
||||
});
|
||||
|
||||
debug("receive settings", JSON.stringify({
|
||||
topic: topic,
|
||||
action: action,
|
||||
message: message,
|
||||
options: options
|
||||
}));
|
||||
mqtt_client.on("error", function (error) {
|
||||
debug("Unable to connect to MQTT server", error);
|
||||
});
|
||||
|
||||
var device = new TuyaDevice(options);
|
||||
mqtt_client.on('message', function (topic, message) {
|
||||
try {
|
||||
message = message.toString();
|
||||
splitTopic = topic.split("/");
|
||||
let action = splitTopic[2];
|
||||
let options = {
|
||||
topicLevel: splitTopic[1]
|
||||
}
|
||||
|
||||
device.then(function (params) {
|
||||
var device = params.device;
|
||||
debug("receive settings", JSON.stringify({
|
||||
topic: topic,
|
||||
action: action,
|
||||
message: message,
|
||||
topicLevel: options.topicLevel
|
||||
}));
|
||||
|
||||
switch (action) {
|
||||
case "command":
|
||||
var command = getCommandFromTopic(topic, message);
|
||||
debug("Received command: ", command);
|
||||
if (command == "toggle") {
|
||||
device.switch(command).then((data) => {
|
||||
debug("Set device status completed: ", data);
|
||||
});
|
||||
// Uses device topic level to find matching device
|
||||
var device = new TuyaDevice(options);
|
||||
|
||||
device.then(function (params) {
|
||||
var device = params.device;
|
||||
switch (action) {
|
||||
case "command":
|
||||
var command = getCommandFromMessage(message);
|
||||
debug("Received command: ", command);
|
||||
if (command == "toggle") {
|
||||
device.switch(command).then((data) => {
|
||||
debug("Set device status completed: ", data);
|
||||
});
|
||||
}
|
||||
if (command.schema === true) {
|
||||
// Trigger device schema update to update state
|
||||
@@ -348,27 +258,27 @@ const main = async() => {
|
||||
});
|
||||
debug("Get schema status command complete");
|
||||
} else {
|
||||
device.set(command).then((data) => {
|
||||
debug("Set device status completed: ", data);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "color":
|
||||
var color = message.toLowerCase();
|
||||
debugColor("Set color: ", color);
|
||||
device.setColor(color).then((data) => {
|
||||
debug("Set device color completed: ", data);
|
||||
});
|
||||
break;
|
||||
}
|
||||
device.set(command).then((data) => {
|
||||
debug("Set device status completed: ", data);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "color":
|
||||
var color = message.toLowerCase();
|
||||
debugColor("Set color: ", color);
|
||||
device.setColor(color).then((data) => {
|
||||
debug("Set device color completed: ", data);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
}).catch((err) => {
|
||||
debugError(err);
|
||||
});
|
||||
} catch (e) {
|
||||
debugError(e);
|
||||
}
|
||||
});
|
||||
}).catch((err) => {
|
||||
debugError(err);
|
||||
});
|
||||
} catch (e) {
|
||||
debugError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Call the main code
|
||||
|
||||
Reference in New Issue
Block a user