Valetudo/backend/lib/mqtt/homeassistant/components/HassComponent.js

229 lines
6.7 KiB
JavaScript

const CallbackHassAnchorSubscriber = require("../CallbackHassAnchorSubscriber");
const HassAnchor = require("../HassAnchor");
const HomieCommonAttributes = require("../../homie/HomieCommonAttributes");
const Logger = require("../../../Logger");
const NotImplementedError = require("../../../core/NotImplementedError");
class HassComponent {
/**
* @param {object} options
* @param {import("../HassController")} options.hass
* @param {import("../../../core/ValetudoRobot")} options.robot
* @param {string} options.componentType
* @param {string} options.componentId
*/
constructor(options) {
this.hass = options.hass;
this.robot = options.robot;
this.componentType = options.componentType;
this.componentId = options.componentId;
this.topicRefSubscriber = new CallbackHassAnchorSubscriber(async () => {
await this.refreshAutoconf();
});
this.anchorSubscriber = new CallbackHassAnchorSubscriber(async () => {
await this.refresh();
});
}
/**
* @public
* @return {string}
*/
getBaseTopic() {
return this.hass.getBaseTopic() + "/" + this.componentId;
}
/**
* @public
* @return {string}
*/
getAutoconfTopic() {
return "homeassistant/" + this.componentType + "/" + this.hass.identifier + "/" + this.componentId + "/config";
}
/**
* @private
* @param {object|null} json
* @return {Array<HassAnchor>}
*/
getAllAnchors(json) {
const anchors = [];
if (json === null) {
return anchors;
}
const findAnchors = function (obj) { //recursion!
for (const value of Object.values(obj)) {
if (value instanceof HassAnchor) {
anchors.push(value);
} else if (value instanceof Object) {
findAnchors(value);
}
}
};
findAnchors(json);
return anchors;
}
/**
* Configure this component
*
* @public
* @return {Promise<void>}
*/
async configure() {
await this.hass.subscribe(this);
for (const anchor of this.getAllAnchors(this.getAutoconf())) {
anchor.subscribe(this.topicRefSubscriber);
}
for (const anchor of this.getAllAnchors(this.getTopics())) {
anchor.subscribe(this.anchorSubscriber);
}
await this.refreshAutoconf();
await this.refresh();
}
/**
* Deconfigure this component
*
* @public
* @param {import("../../MqttController").DeconfigureOptions} [options]
* @return {Promise<void>}
*/
async deconfigure(options) {
if (options.unsubscribe) {
await this.hass.unsubscribe(this);
}
for (const anchor of this.getAllAnchors(this.getAutoconf())) {
anchor.unsubscribe(this.topicRefSubscriber);
}
for (const anchor of this.getAllAnchors(this.getTopics())) {
anchor.unsubscribe(this.anchorSubscriber);
}
if (options.cleanHass) {
await this.hass.dropAutoconf(this);
}
}
/**
* Helper function for debugging incorrect anchor usage during development. Enable with debug.debugHassAnchors
*
* @private
* @param {string} what What was being done by whowever called this function
* @param {object} json Object with unresolved anchors
*/
debugAnchors(what, json) {
if (!this.hass.debugAnchors) {
return;
}
Logger.debug("Failed " + what + " for Hass component of type " + this.componentType + " and id " + this.componentId + " due to the following unresolved anchors");
for (const anchor of this.getAllAnchors(json)) {
if (anchor.getValue() === null) {
Logger.debug(" - type " + anchor.getType() + " subtype " + anchor.getSubType());
}
}
try {
// noinspection ExceptionCaughtLocallyJS
throw new Error("See stack trace");
} catch (err) {
Logger.debug(err);
}
}
/**
* Ask the MQTT controller to refresh the Home Assistant autoconfig for this component.
*
* @return {Promise<void>}
*/
async refreshAutoconf() {
const resolved = this.hass.controller.hassAnchorProvider.resolveTopicReferences(this.getAutoconfInternal());
if (resolved === null) {
this.debugAnchors("autoconf", this.getAutoconf());
return;
}
await this.hass.refreshAutoconf(this, resolved);
}
/**
* Ask the MQTT controller to refresh the topics for this component.
*
* @public
* @return {Promise<void>}
*/
async refresh() {
const resolved = this.hass.controller.hassAnchorProvider.resolveAnchors(this.getTopics());
if (resolved === null) {
this.debugAnchors("refresh", this.getTopics());
return;
}
await this.hass.refresh(this, resolved);
}
/**
* Must be implemented to return the Hass autoconf payload.
* Anchors are allowed, but they may only change while the component is not configured. This is not enforced.
* Do not add the "device" boilerplate or availability topic stuff, they will be overwritten.
* You may NOT return null.
*
* @abstract
* @protected
* @return {object}
*/
getAutoconf() {
throw new NotImplementedError();
}
/**
* @private
* @return {object}
*/
getAutoconfInternal() {
return Object.assign(this.getAutoconf(), {
unique_id: this.componentId,
availability_topic: this.hass.controller.hassAnchorProvider.getTopicReference(HassAnchor.REFERENCE.AVAILABILITY),
payload_available: HomieCommonAttributes.STATE.READY,
// MqttController will try to send "lost" at least once before cleanly disconnecting
payload_not_available: HomieCommonAttributes.STATE.LOST,
device: this.hass.getAutoconfDeviceBoilerplate(),
});
}
/**
* Must be implemented to return the component values. Keys are topic names and values are payloads.
* Anchors are allowed, but they may only change while the component is not configured. This is not enforced.
* You may return null to signal that you are not ready.
*
* @protected
* @return {object|null}
*/
getTopics() {
return null;
}
/**
* Return topics that this component is interested in.
*
* @public
* @return {object}
*/
getInterestingTopics() {
return {};
}
}
module.exports = HassComponent;