mirror of https://github.com/Hypfer/Valetudo.git
247 lines
9.7 KiB
JavaScript
247 lines
9.7 KiB
JavaScript
const ComponentType = require("../homeassistant/ComponentType");
|
|
const crc = require("crc");
|
|
const DataType = require("../homie/DataType");
|
|
const fs = require("fs");
|
|
const HassAnchor = require("../homeassistant/HassAnchor");
|
|
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
|
|
const Logger = require("../../Logger");
|
|
const MqttCommonAttributes = require("../MqttCommonAttributes");
|
|
const NodeMqttHandle = require("./NodeMqttHandle");
|
|
const path = require("path");
|
|
const PropertyMqttHandle = require("./PropertyMqttHandle");
|
|
const zlib = require("zlib");
|
|
|
|
class MapNodeMqttHandle extends NodeMqttHandle {
|
|
/**
|
|
* @param {object} options
|
|
* @param {import("./RobotMqttHandle")} options.parent
|
|
* @param {import("../MqttController")} options.controller MqttController instance
|
|
* @param {import("../../core/ValetudoRobot")} options.robot
|
|
*/
|
|
constructor(options) {
|
|
super(Object.assign(options, {
|
|
topicName: "MapData",
|
|
friendlyName: "Map data",
|
|
type: "Map",
|
|
helpText: "This handle groups access to map data. It is only enabled if `provideMapData` is enabled in " +
|
|
"the MQTT config."
|
|
}));
|
|
|
|
this.robot = options.robot;
|
|
|
|
this.registerChild(
|
|
new PropertyMqttHandle({
|
|
parent: this,
|
|
controller: this.controller,
|
|
topicName: "map-data",
|
|
friendlyName: "Raw map data",
|
|
datatype: DataType.STRING,
|
|
getter: async () => {
|
|
return this.getMapData(false);
|
|
}
|
|
})
|
|
);
|
|
|
|
// Add "I Can't Believe It's Not Valetudo" map property. Unlike Home Assistant, Homie autodiscovery attributes
|
|
// may not be changed by external services, so for proper autodiscovery support it needs to be provided by
|
|
// Valetudo itself. ICBINV may publish the data at any point in time.
|
|
if (this.controller.currentConfig.interfaces.homie.addICBINVMapProperty) {
|
|
this.registerChild(
|
|
new PropertyMqttHandle({
|
|
parent: this,
|
|
controller: this.controller,
|
|
topicName: "map",
|
|
friendlyName: "Map",
|
|
datatype: DataType.STRING,
|
|
getter: async () => {
|
|
/* intentional */
|
|
},
|
|
helpText: "This handle is only enabled if `interfaces.homie.addICBINVMapProperty` is enabled in the config. " +
|
|
"It does not actually provide map data, it only adds a Homie autodiscovery property so that " +
|
|
"'I Can't Believe It's Not Valetudo' can publish its map within the robot's topics and be " +
|
|
"autodetected by clients.\n\n" +
|
|
"ICBINV should be configured so that it publishes the map to this topic."
|
|
})
|
|
);
|
|
}
|
|
|
|
this.registerChild(
|
|
new PropertyMqttHandle({
|
|
parent: this,
|
|
controller: this.controller,
|
|
topicName: "segments",
|
|
friendlyName: "Map segments",
|
|
datatype: DataType.STRING,
|
|
format: "json",
|
|
getter: async () => {
|
|
if (this.robot.state.map === null || !(this.controller.currentConfig.customizations.provideMapData ?? true)|| !this.controller.isInitialized) {
|
|
return {};
|
|
}
|
|
|
|
const res = {};
|
|
for (const segment of this.robot.state.map.getSegments()) {
|
|
res[segment.id] = segment.name ?? segment.id;
|
|
}
|
|
|
|
await this.controller.hassAnchorProvider.getAnchor(
|
|
HassAnchor.ANCHOR.MAP_SEGMENTS_LEN
|
|
).post(Object.keys(res).length);
|
|
|
|
return res;
|
|
},
|
|
helpText: "This property contains a JSON mapping of segment IDs to segment names."
|
|
}).also((prop) => {
|
|
this.controller.withHass((hass) => {
|
|
prop.attachHomeAssistantComponent(
|
|
new InLineHassComponent({
|
|
hass: hass,
|
|
robot: this.robot,
|
|
name: "MapSegments",
|
|
friendlyName: "Map segments",
|
|
componentType: ComponentType.SENSOR,
|
|
baseTopicReference: this.controller.hassAnchorProvider.getTopicReference(
|
|
HassAnchor.REFERENCE.HASS_MAP_SEGMENTS_STATE
|
|
),
|
|
autoconf: {
|
|
state_topic: this.controller.hassAnchorProvider.getTopicReference(
|
|
HassAnchor.REFERENCE.HASS_MAP_SEGMENTS_STATE
|
|
),
|
|
icon: "mdi:vector-selection",
|
|
json_attributes_topic: prop.getBaseTopic(),
|
|
json_attributes_template: "{{ value }}"
|
|
},
|
|
topics: {
|
|
"": this.controller.hassAnchorProvider.getAnchor(
|
|
HassAnchor.ANCHOR.MAP_SEGMENTS_LEN
|
|
)
|
|
}
|
|
})
|
|
);
|
|
});
|
|
})
|
|
);
|
|
|
|
this.controller.withHass((hass) => {
|
|
this.registerChild(
|
|
new PropertyMqttHandle({
|
|
parent: this,
|
|
controller: this.controller,
|
|
topicName: "map-data-hass",
|
|
friendlyName: "Raw map data for Home Assistant",
|
|
datatype: DataType.STRING,
|
|
getter: async () => {
|
|
return this.getMapData(true);
|
|
},
|
|
helpText: "This handle is added automatically if Home Assistant autodiscovery is enabled. It " +
|
|
"provides a map embedded in a PNG image that recommends installing the Valetudo Lovelace card."
|
|
}).also((prop) => {
|
|
prop.attachHomeAssistantComponent(
|
|
new InLineHassComponent({
|
|
hass: hass,
|
|
robot: this.robot,
|
|
name: "MapData",
|
|
friendlyName: "Map data",
|
|
componentType: ComponentType.CAMERA,
|
|
autoconf: {
|
|
topic: prop.getBaseTopic()
|
|
}
|
|
})
|
|
);
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {number}
|
|
*/
|
|
getQoS() {
|
|
// This shall prevent resource issues for the MQTT broker as maps can be quite heavy
|
|
// and might be cached indefinitely with AT_LEAST_ONCE
|
|
return MqttCommonAttributes.QOS.AT_MOST_ONCE;
|
|
}
|
|
|
|
/**
|
|
* Called by MqttController on map updated.
|
|
*
|
|
* @public
|
|
*/
|
|
onMapUpdated() {
|
|
if (this.controller.isInitialized) {
|
|
this.refresh().catch(err => {
|
|
Logger.error("Error during MQTT handle refresh", err);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {boolean} wrapInPng
|
|
* @return {Promise<Buffer|null>}
|
|
*/
|
|
async getMapData(wrapInPng) {
|
|
if (this.robot.state.map === null || !(this.controller.currentConfig.customizations.provideMapData ?? true) || !this.controller.isInitialized) {
|
|
return null;
|
|
}
|
|
const robot = this.robot;
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
zlib.deflate(JSON.stringify(robot.state.map), (err, buf) => {
|
|
if (err !== null) {
|
|
return reject(err);
|
|
}
|
|
|
|
let payload;
|
|
|
|
if (wrapInPng) {
|
|
const length = Buffer.alloc(4);
|
|
const checksum = Buffer.alloc(4);
|
|
|
|
const textChunkData = Buffer.concat([
|
|
PNG_WRAPPER.TEXT_CHUNK_TYPE,
|
|
PNG_WRAPPER.TEXT_CHUNK_METADATA,
|
|
buf
|
|
]);
|
|
|
|
length.writeInt32BE(PNG_WRAPPER.TEXT_CHUNK_METADATA.length + buf.length, 0);
|
|
checksum.writeUInt32BE(crc.crc32(textChunkData), 0);
|
|
|
|
|
|
payload = Buffer.concat([
|
|
PNG_WRAPPER.IMAGE_WITHOUT_END_CHUNK,
|
|
length,
|
|
textChunkData,
|
|
checksum,
|
|
PNG_WRAPPER.END_CHUNK
|
|
]);
|
|
} else {
|
|
payload = buf;
|
|
}
|
|
|
|
resolve(payload);
|
|
});
|
|
});
|
|
|
|
try {
|
|
return await promise;
|
|
} catch (err) {
|
|
Logger.error("Error while deflating map data for mqtt publish", err);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const PNG_WRAPPER = {
|
|
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_wrapper.png"))
|
|
};
|
|
PNG_WRAPPER.IMAGE_WITHOUT_END_CHUNK = PNG_WRAPPER.IMAGE.subarray(0, PNG_WRAPPER.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
|
|
PNG_WRAPPER.END_CHUNK = PNG_WRAPPER.IMAGE.subarray(PNG_WRAPPER.IMAGE.length - 12);
|
|
|
|
module.exports = MapNodeMqttHandle;
|