Skip to content

Commit

Permalink
#43 MQTT Support + Minor cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Mar 22, 2019
1 parent 8a36b11 commit 69be9df
Show file tree
Hide file tree
Showing 13 changed files with 670 additions and 351 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ install:
- npm install

script:
- "./node_modules/.bin/pkg --targets latest-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron ."
- "./node_modules/.bin/pkg --targets latest-linux-armv7 --no-bytecode --options max-old-space-size=72 --public-packages=exif-parser,omggif,trim,prettycron,mqtt ."

deploy:
provider: releases
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ It runs directly on the vacuum and requires no cloud connection whatsoever.
* Go-To
* Zoned Cleanup
* Configure Timers
* MQTT
* MQTT HomeAssistant Autodiscovery
* Start/Stop/Pause Robot
* Find Robot/Send robot to charging dock
* Power settings
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
const Valetudo = require("./Valetudo");
const Valetudo = require("./lib/Valetudo");
new Valetudo();
71 changes: 71 additions & 0 deletions lib/Configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const fs = require("fs");
const Tools = require("./Tools");
const path = require("path");

/**
* @constructor
*/
const Configuration = function() {
this.location = process.env.VALETUDO_CONFIG ? process.env.VALETUDO_CONFIG : "/mnt/data/valetudo/config.json";
this.settings = {
"spots": [],
"areas": [],
"mqtt" : {
enabled: false,
identifier: "rockrobo",
broker_url: "mqtt://foobar.example"
}
};

/* load an existing configuration file. if it is not present, create it using the default configuration */
if(fs.existsSync(this.location)) {
console.log("Loading configuration file:", this.location);

try {
this.settings = JSON.parse(fs.readFileSync(this.location));
} catch(e) {
//TODO: handle this
console.error("Invalid configuration file!");
throw e;
}
} else {
console.log("No configuration file present. Creating one at:", this.location);
Tools.MK_DIR_PATH(path.dirname(this.location));
this.persist();
}
};


/**
*
* @param key {string}
* @returns {*}
*/
Configuration.prototype.get = function(key) {
return this.settings[key];
};

Configuration.prototype.getAll = function() {
return this.settings;
};

/**
*
* @param key {string}
* @param val {string}
*/
Configuration.prototype.set = function(key, val) {
this.settings[key] = val;

this.persist();
};

Configuration.prototype.persist = function() {
fs.writeFile(this.location, JSON.stringify(this.settings, null, 2), (err) => {
if (err) {
console.error(err);
}
});
};

module.exports = Configuration;
230 changes: 230 additions & 0 deletions lib/MqttClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
const mqtt = require("mqtt");


const COMMANDS = {
turn_on: "turn_on",
return_to_base: "return_to_base",
stop: "stop",
clean_spot: "clean_spot",
locate: "locate",
start_pause: "start_pause",
set_fan_speed: "set_fan_speed"
};

//TODO: since this is also displayed in the UI it should be moved somewhere else
const FAN_SPEEDS = {
min: 38,
medium: 60,
high: 75,
max: 100
};



/**
*
* @param options {object}
* @param options.vacuum {Vacuum}
* @param options.brokerURL {string}
* @param options.identifier {string}
* @constructor
*/
const MqttClient = function(options) {
this.vacuum = options.vacuum;
this.brokerURL = options.brokerURL;
this.identifier = options.identifier || "rockrobo";

this.topics = {
command: "valetudo/" + this.identifier + "/command",
set_fan_speed: "valetudo/" + this.identifier + "/set_fan_speed",
send_command: "valetudo/" + this.identifier + "/custom_command",
state: "valetudo/" + this.identifier + "/state",
homeassistant_autoconf: "homeassistant/vacuum/valetudo_" + this.identifier + "/config"
};

this.connect();
this.updateStateTopic();
};

MqttClient.prototype.connect = function() {
if(!this.client || (this.client && this.client.connected === false && this.client.reconnecting === false)) {
this.client = mqtt.connect(this.brokerURL);

this.client.on("connect", () => {
this.client.subscribe([
this.topics.command,
this.topics.set_fan_speed
], err => {
if(!err) {
this.client.publish(this.topics.homeassistant_autoconf, JSON.stringify({
name: this.identifier,
supported_features: [
"turn_on",
"pause",
"stop",
"return_home",
"battery",
"status",
"locate",
"clean_spot",
"fan_speed",
"send_command"
],
command_topic: this.topics.command,
battery_level_topic: this.topics.state,
battery_level_template: "{{ value_json.battery_level }}",
charging_topic: this.topics.state,
charging_template: "{{value_json.charging}}",
cleaning_topic: this.topics.state,
cleaning_template: "{{value_json.cleaning}}",
docked_topic: this.topics.state,
docked_template: "{{value_json.docked}}",
error_topic: this.topics.state,
error_template: "{{value_json.error}}",
fan_speed_topic: this.topics.state,
fan_speed_template: "{{ value_json.fan_speed }}",
set_fan_speed_topic: this.topics.set_fan_speed,
fan_speed_list: [
"min",
"medium",
"high",
"max"
],
send_command_topic: this.topics.send_command

}), {
retain: true
})
} else {
//TODO: needs more error handling
console.error(err);
}
});
});

this.client.on("message", (topic, message) => {
this.handleCommand(topic, message.toString());
})
}
};

MqttClient.prototype.updateStateTopic = function() {
if(this.stateUpdateTimeout) {
clearTimeout(this.stateUpdateTimeout);
}

if(this.client && this.client.connected === true) {
this.vacuum.getCurrentStatus((err, res) => {
if(!err) {
var response = {};

response.battery_level = res.battery;
response.docked = [8,14].indexOf(res.state) !== -1;
response.cleaning = res.in_cleaning === 1;
response.charging = res.state === 8;

switch(res.fan_power) {
case FAN_SPEEDS.min:
response.fan_speed = "min";
break;
case FAN_SPEEDS.medium:
response.fan_speed = "medium";
break;
case FAN_SPEEDS.high:
response.fan_speed = "high";
break;
case FAN_SPEEDS.max:
response.fan_speed = "max";
break;
default:
response.fan_speed = res.fan_power;
}

if(res.error_code !== 0) {
response.error = res.human_error;
}

this.client.publish(this.topics.state, JSON.stringify(response));
this.stateUpdateTimeout = setTimeout(() => {
this.updateStateTopic()
}, 10000);
} else {
console.error(err);
this.stateUpdateTimeout = setTimeout(() => {
this.updateStateTopic()
}, 30000);
}
})
} else {
this.stateUpdateTimeout = setTimeout(() => {
this.updateStateTopic()
}, 30000);
}
};

/**
* @param topic {string}
* @param command {string}
*/
MqttClient.prototype.handleCommand = function(topic, command) {
var param;
if(topic === this.topics.set_fan_speed) {
param = command;
command = COMMANDS.set_fan_speed;
}

switch(command) { //TODO: error handling
case COMMANDS.turn_on:
this.vacuum.startCleaning(() => {
this.updateStateTopic();
});
break;
case COMMANDS.stop:
this.vacuum.stopCleaning(() => {
this.updateStateTopic();
});
break;
case COMMANDS.return_to_base:
this.vacuum.stopCleaning(() => {
this.vacuum.driveHome(() => {
this.updateStateTopic();
});
});
break;
case COMMANDS.clean_spot:
this.vacuum.spotClean(() => {
this.updateStateTopic();
});
break;
case COMMANDS.locate:
this.vacuum.findRobot(() => {
this.updateStateTopic();
});
break;
case COMMANDS.start_pause:
this.vacuum.getCurrentStatus((err, res) => {
if(!err) {
if(res.in_cleaning === 1 && [5,11,17].indexOf(res.state) !== -1) {
this.vacuum.pauseCleaning(() => {
this.updateStateTopic();
});
} else {
this.vacuum.startCleaning(() => {
this.updateStateTopic();
});
}
}
});
break;
case COMMANDS.set_fan_speed:
this.vacuum.setFanSpeed(FAN_SPEEDS[param], () => {
this.updateStateTopic();
});
break;
default:
this.updateStateTopic();
}

};

module.exports = MqttClient;
16 changes: 16 additions & 0 deletions lib/Tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const fs = require("fs");
const path = require("path");

const Tools = {
MK_DIR_PATH : function(filepath) {
var dirname = path.dirname(filepath);
if (!fs.existsSync(dirname)) {
Tools.MK_DIR_PATH(dirname);
}
if (!fs.existsSync(filepath)) {
fs.mkdirSync(filepath);
}
}
};

module.exports = Tools;
22 changes: 15 additions & 7 deletions Valetudo.js → lib/Valetudo.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const fs = require("fs");
const Vacuum = require("./miio/Vacuum");
const Webserver = require("./webserver/WebServer");
const MqttClient = require("./MqttClient");
const Configuration = require("./Configuration");

const defaultConfigFileLocation = "/mnt/data/valetudo/config.json"

const Valetudo = function() {
const Valetudo = function() {
this.configuration = new Configuration();
this.address = process.env.VAC_ADDRESS ? process.env.VAC_ADDRESS : "127.0.0.1";

if(process.env.VAC_TOKEN) {
Expand All @@ -17,7 +18,6 @@ const Valetudo = function() {

this.webPort = process.env.VAC_WEBPORT ? parseInt(process.env.VAC_WEBPORT) : 80;

this.configFileLocation = process.env.VALETUDO_CONFIG ? process.env.VALETUDO_CONFIG : defaultConfigFileLocation;

this.vacuum = new Vacuum({
ip: this.address,
Expand All @@ -27,11 +27,19 @@ const Valetudo = function() {
this.webserver = new Webserver({
vacuum: this.vacuum,
port: this.webPort,
configFileLocation: this.configFileLocation
})
};
configuration: this.configuration
});


if(this.configuration.get("mqtt") && this.configuration.get("mqtt").enabled === true) {
this.mqttClient = new MqttClient({
vacuum: this.vacuum,
brokerURL: this.configuration.get("mqtt").broker_url,
identifier: this.configuration.get("mqtt").identifier
});
}
};

Valetudo.NATIVE_TOKEN_PROVIDER = function() {
const token = fs.readFileSync("/mnt/data/miio/device.token");
if(token && token.length >= 16) {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

7 comments on commit 69be9df

@reaper7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mqtt implementation supports login?
my broker is secured with a username and password...

@Hypfer
Copy link
Owner Author

@Hypfer Hypfer commented on 69be9df Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reaper7 I guess mqtt://user:password@host should work, although I haven't tested that. Can you try and report back?

@reaper7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to check this solution ASAP

Where I should define the server?

@Hypfer
Copy link
Owner Author

@Hypfer Hypfer commented on 69be9df Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config file is stored at /mnt/data/valetudo/config.json

If you're upgrading take a look at the release page for the new settings you need to add to that file

@oechslein
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mqtt://user:password@host works with my secured MQTT server (iobroker).

@Hypfer
Copy link
Owner Author

@Hypfer Hypfer commented on 69be9df Mar 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!
I've added a hint to the release notes

@reaper7
Copy link
Contributor

@reaper7 reaper7 commented on 69be9df Mar 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can also confirm, it works without a problem (mosquitto) 👍

Please sign in to comment.