mirror of https://github.com/Hypfer/Valetudo.git
762 lines
28 KiB
JavaScript
762 lines
28 KiB
JavaScript
/* eslint-disable */
|
|
const MockRobot = require("../backend/lib/robots/mock/MockRobot");
|
|
const RobotMqttHandle = require("../backend/lib/mqtt/handles/RobotMqttHandle");
|
|
const MqttController = require("../backend/lib/mqtt/MqttController");
|
|
const CapabilityMqttHandle = require("../backend/lib/mqtt/capabilities/CapabilityMqttHandle");
|
|
const NodeMqttHandle = require("../backend/lib/mqtt/handles/NodeMqttHandle");
|
|
const RobotStateNodeMqttHandle = require("../backend/lib/mqtt/handles/RobotStateNodeMqttHandle");
|
|
const MapNodeMqttHandle = require("../backend/lib/mqtt/handles/MapNodeMqttHandle");
|
|
const ValetudoEventsNodeMqttHandle = require("../backend/lib/mqtt/handles/ValetudoEventsNodeMqttHandle");
|
|
const MockConsumableMonitoringCapability = require("../backend/lib/robots/mock/capabilities/MockConsumableMonitoringCapability");
|
|
const ConsumableStateAttribute = require("../backend/lib/entities/state/attributes/ConsumableStateAttribute");
|
|
const ValetudoMapSegment = require("../backend/lib/entities/core/ValetudoMapSegment");
|
|
const PropertyMqttHandle = require("../backend/lib/mqtt/handles/PropertyMqttHandle");
|
|
const DataType = require("../backend/lib/mqtt/homie/DataType");
|
|
const HassController = require("../backend/lib/mqtt/homeassistant/HassController");
|
|
const ConsumableMonitoringCapability = require("../backend/lib/core/capabilities/ConsumableMonitoringCapability");
|
|
const ConsumableMonitoringCapabilityMqttHandle = require("../backend/lib/mqtt/capabilities/ConsumableMonitoringCapabilityMqttHandle");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const BatteryStateAttribute = require("../backend/lib/entities/state/attributes/BatteryStateAttribute");
|
|
const AttachmentStateAttribute = require("../backend/lib/entities/state/attributes/AttachmentStateAttribute");
|
|
const StatusStateAttribute = require("../backend/lib/entities/state/attributes/StatusStateAttribute");
|
|
const PresetSelectionStateAttribute = require("../backend/lib/entities/state/attributes/PresetSelectionStateAttribute");
|
|
const Unit = require("../backend/lib/mqtt/common/Unit");
|
|
const HomieCommonAttributes = require("../backend/lib/mqtt/homie/HomieCommonAttributes");
|
|
const ValetudoEventStore = require("valetudo-backend/lib/ValetudoEventStore");
|
|
|
|
|
|
function jekyllAlert(type, content) {
|
|
return "{% include alert.html type=\"" + type + "\" content=\"" + content.replace(/"/g, "\\\"") + "\" %}\n\n";
|
|
}
|
|
|
|
const markdownPreamble = `---
|
|
title: MQTT
|
|
category: Integrations
|
|
order: 20
|
|
---
|
|
|
|
# MQTT integration
|
|
|
|
To make your robot talk to your MQTT broker and integrate with home automation software, such as but not limited to
|
|
Home Assistant, openHAB and Node-RED, configure MQTT via Valetudo's web interface (Connectivity → MQTT connectivity).
|
|
|
|
## Autodiscovery
|
|
|
|
See the specific integration pages for instructions on how to set up autodiscovery for your home automation software
|
|
platform:
|
|
|
|
- [Home Assistant](./home-assistant-integration)
|
|
- [openHAB](./openhab-integration)
|
|
- [Node-RED](./node-red)
|
|
|
|
Other home automation software that follows the [Homie convention](https://homieiot.github.io/) should also be able to
|
|
automatically discover your Valetudo instance.
|
|
|
|
<div style="text-align: center;">
|
|
<a href="https://homieiot.github.io" rel="noopener" target="_blank">
|
|
<img src="./img/works-with-homie.svg" />
|
|
</a>
|
|
<br>
|
|
<br>
|
|
</div>
|
|
|
|
## Map
|
|
|
|
Note that, in order to view the map provided over MQTT, you additionally need
|
|
[I Can't Believe It's Not Valetudo](/pages/companion_apps/i_cant_believe_its_not_valetudo.html) to generate PNG maps.
|
|
You can then configure it to serve the PNG map over HTTP for openHAB and other software, or install the
|
|
[Lovelace Valetudo Card Map](/pages/companion_apps/lovelace_valetudo_map_card.html) for Home Assistant.
|
|
|
|
## Custom integrations
|
|
|
|
If you're planning to use one of the home automation platforms listed above, this is all you need to know to get started.
|
|
|
|
If you're instead planning to do something more custom, in this document you will find a reference to all MQTT topics
|
|
provided by this software. Values such as \`<TOPIC PREFIX>\` and \`<IDENTIFIER>\` are those configured in the MQTT
|
|
settings page.
|
|
|
|
` + jekyllAlert("tip", `It is recommended to leave Homie autodiscovery enabled, even if you're not planning to use it, if you want to develop
|
|
custom integrations or access the MQTT topics directly: the Homie protocol is very readable and self-documenting.
|
|
It will provide additional context and information on how to use specific APIs.
|
|
|
|
|
|
Homie autodiscovery info is best viewed with something like [MQTT Explorer](https://mqtt-explorer.com/).
|
|
`);
|
|
|
|
const fakeConfig = {
|
|
onUpdate: (_) => {
|
|
},
|
|
get: key => fakeConfig[key],
|
|
};
|
|
const eventStore = new ValetudoEventStore()
|
|
|
|
class FakeHassController extends HassController {
|
|
// @ts-ignore
|
|
constructor(mqttController, robot) {
|
|
super({
|
|
controller: mqttController,
|
|
robot: robot,
|
|
config: fakeConfig
|
|
});
|
|
|
|
this.mqtt = mqttController;
|
|
this.robot = robot;
|
|
}
|
|
|
|
loadConfig() {
|
|
}
|
|
|
|
registerNonCompliantComponent(component) {
|
|
}
|
|
|
|
async subscribe(component) {
|
|
}
|
|
|
|
async unsubscribe(component) {
|
|
}
|
|
|
|
async refresh(component, topics) {
|
|
}
|
|
|
|
async dropAutoconf(component) {
|
|
}
|
|
|
|
async refreshAutoconf(component, payload) {
|
|
}
|
|
}
|
|
|
|
function keyFn(key) {
|
|
return function (a, b) {
|
|
if (a[key] < b[key]) {
|
|
return -1;
|
|
}
|
|
if (a[key] > b[key]) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
};
|
|
}
|
|
|
|
|
|
ConsumableMonitoringCapabilityMqttHandle.prototype.genConsumableFriendlyName = (type, subType) => {
|
|
return `${type}`
|
|
}
|
|
|
|
|
|
class FakeMqttController extends MqttController {
|
|
// @ts-ignore
|
|
constructor() {
|
|
const robot = new MockRobot({config: fakeConfig, valetudoEventStore: eventStore});
|
|
|
|
robot.capabilities[ConsumableMonitoringCapability.TYPE].getProperties = () => {
|
|
return {
|
|
availableConsumables: [
|
|
{
|
|
type: "<CONSUMABLE-MINUTES>",
|
|
subType: ConsumableStateAttribute.SUB_TYPE.NONE,
|
|
unit: ConsumableStateAttribute.UNITS.MINUTES
|
|
},
|
|
{
|
|
type: "<CONSUMABLE-PERCENT>",
|
|
subType: ConsumableStateAttribute.SUB_TYPE.NONE,
|
|
unit: ConsumableStateAttribute.UNITS.PERCENT
|
|
},
|
|
]
|
|
};
|
|
}
|
|
|
|
robot.state.map.getSegments = () => {
|
|
return [
|
|
new ValetudoMapSegment({
|
|
id: "20",
|
|
name: "Kitchen"
|
|
}),
|
|
new ValetudoMapSegment({
|
|
id: "18",
|
|
name: "Bathroom"
|
|
}),
|
|
new ValetudoMapSegment({
|
|
id: "16",
|
|
name: "Hallway"
|
|
}),
|
|
]
|
|
}
|
|
|
|
super({
|
|
robot: robot,
|
|
config: fakeConfig,
|
|
valetudoEventStore: eventStore,
|
|
valetudoHelper: {
|
|
onFriendlyNameChanged: () => {}
|
|
}
|
|
});
|
|
|
|
this.enabled = true;
|
|
|
|
this.hassController = new FakeHassController(this, this.robot);
|
|
|
|
this.robotHandle = new RobotMqttHandle({
|
|
robot: this.robot,
|
|
valetudoEventStore: eventStore,
|
|
controller: this,
|
|
baseTopic: "<TOPIC PREFIX>",
|
|
topicName: "<IDENTIFIER>",
|
|
friendlyName: "Robot",
|
|
optionalExposedCapabilities: this.getOptionalExposableCapabilities()
|
|
});
|
|
|
|
this.generatePromise = new Promise((resolve, reject) => {
|
|
this.resolveGenerate = resolve;
|
|
this.rejectGenerate = reject;
|
|
});
|
|
this.docsGenerated = false;
|
|
this.addedConsumables = [];
|
|
}
|
|
|
|
async injectStatus() {
|
|
const attributes = [
|
|
new BatteryStateAttribute({
|
|
level: 42,
|
|
flag: BatteryStateAttribute.FLAG.CHARGING
|
|
}),
|
|
new AttachmentStateAttribute({
|
|
type: AttachmentStateAttribute.TYPE.DUSTBIN,
|
|
attached: true
|
|
}),
|
|
new AttachmentStateAttribute({
|
|
type: AttachmentStateAttribute.TYPE.MOP,
|
|
attached: false
|
|
}),
|
|
new AttachmentStateAttribute({
|
|
type: AttachmentStateAttribute.TYPE.WATERTANK,
|
|
attached: true
|
|
}),
|
|
new StatusStateAttribute({
|
|
value: StatusStateAttribute.VALUE.CLEANING,
|
|
flag: StatusStateAttribute.FLAG.SEGMENT
|
|
}),
|
|
new PresetSelectionStateAttribute({
|
|
type: PresetSelectionStateAttribute.TYPE.FAN_SPEED,
|
|
value: PresetSelectionStateAttribute.INTENSITY.MAX
|
|
}),
|
|
new PresetSelectionStateAttribute({
|
|
type: PresetSelectionStateAttribute.TYPE.WATER_GRADE,
|
|
value: PresetSelectionStateAttribute.INTENSITY.MIN
|
|
}),
|
|
new ConsumableStateAttribute({
|
|
type: "<CONSUMABLE-MINUTES>",
|
|
remaining: {
|
|
value: 492,
|
|
unit: ConsumableStateAttribute.UNITS.MINUTES
|
|
}
|
|
}),
|
|
new ConsumableStateAttribute({
|
|
type: "<CONSUMABLE-PERCENT>",
|
|
remaining: {
|
|
value: 59,
|
|
unit: ConsumableStateAttribute.UNITS.PERCENT
|
|
}
|
|
})
|
|
];
|
|
for (const attr of attributes) {
|
|
this.robot.state.upsertFirstMatchingAttribute(attr);
|
|
}
|
|
await this.robotHandle.refresh();
|
|
}
|
|
|
|
async generateDocs() {
|
|
this.currentConfig.optionalExposedCapabilities = this.getOptionalExposableCapabilities();
|
|
|
|
await this.reconfigure(async () => {
|
|
await this.robotHandle.configure();
|
|
}, {
|
|
reconfigState: HomieCommonAttributes.STATE.INIT,
|
|
targetState: HomieCommonAttributes.STATE.INIT
|
|
});
|
|
this.injectStatus();
|
|
|
|
// Give time for the status attributes to propagate
|
|
setTimeout(() => {
|
|
this.setState("sentinel").catch(err => {console.error(err)});
|
|
}, 500);
|
|
|
|
// Promise resolved/rejected by doGenerateDocs(), in turn called when Homie state == ready by setState().
|
|
return await this.generatePromise;
|
|
}
|
|
|
|
crawlGetHandlesOfType(rootHandle, clazz, notClazz) {
|
|
let result = [];
|
|
if (rootHandle instanceof clazz && (notClazz === undefined || !(rootHandle instanceof notClazz))) {
|
|
result.push(rootHandle);
|
|
}
|
|
for (const handle of rootHandle.children ?? []) {
|
|
result.push(...this.crawlGetHandlesOfType(handle, clazz, notClazz));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
generateAnchor(str) {
|
|
return str.replace(/[^0-9a-z-A-Z]/g, "").toLowerCase();
|
|
}
|
|
|
|
async generateHandleDoc(handle, markdownLevel, recurse) {
|
|
const childAnchors = [];
|
|
// Not HassAnchors but HTML anchors to Hass components!
|
|
const hassComponentAnchors = {};
|
|
const stateAttrAnchors = {};
|
|
|
|
if (recurse === undefined) {
|
|
recurse = true;
|
|
}
|
|
let markdown = "";
|
|
|
|
// Inject consumable friendly names since we're not using the standard ones
|
|
if (handle instanceof PropertyMqttHandle && handle.parent instanceof ConsumableMonitoringCapabilityMqttHandle && !handle.topicName.endsWith("reset")) {
|
|
if (handle.getBaseTopic().endsWith("<CONSUMABLE-MINUTES>")) {
|
|
handle.friendlyName = "Consumable (minutes)";
|
|
handle.hassComponents[0].friendlyName = "Consumable (minutes)";
|
|
} else {
|
|
handle.friendlyName = "Consumable (percent)";
|
|
handle.hassComponents[0].friendlyName = "Consumable (percent)";
|
|
}
|
|
|
|
if (this.addedConsumables.includes(handle.friendlyName)) {
|
|
return null;
|
|
}
|
|
this.addedConsumables.push(handle.friendlyName);
|
|
}
|
|
|
|
let title = handle.friendlyName;
|
|
if (handle instanceof CapabilityMqttHandle) {
|
|
title += ` (\`${handle.capability.getType()}\`)`;
|
|
} else if (handle instanceof RobotStateNodeMqttHandle) {
|
|
title += ` (\`${handle.getInterestingStatusAttributes()[0].attributeClass}\`)`;
|
|
} else if (handle instanceof PropertyMqttHandle) {
|
|
title += ` (\`${handle.topicName}\`)`;
|
|
}
|
|
const anchor = this.generateAnchor(title);
|
|
markdown += `${"#".repeat(markdownLevel)} ${title} <a id="${anchor}" />` + "\n\n";
|
|
|
|
let homieType = "Handle";
|
|
const attributes = [];
|
|
if (handle instanceof PropertyMqttHandle) {
|
|
homieType = "Property";
|
|
attributes.push("Property");
|
|
|
|
if (handle.gettable) {
|
|
attributes.push("readable");
|
|
}
|
|
if (handle.isCommand) {
|
|
attributes.push("command");
|
|
} else if (handle.settable) {
|
|
attributes.push("settable");
|
|
}
|
|
attributes.push((handle.retained ? "" : "not ") + "retained");
|
|
|
|
} else if (handle instanceof NodeMqttHandle) {
|
|
homieType = "Node";
|
|
attributes.push("Node");
|
|
} else if (handle instanceof RobotMqttHandle) {
|
|
homieType = "Device";
|
|
attributes.push("Device");
|
|
}
|
|
if (handle instanceof CapabilityMqttHandle) {
|
|
attributes.push(`capability: [${handle.capability.getType()}](/pages/usage/capabilities-overview.html#${this.generateAnchor(handle.capability.getType())})`);
|
|
}
|
|
markdown += `*${attributes.join(", ")}*` + "\n\n";
|
|
|
|
if (handle.constructor.OPTIONAL === true) {
|
|
markdown += `**Note:** This is an optional exposed capability handle and thus will only be available via MQTT if enabled in the Valetudo configuration.\n\n`;
|
|
}
|
|
|
|
if (handle.helpText) {
|
|
markdown += handle.helpText + "\n\n";
|
|
}
|
|
let readTopicDesc = "Base topic";
|
|
let setTopicDesc = "Set topic";
|
|
if (handle instanceof PropertyMqttHandle) {
|
|
if (handle.isCommand) {
|
|
readTopicDesc = "Command response topic";
|
|
setTopicDesc = "Command topic";
|
|
} else {
|
|
readTopicDesc = "Read topic";
|
|
}
|
|
}
|
|
let readTopic = handle.getBaseTopic();
|
|
const setTopic = handle.getBaseTopic() + "/set";
|
|
|
|
if (handle instanceof PropertyMqttHandle && handle.isCommand) {
|
|
// Command topic before response topic
|
|
markdown += `- ${setTopicDesc}: \`${setTopic}\`` + "\n";
|
|
markdown += `- ${readTopicDesc}: \`${readTopic}\`` + "\n";
|
|
} else {
|
|
if (handle.gettable) {
|
|
markdown += `- ${readTopicDesc}: \`${readTopic}\`` + "\n";
|
|
}
|
|
if (handle.settable) {
|
|
markdown += `- ${setTopicDesc}: \`${setTopic}\`` + "\n";
|
|
}
|
|
}
|
|
if (handle instanceof PropertyMqttHandle) {
|
|
if ((handle.dataType === DataType.INTEGER || handle.dataType === DataType.FLOAT) && handle.unit === Unit.PERCENT) {
|
|
markdown += `- Data type: [${handle.dataType} percentage](https://homieiot.github.io/specification/#percent)`;
|
|
} else {
|
|
markdown += `- Data type: [${handle.dataType}](https://homieiot.github.io/specification/#${handle.dataType})`;
|
|
}
|
|
if (handle.dataType === DataType.DATETIME) {
|
|
markdown += " (in [ISO8601 date and time format](https://en.wikipedia.org/wiki/ISO_8601))";
|
|
} else if (handle.dataType === DataType.DURATION) {
|
|
markdown += " (in [ISO8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations))";
|
|
}
|
|
|
|
if (handle.format) {
|
|
markdown += " ";
|
|
if (handle.format === "json") {
|
|
markdown += "(JSON)";
|
|
} else {
|
|
switch (handle.dataType) {
|
|
case DataType.INTEGER:
|
|
case DataType.FLOAT:
|
|
markdown += "(range: " + handle.format.replace(":", " to ");
|
|
if (handle.unit) {
|
|
markdown += ", unit: " + handle.unit;
|
|
}
|
|
markdown += ")";
|
|
break;
|
|
case DataType.ENUM:
|
|
markdown += "(allowed payloads: " + handle.format.split(",").map(val => "`" + val + "`").join(", ") + ")";
|
|
break;
|
|
default:
|
|
markdown += `(format: \`${handle.format}\`)`;
|
|
}
|
|
}
|
|
}
|
|
if (handle.unit && !handle.format && (handle.dataType === DataType.INTEGER || handle.DATETIME === DataType.FLOAT)) {
|
|
markdown += ` (unit: ${handle.unit})`;
|
|
}
|
|
markdown += "\n";
|
|
if (handle.unit && handle.dataType !== DataType.INTEGER && handle.DATETIME !== DataType.FLOAT) {
|
|
markdown += "- Unit: " + handle.unit + "\n";
|
|
}
|
|
markdown += "\n";
|
|
}
|
|
|
|
if (handle.helpMayChange && Object.keys(handle.helpMayChange).length > 0) {
|
|
let alert = "Some information contained in this document " +
|
|
"may not be exactly what is sent or expected by actual robots, since different vendors have different" +
|
|
" implementations. Refer to the table below.\n\n";
|
|
alert += "|------+--------|\n";
|
|
alert += "| What | Reason |\n";
|
|
alert += "|------|--------|\n";
|
|
for (const [what, reason] of Object.entries(handle.helpMayChange)) {
|
|
alert += `| ${what} | ${reason} |` + "\n";
|
|
}
|
|
alert += "|------+--------|\n\n";
|
|
markdown += jekyllAlert("warning", alert);
|
|
}
|
|
|
|
if (handle.gettable) {
|
|
try {
|
|
const sampleValue = await handle.getHomie();
|
|
if (sampleValue) {
|
|
markdown += "Sample value:\n\n";
|
|
if (handle.format === "json" || [DataType.BOOLEAN, DataType.INTEGER, DataType.FLOAT].includes(handle.dataType)) {
|
|
markdown += "```json\n";
|
|
} else {
|
|
markdown += "```\n";
|
|
}
|
|
if (handle.format === "json") {
|
|
markdown += JSON.stringify(JSON.parse(sampleValue), null, 2) + "\n";
|
|
} else {
|
|
markdown += sampleValue + "\n";
|
|
}
|
|
markdown += "```\n\n";
|
|
}
|
|
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
if (handle instanceof RobotStateNodeMqttHandle && handle.getInterestingStatusAttributes().length > 0) {
|
|
markdown += `Status attributes managed by this ${homieType.toLowerCase()}:` + "\n\n";
|
|
for (const attr of handle.getInterestingStatusAttributes()) {
|
|
markdown += "- " + attr.attributeClass;
|
|
stateAttrAnchors[attr.attributeClass] = anchor;
|
|
}
|
|
markdown += "\n\n";
|
|
}
|
|
|
|
if (handle.hassComponents.length > 0) {
|
|
markdown += `Home Assistant components controlled by this ${homieType.toLowerCase()}:` + "\n\n";
|
|
for (const component of handle.hassComponents.sort(keyFn("friendlyName")).sort(keyFn("componentType"))) {
|
|
if (component.componentType === "vacuum") {
|
|
component.friendlyName = "Vacuum";
|
|
}
|
|
markdown += "- " + (component.friendlyName ?? component.name ?? component.componentId) +
|
|
` ([\`${component.componentType}.mqtt\`](https://www.home-assistant.io/integrations/${component.componentType}.mqtt/))\n`;
|
|
const compTitle = (component.friendlyName ?? component.name ?? component.componentId) + ` (\`${component.componentType}.mqtt\`)`;
|
|
hassComponentAnchors[compTitle] = anchor;
|
|
}
|
|
markdown += "\n";
|
|
}
|
|
|
|
if (recurse && handle.children.length > 0) {
|
|
for (const child of handle.children.sort(keyFn("topicName"))) {
|
|
const result = await this.generateHandleDoc(child, markdownLevel + 1, true);
|
|
if (!result) {
|
|
continue;
|
|
}
|
|
markdown += result.markdown;
|
|
childAnchors.push(result.anchors);
|
|
Object.assign(hassComponentAnchors, result.hassComponentAnchors);
|
|
}
|
|
}
|
|
|
|
markdown += "\n\n";
|
|
|
|
return {
|
|
markdown: markdown,
|
|
anchors: {
|
|
title: title,
|
|
anchor: anchor,
|
|
children: childAnchors
|
|
},
|
|
hassComponentAnchors: hassComponentAnchors,
|
|
stateAttrAnchors: stateAttrAnchors
|
|
};
|
|
}
|
|
|
|
generateIndex(anchors) {
|
|
return Object.entries(anchors).sort(keyFn(0)).map(([key, val]) => {
|
|
return `- [${key}](#${val})\n`;
|
|
}).join("");
|
|
}
|
|
|
|
generateToc(anchors, markdownLevel) {
|
|
let markdown = `${" ".repeat(markdownLevel)} - [${anchors.title}](#${anchors.anchor})\n`;
|
|
for (const child of anchors.children.sort(keyFn("title"))) {
|
|
markdown += this.generateToc(child, markdownLevel + 1);
|
|
}
|
|
return markdown;
|
|
}
|
|
|
|
async doGenerateDocs() {
|
|
let markdown = "# MQTT API reference\n\n";
|
|
|
|
try {
|
|
const capabilities = this.crawlGetHandlesOfType(this.robotHandle, CapabilityMqttHandle);
|
|
const stateAttrs = this.crawlGetHandlesOfType(this.robotHandle, RobotStateNodeMqttHandle, CapabilityMqttHandle);
|
|
const map = this.crawlGetHandlesOfType(this.robotHandle, MapNodeMqttHandle);
|
|
const valetudoEvents = this.crawlGetHandlesOfType(this.robotHandle, ValetudoEventsNodeMqttHandle);
|
|
|
|
let anchors;
|
|
let hassComponentAnchors;
|
|
let stateAttrAnchors;
|
|
|
|
const robotRes = await this.generateHandleDoc(this.robotHandle, 2, false);
|
|
markdown += robotRes.markdown;
|
|
anchors = robotRes.anchors;
|
|
hassComponentAnchors = robotRes.hassComponentAnchors;
|
|
stateAttrAnchors = robotRes.stateAttrAnchors;
|
|
|
|
const capsAnchor = {
|
|
title: "Capabilities",
|
|
anchor: "capabilities",
|
|
children: []
|
|
};
|
|
anchors.children.push(capsAnchor);
|
|
|
|
markdown += "### Capabilities <a id=\"capabilities\" />\n\n";
|
|
for (const handle of capabilities.sort(keyFn("topicName"))) {
|
|
const result = await this.generateHandleDoc(handle, 4, true);
|
|
markdown += result.markdown;
|
|
capsAnchor.children.push(result.anchors);
|
|
Object.assign(hassComponentAnchors, result.hassComponentAnchors);
|
|
Object.assign(stateAttrAnchors, result.stateAttrAnchors);
|
|
}
|
|
|
|
const mapRes = await this.generateHandleDoc(map[0], 3, true);
|
|
markdown += mapRes.markdown;
|
|
anchors.children.push(mapRes.anchors);
|
|
Object.assign(hassComponentAnchors, mapRes.hassComponentAnchors);
|
|
Object.assign(stateAttrAnchors, mapRes.stateAttrAnchors);
|
|
|
|
const valetudoEventsRes = await this.generateHandleDoc(valetudoEvents[0], 3, true);
|
|
markdown += valetudoEventsRes.markdown;
|
|
anchors.children.push(valetudoEventsRes.anchors);
|
|
Object.assign(hassComponentAnchors, valetudoEventsRes.hassComponentAnchors);
|
|
Object.assign(stateAttrAnchors, valetudoEventsRes.stateAttrAnchors);
|
|
|
|
const statusAnchor = {
|
|
title: "Status",
|
|
anchor: "status",
|
|
children: []
|
|
};
|
|
anchors.children.push(statusAnchor);
|
|
|
|
markdown += "### Status <a id=\"status\" />\n\n";
|
|
// noinspection DuplicatedCode
|
|
for (const handle of stateAttrs.sort(keyFn("topicName"))) {
|
|
const result = await this.generateHandleDoc(handle, 4, true);
|
|
markdown += result.markdown;
|
|
statusAnchor.children.push(result.anchors);
|
|
Object.assign(hassComponentAnchors, result.hassComponentAnchors);
|
|
Object.assign(stateAttrAnchors, result.stateAttrAnchors);
|
|
}
|
|
|
|
const toc = this.generateToc(anchors, 0);
|
|
const hassIndex = this.generateIndex(hassComponentAnchors);
|
|
const stateAttrIndex = this.generateIndex(stateAttrAnchors);
|
|
|
|
markdown = markdownPreamble +
|
|
"### Table of contents\n\n" + toc + "\n\n" +
|
|
"### State attributes index\n\n" + stateAttrIndex + "\n\n" +
|
|
"### Home Assistant components index\n\n" + hassIndex + "\n\n" +
|
|
markdown;
|
|
} catch (e) {
|
|
this.rejectGenerate(e);
|
|
}
|
|
|
|
this.resolveGenerate(markdown);
|
|
}
|
|
|
|
|
|
get isInitialized() {
|
|
const stack = new Error().stack;
|
|
|
|
// Now this is some major jank engineering
|
|
if (
|
|
stack.split("\n").find(line => {
|
|
return line.includes("MapNodeMqttHandle") && line.includes("PropertyMqttHandle.getter")
|
|
}) &&
|
|
!stack.includes("MapNodeMqttHandle.getMapData")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return !!this.docsGenerated;
|
|
}
|
|
|
|
async setState(state) {
|
|
this.state = state;
|
|
if (state === "sentinel") {
|
|
await this.doGenerateDocs();
|
|
}
|
|
}
|
|
|
|
loadConfig() {
|
|
this.currentConfig = {
|
|
"clientId": "rolf",
|
|
"qos": 1,
|
|
"enabled": false,
|
|
"connection": {
|
|
"host": "lol",
|
|
"port": 1883,
|
|
"tls": {
|
|
"enabled": false,
|
|
"ca": ""
|
|
},
|
|
"authentication": {
|
|
"credentials": {
|
|
"enabled": false,
|
|
"username": "",
|
|
"password": ""
|
|
},
|
|
"clientCertificate": {
|
|
"enabled": false,
|
|
"certificate": "",
|
|
"key": ""
|
|
}
|
|
}
|
|
},
|
|
"identity": {
|
|
"friendlyName": "Valetudo Robot",
|
|
"identifier": "<IDENTIFIER>"
|
|
},
|
|
"interfaces": {
|
|
"homie": {
|
|
"enabled": true,
|
|
"addICBINVMapProperty": true,
|
|
"cleanAttributesOnShutdown": false
|
|
},
|
|
"homeassistant": {
|
|
"enabled": true,
|
|
"cleanAutoconfOnShutdown": false
|
|
}
|
|
},
|
|
"customizations": {
|
|
"topicPrefix": "<TOPIC PREFIX>",
|
|
"provideMapData": true
|
|
},
|
|
"stateTopic": "<TOPIC PREFIX>/<IDENTIFIER>/$state"
|
|
}
|
|
}
|
|
|
|
async publishHomie(handle) {
|
|
}
|
|
|
|
async subscribe(handle) {
|
|
}
|
|
|
|
async publish(handle) {
|
|
}
|
|
|
|
// noinspection JSCheckFunctionSignatures
|
|
async publishHass(component) {
|
|
}
|
|
|
|
async shutdown() {
|
|
}
|
|
|
|
async dropHomieAttributes(handle) {
|
|
}
|
|
|
|
stopAutorefreshService() {
|
|
}
|
|
|
|
getMqttOptions() {
|
|
}
|
|
|
|
getHassMigrationTopics() {
|
|
}
|
|
|
|
async connect() {
|
|
}
|
|
|
|
async disconnect() {
|
|
}
|
|
|
|
onMapUpdated() {
|
|
}
|
|
|
|
async unsubscribe(handle) {
|
|
}
|
|
|
|
|
|
async publishHomieAttributes(handle) {
|
|
}
|
|
|
|
startAutorefreshService() {
|
|
}
|
|
|
|
async refresh(handle) {
|
|
}
|
|
}
|
|
|
|
process.on("uncaughtException", function (err) {
|
|
console.log(err);
|
|
process.exit(1);
|
|
});
|
|
|
|
async function genDocs() {
|
|
const fakeController = new FakeMqttController();
|
|
const markdown = await fakeController.generateDocs();
|
|
fs.writeFileSync(path.join(__dirname, "../docs/_pages/integrations/mqtt.md"), markdown);
|
|
}
|
|
|
|
genDocs().then(() => {
|
|
process.exit(0);
|
|
});
|