Skip to content

Commit

Permalink
feat(mqtt): Publish ValetudoEvents to MQTT and allow interacting with…
Browse files Browse the repository at this point in the history
… it via MQTT
  • Loading branch information
Hypfer committed Jan 26, 2024
1 parent e38c309 commit cc2ecc3
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 0 deletions.
1 change: 1 addition & 0 deletions backend/lib/Valetudo.js
Expand Up @@ -75,6 +75,7 @@ class Valetudo {
this.mqttController = new MqttController({
config: this.config,
robot: this.robot,
valetudoEventStore: this.valetudoEventStore,
valetudoHelper: this.valetudoHelper
});

Expand Down
11 changes: 11 additions & 0 deletions backend/lib/ValetudoEventStore.js
Expand Up @@ -53,6 +53,7 @@ class ValetudoEventStore {

this.events.set(event.id, event);
this.eventEmitter.emit(EVENT_RAISED, event);
this.eventEmitter.emit(EVENTS_UPDATED, event);
}

/**
Expand All @@ -77,6 +78,7 @@ class ValetudoEventStore {
event.processed = true;
//Even though this isn't required as we're interfacing with it by reference. Just for good measure
this.events.set(event.id, event);
this.eventEmitter.emit(EVENTS_UPDATED, event);
}

/**
Expand All @@ -87,6 +89,14 @@ class ValetudoEventStore {
this.eventEmitter.on(EVENT_RAISED, listener);
}

/**
* @param {*} listener
* @public
*/
onEventsUpdated(listener) {
this.eventEmitter.on(EVENTS_UPDATED, listener);
}

/**
* @param {import("./valetudo_events/events/ValetudoEvent")} event
* @param {import("./valetudo_events/handlers/ValetudoEventHandler").INTERACTIONS} interaction
Expand Down Expand Up @@ -117,5 +127,6 @@ class ValetudoEventStore {

const LIMIT = 50;
const EVENT_RAISED = "event_raised";
const EVENTS_UPDATED = "events_updated";

module.exports = ValetudoEventStore;
20 changes: 20 additions & 0 deletions backend/lib/mqtt/MqttController.js
Expand Up @@ -24,11 +24,13 @@ class MqttController {
* @param {object} options
* @param {import("../core/ValetudoRobot")} options.robot
* @param {import("../Configuration")} options.config
* @param {import("../ValetudoEventStore")} options.valetudoEventStore
* @param {import("../utils/ValetudoHelper")} options.valetudoHelper
*/
constructor(options) {
this.config = options.config;
this.robot = options.robot;
this.valetudoEventStore = options.valetudoEventStore;
this.valetudoHelper = options.valetudoHelper;

this.mutexes = {
Expand Down Expand Up @@ -87,6 +89,10 @@ class MqttController {
this.onMapUpdated();
});

this.valetudoEventStore.onEventsUpdated(() => {
this.onValetudoEventsUpdated();
});

/** @type {import("./handles/RobotMqttHandle")|null} */
this.robotHandle = null;
/** @type {HassController} */
Expand All @@ -105,6 +111,7 @@ class MqttController {

this.robotHandle = new RobotMqttHandle({
robot: this.robot,
valetudoEventStore: this.valetudoEventStore,
controller: this,
baseTopic: this.currentConfig.customizations.topicPrefix,
topicName: this.currentConfig.identity.identifier,
Expand Down Expand Up @@ -162,6 +169,7 @@ class MqttController {

this.robotHandle = new RobotMqttHandle({
robot: this.robot,
valetudoEventStore: this.valetudoEventStore,
controller: this,
baseTopic: this.currentConfig.customizations.topicPrefix,
topicName: this.currentConfig.identity.identifier,
Expand Down Expand Up @@ -360,6 +368,8 @@ class MqttController {
Logger.info("MQTT configured");
}).then(() => {
this.setState(HomieCommonAttributes.STATE.READY).then(() => {
this.onValetudoEventsUpdated(); // Publish the initial state

this.robotHandle.refresh().catch(err => {
Logger.error("Error during MQTT handle refresh", err);
});
Expand Down Expand Up @@ -589,6 +599,16 @@ class MqttController {
}
}

onValetudoEventsUpdated() {
if (this.currentConfig.enabled && this.isInitialized && this.robotHandle !== null) {
const valetudoEventsHandle = this.robotHandle.getValetudoEventsHandle();

if (valetudoEventsHandle !== null) {
valetudoEventsHandle.onValetudoEventsUpdated();
}
}
}

/**
* @callback reconfigureCb
* @return {Promise<void>}
Expand Down
21 changes: 21 additions & 0 deletions backend/lib/mqtt/handles/RobotMqttHandle.js
Expand Up @@ -6,6 +6,7 @@ const Logger = require("../../Logger");
const MapNodeMqttHandle = require("./MapNodeMqttHandle");
const STATUS_ATTR_TO_HANDLE_MAPPING = require("./HandleMappings").STATUS_ATTR_TO_HANDLE_MAPPING;
const VacuumHassComponent = require("../homeassistant/components/VacuumHassComponent");
const ValetudoEventsNodeMqttHandle = require("./ValetudoEventsNodeMqttHandle");

/**
* This class represents the robot as a Homie device
Expand All @@ -15,6 +16,7 @@ class RobotMqttHandle extends MqttHandle {
* @param {object} options
* @param {import("../../core/ValetudoRobot")} options.robot
* @param {import("../MqttController")} options.controller
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
* @param {string} options.baseTopic Base topic for Valetudo
* @param {string} options.topicName Topic identifier for this robot
* @param {string} options.friendlyName Friendly name for this robot
Expand All @@ -23,6 +25,7 @@ class RobotMqttHandle extends MqttHandle {
constructor(options) {
super(options);
this.robot = options.robot;
this.valetudoEventStore = options.valetudoEventStore;
this.baseTopic = options.baseTopic;
this.mapHandle = null;

Expand All @@ -34,6 +37,14 @@ class RobotMqttHandle extends MqttHandle {
});
this.registerChild(this.mapHandle);

this.valetudoEventsHandle = new ValetudoEventsNodeMqttHandle({
parent: this,
controller: this.controller,
robot: this.robot,
valetudoEventStore: this.valetudoEventStore
});
this.registerChild(this.valetudoEventsHandle);

// Attach all available capabilities to self
for (const [type, capability] of Object.entries(this.robot.capabilities)) {
const handle = CAPABILITY_TYPE_TO_HANDLE_MAPPING[type];
Expand Down Expand Up @@ -163,6 +174,16 @@ class RobotMqttHandle extends MqttHandle {
getMapHandle() {
return this.mapHandle;
}

/**
* Same as getMapHandle()
*
* @public
* @return {null|ValetudoEventsNodeMqttHandle}
*/
getValetudoEventsHandle() {
return this.valetudoEventsHandle;
}
}

module.exports = RobotMqttHandle;
140 changes: 140 additions & 0 deletions backend/lib/mqtt/handles/ValetudoEventsNodeMqttHandle.js
@@ -0,0 +1,140 @@
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const Logger = require("../../Logger");
const NodeMqttHandle = require("./NodeMqttHandle");
const PropertyMqttHandle = require("./PropertyMqttHandle");

class ValetudoEventsNodeMqttHandle extends NodeMqttHandle {
/**
* @param {object} options
* @param {import("./RobotMqttHandle")} options.parent
* @param {import("../MqttController")} options.controller MqttController instance
* @param {import("../../core/ValetudoRobot")} options.robot
* @param {import("../../ValetudoEventStore")} options.valetudoEventStore
*/
constructor(options) {
super(Object.assign(options, {
topicName: "ValetudoEvents",
friendlyName: "Valetudo Events",
type: "Events"
}));

this.robot = options.robot;
this.valetudoEventStore = options.valetudoEventStore;


this.registerChild(
new PropertyMqttHandle({
parent: this,
controller: this.controller,
topicName: "valetudo_events",
friendlyName: "Events",
datatype: DataType.STRING,
format: "json",
getter: async () => {
const activeEvents = this.valetudoEventStore.getAll().filter(e => e.processed !== true);

await this.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.ACTIVE_VALETUDO_EVENTS_COUNT
).post(activeEvents.length);

const out = {};
activeEvents.forEach(e => {
out[e.id] = e;
});

return out;
},
helpText: "This property contains all raised and not yet processed ValetudoEvents."
}).also((prop) => {
this.controller.withHass((hass) => {
prop.attachHomeAssistantComponent(
new InLineHassComponent({
hass: hass,
robot: this.robot,
name: "ValetudoEvents",
friendlyName: "Events",
componentType: ComponentType.SENSOR,
baseTopicReference: this.controller.hassAnchorProvider.getTopicReference(
HassAnchor.REFERENCE.HASS_ACTIVE_VALETUDO_EVENTS
),
autoconf: {
state_topic: this.controller.hassAnchorProvider.getTopicReference(
HassAnchor.REFERENCE.HASS_ACTIVE_VALETUDO_EVENTS
),
icon: "mdi:bell",
json_attributes_topic: prop.getBaseTopic(),
json_attributes_template: "{{ value }}"
},
topics: {
"": this.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.ACTIVE_VALETUDO_EVENTS_COUNT
)
}
})
);
});
})
);

this.registerChild(
new PropertyMqttHandle({
parent: this,
controller: this.controller,
topicName: "valetudo_events/interact",
friendlyName: "Interact with Events",
datatype: DataType.STRING,
format: "json",
helpText: "Note that the interaction payload is event-specific, but for most use-cases, the example should be sufficient.\n\n" +
"Sample payload for a dismissible event (e.g. an ErrorStateValetudoEvent):\n\n" +
"```json\n" +
JSON.stringify({
id: "b89bd27c-5563-4cfd-87df-2f23e8bbeef7",
interaction: "ok"
}, null, 2) +
"\n```",
setter: async (value) => {
let payload;
try {
payload = JSON.parse(value);
} catch (e) {
/* intentional */
}
if (!payload?.id || !payload?.interaction) {
Logger.warn("Received invalid valetudo_events/interact/set payload.");
return;
}

const event = this.valetudoEventStore.getById(payload.id);
if (!event) {
Logger.warn("Received valetudo_events/interact/set payload with invalid ID.");
return;
}

try {
await this.valetudoEventStore.interact(event, payload.interaction);
} catch (e) {
Logger.warn("Error while interacting with ValetudoEvent", e?.message ?? e);
}
}
})
);
}

/**
* Called by MqttController on any change to the ValetudoEventStore
*
* @public
*/
onValetudoEventsUpdated() {
if (this.controller.isInitialized) {
this.refresh().catch(err => {
Logger.error("Error during MQTT handle refresh", err);
});
}
}
}

module.exports = ValetudoEventsNodeMqttHandle;
2 changes: 2 additions & 0 deletions backend/lib/mqtt/homeassistant/HassAnchor.js
Expand Up @@ -106,6 +106,7 @@ HassAnchor.ANCHOR = Object.freeze({
TOTAL_STATISTICS_COUNT: "total_statistics_count",
FAN_SPEED: "fan_speed",
MAP_SEGMENTS_LEN: "map_segments_len",
ACTIVE_VALETUDO_EVENTS_COUNT: "active_valetudo_events_count",
VACUUM_STATE: "vacuum_state",
WIFI_IPS: "wifi_ips",
WIFI_FREQUENCY: "wifi_freq",
Expand All @@ -123,6 +124,7 @@ HassAnchor.REFERENCE = Object.freeze({
VALETUDO_ROBOT_ERROR: "valetudo_robot_error",
HASS_CONSUMABLE_STATE: "hass_consumable_state_",
HASS_MAP_SEGMENTS_STATE: "hass_map_segments_state",
HASS_ACTIVE_VALETUDO_EVENTS: "hass_active_valetudo_events",
HASS_WIFI_CONFIG_ATTRS: "hass_wifi_config_attrs",
});

Expand Down
10 changes: 10 additions & 0 deletions util/generate_mqtt_docs.js
Expand Up @@ -6,6 +6,7 @@ const CapabilityMqttHandle = require("../backend/lib/mqtt/capabilities/Capabilit
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");
Expand Down Expand Up @@ -185,6 +186,7 @@ class FakeMqttController extends MqttController {
super({
robot: robot,
config: fakeConfig,
valetudoEventStore: eventStore,
valetudoHelper: {
onFriendlyNameChanged: () => {}
}
Expand All @@ -196,6 +198,7 @@ class FakeMqttController extends MqttController {

this.robotHandle = new RobotMqttHandle({
robot: this.robot,
valetudoEventStore: eventStore,
controller: this,
baseTopic: "<TOPIC PREFIX>",
topicName: "<IDENTIFIER>",
Expand Down Expand Up @@ -546,6 +549,7 @@ class FakeMqttController extends MqttController {
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;
Expand Down Expand Up @@ -579,6 +583,12 @@ class FakeMqttController extends MqttController {
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",
Expand Down

0 comments on commit cc2ecc3

Please sign in to comment.