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:
tsightler
2020-09-21 00:23:31 -04:00
parent b0549bf392
commit 670a87e9fb
3 changed files with 212 additions and 264 deletions

View File

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

View File

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

View File

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