Skip to content

Commit

Permalink
Implemented non-interactive Map via MQTT
Browse files Browse the repository at this point in the history
  • Loading branch information
Hypfer committed Mar 23, 2019
1 parent 19738af commit 9d44048
Show file tree
Hide file tree
Showing 8 changed files with 643 additions and 702 deletions.
27 changes: 0 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ The Valetudo binary however does not so if you are upgrading your firmware, you
* Carpet Mode
* Cleaning History
* Volume Control
* [DEPRECATED] Image API which provides Map PNGs

### Screenshots:

Expand All @@ -52,32 +51,6 @@ The Valetudo binary however does not so if you are upgrading your firmware, you
2. Replace the binary `/usr/local/bin/valetudo` by the new one (make sure to `chmod +x` it).
3. Start the service again `service valetudo start`.

### [DEPRECATED] Remote API
The map is available as a PNG using this API.
It can be found at:
`YOUR.VACUUM.ROBOT.IP/api/remote/map`

The current implementation allows you to _grab_/**set**:
* The recent generated map as PNG
* The map contains the 2D contour and configurable:
- `drawPath` [**true**|undefined]
- `drawCharger` [**true**|undefined]
- `drawRobot` [**true**|undefined]
- `border` (in px around the map, will be scaled as well!), default: **2**
- `doCropping` (for debug purpose) [**true**|undefined]
- `scale` [1,..n], default: **4**
* The position of the charger (`charger[X,Y]`: position in px to overlay on the generated image)
* The position of the robot (`robot[X,Y]`: position in px to overlay on the generated image)
* The angle of the robot defined by the last path (`robotAngle`: angle in [0-360] of the robot; 0: oriented to the top, 90: oriented to the right)

A fully configured call would look like that:
`YOUR.VACUUM.ROBOT.IP/api/remote/map?drawRobot=false&drawCharger=true&scale=5&border=3&doCropping=true&drawPath=true`.
The json answer would look like the following:
```json
{"scale":5, "border":15, "doCropping":true, "drawPath":true, "mapsrc":"/maps/2018-08-19_10-43-50.png", "drawCharger":true, "charger":[65,620], "drawRobot":false, "robot":[51,625], "robotAngle":90}
```
If a parameter has not been defined/set, the default value will be used (marked bold above).

### Misc
Valetudo does not feature access controls and I'm not planning on adding it since I trust my local network.
You could just put a reverse proxy with authentication in front of it if you really need it.
Expand Down
2 changes: 1 addition & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,7 @@
if (remainingShownCount>0) {
loadingBarSettingsCleaningHistory.setAttribute("indeterminate", "indeterminate");
var historyTimestamp = historyArray.shift(); //array is sorted with latest items in the beginning
fn.requestWithPayload("/api/clean_record", JSON.stringify({recordId : historyTimestamp}) , "PUT", function (err, res) {
fn.requestWithPayload("api/clean_record", JSON.stringify({recordId : historyTimestamp}) , "PUT", function (err, res) {
loadingBarSettingsCleaningHistory.removeAttribute("indeterminate");
if (err) {
ons.notification.toast(err, { buttonLabel: 'Dismiss', timeout: 1500 });
Expand Down
16 changes: 13 additions & 3 deletions lib/Configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ const Configuration = function() {
"mqtt" : {
enabled: false,
identifier: "rockrobo",
broker_url: "mqtt://foobar.example"
broker_url: "mqtt://user:[email protected]",
mapSettings: {
drawPath: true,
drawCharger: true,
drawRobot: true,
border: 2,
doCropping: true,
scale: 4
},
mapUpdateInterval: 30000
}
};

Expand All @@ -24,9 +33,10 @@ const Configuration = function() {
try {
this.settings = JSON.parse(fs.readFileSync(this.location));
} catch(e) {
//TODO: handle this
console.error("Invalid configuration file!");
throw e;
console.log("Writing new file using defaults");

this.persist();
}
} else {
console.log("No configuration file present. Creating one at:", this.location);
Expand Down
143 changes: 102 additions & 41 deletions lib/MqttClient.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mqtt = require("mqtt");
const Tools = require("./Tools");


const COMMANDS = {
Expand Down Expand Up @@ -27,23 +28,74 @@ const FAN_SPEEDS = {
* @param options.vacuum {Vacuum}
* @param options.brokerURL {string}
* @param options.identifier {string}
* @param options.mapSettings {object}
* @param options.mapUpdateInterval {number}
* @constructor
*/
const MqttClient = function(options) {
this.vacuum = options.vacuum;
this.brokerURL = options.brokerURL;
this.identifier = options.identifier || "rockrobo";
this.mapSettings = options.mapSettings || {};
this.mapUpdateInterval = options.mapUpdateInterval || 30000;

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"
map: "valetudo/" + this.identifier + "/map",
homeassistant_autoconf_vacuum: "homeassistant/vacuum/valetudo_" + this.identifier + "/config",
homeassistant_autoconf_map: "homeassistant/camera/valetudo_" + this.identifier + "_map/config"
};

this.autoconf_payloads = {
vacuum: {
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
},
map: {
name: this.identifier + "_map",
unique_id: this.identifier + "_map",
topic: this.topics.map
}
};

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

MqttClient.prototype.connect = function() {
Expand All @@ -56,45 +108,15 @@ MqttClient.prototype.connect = function() {
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

}), {

this.client.publish(this.topics.homeassistant_autoconf_vacuum, JSON.stringify(this.autoconf_payloads.vacuum), {
retain: true
});

this.client.publish(this.topics.homeassistant_autoconf_map, JSON.stringify(this.autoconf_payloads.map), {
retain: true
})
});

} else {
//TODO: needs more error handling
console.error(err);
Expand All @@ -108,6 +130,45 @@ MqttClient.prototype.connect = function() {
}
};

MqttClient.prototype.updateMapTopic = function() {
if(this.mapUpdateTimeout) {
clearTimeout(this.mapUpdateTimeout);
}

if(this.client && this.client.connected === true) {
Tools.FIND_LATEST_MAP((err, data) => {
if(!err) {
Tools.DRAW_MAP_PNG({
mapData: data.mapData,
log: data.log,
settings: this.mapSettings
}, (err, buf) => {
if(!err) {
this.client.publish(this.topics.map, buf, {retain: true});
this.mapUpdateTimeout = setTimeout(() => {
this.updateMapTopic()
}, this.mapUpdateInterval);
} else {
console.error(err);
this.mapUpdateTimeout = setTimeout(() => {
this.updateMapTopic()
}, this.mapUpdateInterval);
}
})
} else {
console.error(err);
this.mapUpdateTimeout = setTimeout(() => {
this.updateMapTopic()
}, this.mapUpdateInterval);
}
});
} else {
this.mapUpdateTimeout = setTimeout(() => {
this.updateMapTopic()
}, this.mapUpdateInterval);
}
};

MqttClient.prototype.updateStateTopic = function() {
if(this.stateUpdateTimeout) {
clearTimeout(this.stateUpdateTimeout);
Expand Down Expand Up @@ -144,7 +205,7 @@ MqttClient.prototype.updateStateTopic = function() {
response.error = res.human_error;
}

this.client.publish(this.topics.state, JSON.stringify(response));
this.client.publish(this.topics.state, JSON.stringify(response), {retain: true});
this.stateUpdateTimeout = setTimeout(() => {
this.updateStateTopic()
}, 10000);
Expand Down Expand Up @@ -227,4 +288,4 @@ MqttClient.prototype.handleCommand = function(topic, command) {

};

module.exports = MqttClient;
module.exports = MqttClient;
Loading

4 comments on commit 9d44048

@reaper7
Copy link
Contributor

@reaper7 reaper7 commented on 9d44048 Mar 23, 2019

Choose a reason for hiding this comment

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

mqtt png map works very well with Hass!

@reaper7
Copy link
Contributor

@reaper7 reaper7 commented on 9d44048 Mar 23, 2019

Choose a reason for hiding this comment

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

one request regarding mqtt:
Please add to configuration two types of prefixes:

  • first for general vacuum commands and states like: MYDEFINEDPREFIX + "valetudo/" + this.identifier + "/command"

    if MYDEFINEDPREFIX not defined then: "valetudo/" + this.identifier + "/command"

  • second for hass autoconf, instead "homeassistant/vacuum/valetudo_" + this.identifier + "/config"
    something like MYDISCOVERYPREFIX + "/vacuum/valetudo_" + this.identifier + "/config"

    if MYDISCOVERYPREFIX not defined then: "homeassistant/vacuum/valetudo_" + this.identifier + "/config"

it seems to me that this is necessary because hass has the option of configuring the discovery prefix,
and not everyone has the default value of "homeassistant" in their configuration (like me :) )

@Hypfer
Copy link
Owner Author

@Hypfer Hypfer commented on 9d44048 Mar 23, 2019

Choose a reason for hiding this comment

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

What would be the benefit of those?

@reaper7
Copy link
Contributor

@reaper7 reaper7 commented on 9d44048 Mar 23, 2019

Choose a reason for hiding this comment

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

for second:
https://www.home-assistant.io/docs/mqtt/discovery/#discovery_prefix

I have my own prefix defined in all existing devices,
the vacuum cleaner should also be able to adapt this prefix

Please sign in to comment.