mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(mqtt): Publish ValetudoEvents to MQTT and allow interacting with it via MQTT
This commit is contained in:
parent
e38c309340
commit
cc2ecc3c00
@ -75,6 +75,7 @@ class Valetudo {
|
||||
this.mqttController = new MqttController({
|
||||
config: this.config,
|
||||
robot: this.robot,
|
||||
valetudoEventStore: this.valetudoEventStore,
|
||||
valetudoHelper: this.valetudoHelper
|
||||
});
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ class ValetudoEventStore {
|
||||
|
||||
this.events.set(event.id, event);
|
||||
this.eventEmitter.emit(EVENT_RAISED, event);
|
||||
this.eventEmitter.emit(EVENTS_UPDATED, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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
|
||||
@ -117,5 +127,6 @@ class ValetudoEventStore {
|
||||
|
||||
const LIMIT = 50;
|
||||
const EVENT_RAISED = "event_raised";
|
||||
const EVENTS_UPDATED = "events_updated";
|
||||
|
||||
module.exports = ValetudoEventStore;
|
||||
|
||||
@ -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 = {
|
||||
@ -87,6 +89,10 @@ class MqttController {
|
||||
this.onMapUpdated();
|
||||
});
|
||||
|
||||
this.valetudoEventStore.onEventsUpdated(() => {
|
||||
this.onValetudoEventsUpdated();
|
||||
});
|
||||
|
||||
/** @type {import("./handles/RobotMqttHandle")|null} */
|
||||
this.robotHandle = null;
|
||||
/** @type {HassController} */
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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);
|
||||
});
|
||||
@ -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>}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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];
|
||||
@ -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
backend/lib/mqtt/handles/ValetudoEventsNodeMqttHandle.js
Normal file
140
backend/lib/mqtt/handles/ValetudoEventsNodeMqttHandle.js
Normal file
@ -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;
|
||||
@ -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",
|
||||
@ -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",
|
||||
});
|
||||
|
||||
|
||||
@ -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");
|
||||
@ -185,6 +186,7 @@ class FakeMqttController extends MqttController {
|
||||
super({
|
||||
robot: robot,
|
||||
config: fakeConfig,
|
||||
valetudoEventStore: eventStore,
|
||||
valetudoHelper: {
|
||||
onFriendlyNameChanged: () => {}
|
||||
}
|
||||
@ -196,6 +198,7 @@ class FakeMqttController extends MqttController {
|
||||
|
||||
this.robotHandle = new RobotMqttHandle({
|
||||
robot: this.robot,
|
||||
valetudoEventStore: eventStore,
|
||||
controller: this,
|
||||
baseTopic: "<TOPIC PREFIX>",
|
||||
topicName: "<IDENTIFIER>",
|
||||
@ -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;
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user