diff --git a/assets/misc/valetudo_home_assistant_mqtt_camera_hack.xcf b/assets/misc/valetudo_home_assistant_mqtt_camera_hack.xcf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/client/settings-mqtt.html b/client/settings-mqtt.html index 8f9a2c04467..2e5a3c8385d 100644 --- a/client/settings-mqtt.html +++ b/client/settings-mqtt.html @@ -149,10 +149,10 @@
- Base64 Encode compressed map data: + Homeassistant Map Hack:
diff --git a/client/settings-mqtt.js b/client/settings-mqtt.js index 419647e5a89..60d989c58be 100644 --- a/client/settings-mqtt.js +++ b/client/settings-mqtt.js @@ -19,7 +19,7 @@ async function updateSettingsMqttPage() { var mqttInputTopicPrefix = document.getElementById("settings-mqtt-input-topic-prefix"); var mqttInputAutoconfPrefix = document.getElementById("settings-mqtt-input-autoconf-prefix"); var mqttInputProvideMapData = document.getElementById("settings-mqtt-input-provide-map-data"); - var mqttInputBase64EncodeMapData = document.getElementById("settings-mqtt-input-base64-encode-map-data"); + var mqttInputHomeassistantMapHack = document.getElementById("settings-mqtt-input-homeassistant-maphack"); mqttInputEnabled.addEventListener("input", updateMqttSaveButton); mqttInputServer.addEventListener("input", updateMqttSaveButton); @@ -33,7 +33,7 @@ async function updateSettingsMqttPage() { mqttInputTopicPrefix.addEventListener("input", updateMqttSaveButton); mqttInputAutoconfPrefix.addEventListener("input", updateMqttSaveButton); mqttInputProvideMapData.addEventListener("input", updateMqttSaveButton); - mqttInputBase64EncodeMapData.addEventListener("input", updateMqttSaveButton); + mqttInputHomeassistantMapHack.addEventListener("input", updateMqttSaveButton); loadingBarSettingsMqtt.setAttribute("indeterminate", "indeterminate"); try { @@ -55,7 +55,7 @@ async function updateSettingsMqttPage() { mqttInputTopicPrefix.value = res.topicPrefix || "valetudo"; mqttInputAutoconfPrefix.value = res.autoconfPrefix || ""; mqttInputProvideMapData.checked = (res.provideMapData === true); - mqttInputBase64EncodeMapData.checked = (res.base64EncodeMapData === true); + mqttInputHomeassistantMapHack.checked = (res.homeassistantMapHack !== false); } catch (err) { ons.notification.toast(err.message, {buttonLabel: "Dismiss", timeout: window.fn.toastErrorTimeout}); @@ -98,7 +98,7 @@ async function handleMqttSettingsSaveButton() { var mqttInputTopicPrefix = document.getElementById("settings-mqtt-input-topic-prefix"); var mqttInputAutoconfPrefix = document.getElementById("settings-mqtt-input-autoconf-prefix"); var mqttInputProvideMapData = document.getElementById("settings-mqtt-input-provide-map-data"); - var mqttInputBase64EncodeMapData = document.getElementById("settings-mqtt-input-base64-encode-map-data"); + var mqttInputHomeassistantMapHack = document.getElementById("settings-mqtt-input-homeassistant-maphack"); loadingBarSettingsMqtt.setAttribute("indeterminate", "indeterminate"); try { @@ -118,7 +118,7 @@ async function handleMqttSettingsSaveButton() { topicPrefix: mqttInputTopicPrefix.value, autoconfPrefix: mqttInputAutoconfPrefix.value, provideMapData: mqttInputProvideMapData.checked, - base64EncodeMapData: mqttInputBase64EncodeMapData.checked + homeassistantMapHack: mqttInputHomeassistantMapHack.checked }); ons.notification.toast( "MQTT settings saved. MQTT Client will apply changes now.", diff --git a/lib/miio/RetryWrapper.js b/lib/miio/RetryWrapper.js index d1094ada70d..a59389598e7 100644 --- a/lib/miio/RetryWrapper.js +++ b/lib/miio/RetryWrapper.js @@ -50,7 +50,7 @@ class RetryWrapper { this.state = STATES.HANDSHAKING; - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { //TODO: uhm? return await new promise? this.loopHandshake(() => { resolve(); }); diff --git a/lib/mqtt/MqttAutoConfManager.js b/lib/mqtt/MqttAutoConfManager.js index 709bccbb40a..bf0c5e8e8b5 100644 --- a/lib/mqtt/MqttAutoConfManager.js +++ b/lib/mqtt/MqttAutoConfManager.js @@ -55,15 +55,22 @@ class MqttAutoConfManager { //TODO: does this thing even make sense? set_fan_speed_topic: this.supportedFeatures.includes("fan_speed") ? this.topics.set_fan_speed : undefined, fan_speed_list: this.supportedFeatures.includes("fan_speed") ? this.robot.capabilities.FanSpeedControlCapability.getPresets() : undefined } + }, + { + topic: this.autoconfPrefix + "/camera/" + this.topicPrefix + "_" + this.identifier + "/config", + payload: { + name: "Map", + unique_id: this.identifier + "_map", + device: this.deviceSpecification, + availability_topic: this.topics.availability, + topic: this.topics.map_data + } } - //Since the map_data will kill the recorder: component, we can't add autoconfig for it (yet) :( ]; this.registeredTopics = {}; this.registeredAutoconfData = []; - - } get deviceSpecification() { diff --git a/lib/mqtt/MqttClient.js b/lib/mqtt/MqttClient.js index 9e752a71bb3..8b957c49ee6 100644 --- a/lib/mqtt/MqttClient.js +++ b/lib/mqtt/MqttClient.js @@ -3,6 +3,9 @@ require("../DnsHack"); const mqtt = require("mqtt"); const zlib = require("zlib"); +const fs = require("fs"); +const path = require("path"); +const crc = require("crc"); const Logger = require("../Logger"); const MqttAutoConfManager = require("./MqttAutoConfManager"); const attributes = require("../entities/state/attributes"); @@ -90,7 +93,7 @@ class MqttClient { this.attributesUpdateInterval = mqttConfig.attributesUpdateInterval || 60000; this.provideMapData = mqttConfig.provideMapData !== undefined ? mqttConfig.provideMapData : true; - this.base64EncodeMapData = mqttConfig.base64EncodeMapData !== undefined ? mqttConfig.base64EncodeMapData : true; + this.homeassistantMapHack = mqttConfig.homeassistantMapHack !== undefined ? mqttConfig.homeassistantMapHack : true; this.registerCapabilityAttributeHandlers(); @@ -359,9 +362,37 @@ class MqttClient { if (this.client && this.client.connected === true && map) { zlib.deflate(JSON.stringify(map), (err, buf) => { if (!err) { + let payload; + + if (this.homeassistantMapHack === true) { + const length = Buffer.alloc(4); + const checksum = Buffer.alloc(4); + + const textChunkData = Buffer.concat([ + HOMEASSISTANT_MAP_HACK.TEXT_CHUNK_TYPE, + HOMEASSISTANT_MAP_HACK.TEXT_CHUNK_METADATA, + buf + ]); + + length.writeInt32BE(HOMEASSISTANT_MAP_HACK.TEXT_CHUNK_METADATA.length + buf.length, 0); + checksum.writeUInt32BE(crc.crc32(textChunkData), 0); + + + payload = Buffer.concat([ + HOMEASSISTANT_MAP_HACK.IMAGE_WITHOUT_END_CHUNK, + length, + textChunkData, + checksum, + HOMEASSISTANT_MAP_HACK.END_CHUNK + ]); + } else { + payload = buf; + } + + this.client.publish( this.autoConfManager.topics.map_data, - this.base64EncodeMapData ? buf.toString("base64") : buf, + payload, {retain: true, qos:this.qos} ); } else { @@ -654,4 +685,13 @@ const CAPABILITY_TYPE_TO_HANDLER_MAPPING = { [capabilities.GoToLocationCapability.TYPE]: attributeHandlers.capability.GoToLocationCapabilityBasedAttributeMqttHandler }; +const HOMEASSISTANT_MAP_HACK = { + TEXT_CHUNK_TYPE: Buffer.from("zTXt"), + TEXT_CHUNK_METADATA: Buffer.from("ValetudoMap\0\0"), + IMAGE: fs.readFileSync(path.join(__dirname, "../res/valetudo_home_assistant_mqtt_camera_hack.png")) +}; +HOMEASSISTANT_MAP_HACK.IMAGE_WITHOUT_END_CHUNK = HOMEASSISTANT_MAP_HACK.IMAGE.slice(0, HOMEASSISTANT_MAP_HACK.IMAGE.length - 12); +//The PNG IEND chunk is always the last chunk and consists of a 4-byte length, the 4-byte chunk type, 0-byte chunk data and a 4-byte crc +HOMEASSISTANT_MAP_HACK.END_CHUNK = HOMEASSISTANT_MAP_HACK.IMAGE.slice(HOMEASSISTANT_MAP_HACK.IMAGE.length - 12); + module.exports = MqttClient; diff --git a/lib/res/default_config.json b/lib/res/default_config.json index 87f2e6d41ca..0f79724ff1e 100644 --- a/lib/res/default_config.json +++ b/lib/res/default_config.json @@ -32,7 +32,7 @@ "topicPrefix": "valetudo", "autoconfPrefix": "homeassistant", "provideMapData": true, - "base64EncodeMapData": true + "homeassistantMapHack": true }, "ntpClient": { "enabled": true, diff --git a/lib/res/valetudo_home_assistant_mqtt_camera_hack.png b/lib/res/valetudo_home_assistant_mqtt_camera_hack.png new file mode 100644 index 00000000000..aee5d301dad Binary files /dev/null and b/lib/res/valetudo_home_assistant_mqtt_camera_hack.png differ diff --git a/package.json b/package.json index b81795232fd..dc7f921c3a1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@destinationstransfers/ntp": "^2.0.0", "body-parser": "^1.18.3", "compression": "^1.7.2", + "crc": "^3.8.0", "express": "^4.17.1", "express-basic-auth": "^1.2.0", "express-dynamic-middleware": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 3446ce8ee9d..eb4fb09d6b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -353,6 +353,11 @@ base64-js@^1.3.0: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + basic-auth@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" @@ -437,6 +442,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.1.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + busboy@^0.2.11: version "0.2.14" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" @@ -647,6 +660,13 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +crc@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== + dependencies: + buffer "^5.1.0" + cross-env@7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" @@ -1498,6 +1518,11 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"