Set entiteis to correct type (switch/light) and fix availability for scenes

- General cleanup and clarification of MQTT messages
This commit is contained in:
Victor Hagelbäck 2021-05-03 09:45:57 +02:00
parent 36e5c62b4f
commit 236e533c8a
3 changed files with 122 additions and 63 deletions

View file

@ -8,39 +8,37 @@ const startTopics = ['hass/status', 'homeassistant/status'];
const logger = Logger.getLogger('plejd-mqtt'); const logger = Logger.getLogger('plejd-mqtt');
// #region discovery
const discoveryPrefix = 'homeassistant'; const discoveryPrefix = 'homeassistant';
const nodeId = 'plejd'; const nodeId = 'plejd';
/** @type {import('./types/Mqtt').MQTT_TYPES} */
const MQTT_TYPES = { const MQTT_TYPES = {
LIGHT: 'light', LIGHT: 'light',
SCENE: 'scene', // A bit problematic. Will assume scene if length === guid SCENE: 'scene',
SWITCH: 'switch', SWITCH: 'switch',
DEVICE_AUTOMATION: 'device_automation',
}; };
const TOPICS = { /** @type {import('./types/Mqtt').TOPIC_TYPES} */
const TOPIC_TYPES = {
CONFIG: 'config', CONFIG: 'config',
STATE: 'state', STATE: 'state',
AVAILABILITY: 'availability', AVAILABILITY: 'availability',
COMMAND: 'set', COMMAND: 'set',
}; };
const getMqttType = (/** @type {{ uniqueId: string; type: string; }} */ plug) => (plug.type === 'switch' ? MQTT_TYPES.LIGHT : plug.type); const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) => `${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
const getBaseTopic = (/** @type {{ uniqueId: string; type: string; }} */ plug) => `${discoveryPrefix}/${getMqttType(plug)}/${nodeId}/${plug.uniqueId}`;
const getTopicName = ( const getTopicName = (
/** @type {{ uniqueId: string; type: string; }} */ plug, /** @type { string } */ uniqueId,
/** @type {'config' | 'state' | 'availability' | 'set'} */ topicType, /** @type { import('./types/Mqtt').MqttType } */ mqttDeviceType,
) => `${getBaseTopic(plug)}/${topicType}`; /** @type { import('./types/Mqtt').TopicType } */ topicType,
) => `${getBaseTopic(uniqueId, mqttDeviceType)}/${topicType}`;
const getSceneEventTopic = (sceneId) => `${getTopicName({ uniqueId: `${sceneId}_trigger`, type: 'device_automation' }, 'state')}`; const getTriggerUniqueId = (/** @type { string } */ uniqueId) => `${uniqueId}_trigger`;
const getSceneEventTopic = (/** @type {string} */ sceneId) => `${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
// Very loosely check if string is a GUID/UUID
const isGuid = (s) => /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/.test(s);
const decodeTopicRegexp = new RegExp( const decodeTopicRegexp = new RegExp(
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/, /(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
); );
@ -53,20 +51,18 @@ const decodeTopic = (topic) => {
return matches.groups; return matches.groups;
}; };
const getLightDiscoveryPayload = ( const getOutputDeviceDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ device, /** @type {import('./types/DeviceRegistry').OutputDevice} */ device,
) => ({ ) => ({
schema: 'json',
name: device.name, name: device.name,
unique_id: device.uniqueId, unique_id: device.uniqueId,
'~': getBaseTopic(device), '~': getBaseTopic(device.uniqueId, device.type),
state_topic: `~/${TOPICS.STATE}`, state_topic: `~/${TOPIC_TYPES.STATE}`,
command_topic: `~/${TOPICS.COMMAND}`, command_topic: `~/${TOPIC_TYPES.COMMAND}`,
availability_topic: `~/${TOPICS.AVAILABILITY}`, availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
optimistic: false, optimistic: false,
qos: 1, qos: 1,
retain: true, retain: true,
brightness: device.dimmable,
device: { device: {
identifiers: `${device.deviceId}`, identifiers: `${device.deviceId}`,
manufacturer: 'Plejd', manufacturer: 'Plejd',
@ -74,6 +70,7 @@ const getLightDiscoveryPayload = (
name: device.name, name: device.name,
sw_version: device.version, sw_version: device.version,
}, },
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
}); });
const getSceneDiscoveryPayload = ( const getSceneDiscoveryPayload = (
@ -81,9 +78,9 @@ const getSceneDiscoveryPayload = (
) => ({ ) => ({
name: sceneDevice.name, name: sceneDevice.name,
unique_id: sceneDevice.uniqueId, unique_id: sceneDevice.uniqueId,
'~': getBaseTopic(sceneDevice), '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
command_topic: `~/${TOPICS.COMMAND}`, command_topic: `~/${TOPIC_TYPES.COMMAND}`,
availability_topic: `~/${TOPICS.AVAILABILITY}`, availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
payload_on: 'ON', payload_on: 'ON',
qos: 1, qos: 1,
retain: false, retain: false,
@ -93,12 +90,9 @@ const getSceneDeviceTriggerhDiscoveryPayload = (
/** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice, /** @type {import('./types/DeviceRegistry').OutputDevice} */ sceneDevice,
) => ({ ) => ({
automation_type: 'trigger', automation_type: 'trigger',
'~': getBaseTopic({ '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION),
uniqueId: sceneDevice.uniqueId,
type: 'device_automation',
}),
qos: 1, qos: 1,
topic: `~/${TOPICS.STATE}`, topic: `~/${TOPIC_TYPES.STATE}`,
type: 'scene', type: 'scene',
subtype: 'trigger', subtype: 'trigger',
device: { device: {
@ -109,8 +103,6 @@ const getSceneDeviceTriggerhDiscoveryPayload = (
}, },
}); });
// #endregion
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF'); const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' }; const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
@ -192,8 +184,7 @@ class MqttClient extends EventEmitter {
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
let device; let device;
if (decodedTopic.type === MQTT_TYPES.SCENE && isGuid(decodedTopic.id)) { if (decodedTopic.type === MQTT_TYPES.SCENE) {
// UUID device id => It's a scene
logger.verbose(`Getting scene ${decodedTopic.id} from registry`); logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
device = this.deviceRegistry.getScene(decodedTopic.id); device = this.deviceRegistry.getScene(decodedTopic.id);
} else { } else {
@ -256,11 +247,29 @@ class MqttClient extends EventEmitter {
} }
disconnect(callback) { disconnect(callback) {
logger.info('Mqtt disconnect requested. Setting all devices as unavailable in HA...');
this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => { this.deviceRegistry.getAllOutputDevices().forEach((outputDevice) => {
this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.OFFLINE, { const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
retain: true, this.client.publish(
qos: 1, getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
}); AVAILABLILITY.OFFLINE,
{
retain: true,
qos: 1,
},
);
});
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
allSceneDevices.forEach((sceneDevice) => {
this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
AVAILABLILITY.OFFLINE,
{
retain: true,
qos: 1,
},
);
}); });
this.client.end(callback); this.client.end(callback);
} }
@ -271,20 +280,29 @@ class MqttClient extends EventEmitter {
allOutputDevices.forEach((outputDevice) => { allOutputDevices.forEach((outputDevice) => {
logger.debug(`Sending discovery for ${outputDevice.name}`); logger.debug(`Sending discovery for ${outputDevice.name}`);
const configPayload = getLightDiscoveryPayload(outputDevice); const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
logger.info( logger.info(
`Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`, `Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
); );
this.client.publish(getTopicName(outputDevice, 'config'), JSON.stringify(configPayload), { const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
retain: true, this.client.publish(
qos: 1, getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG),
}); JSON.stringify(configPayload),
setTimeout(() => { {
this.client.publish(getTopicName(outputDevice, 'availability'), AVAILABLILITY.ONLINE, {
retain: true, retain: true,
qos: 1, qos: 1,
}); },
);
setTimeout(() => {
this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
AVAILABLILITY.ONLINE,
{
retain: true,
qos: 1,
},
);
}, 2000); }, 2000);
}); });
@ -298,21 +316,22 @@ class MqttClient extends EventEmitter {
`Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`, `Discovered ${sceneDevice.typeName} (${sceneDevice.type}) named ${sceneDevice.name} (${sceneDevice.bleOutputAddress} : ${sceneDevice.uniqueId}).`,
); );
this.client.publish(getTopicName(sceneDevice, 'config'), JSON.stringify(sceneConfigPayload), { this.client.publish(
retain: true, getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
qos: 1, JSON.stringify(sceneConfigPayload),
}); {
retain: true,
qos: 1,
},
);
const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice); const sceneTriggerConfigPayload = getSceneDeviceTriggerhDiscoveryPayload(sceneDevice);
this.client.publish( this.client.publish(
getTopicName( getTopicName(
{ getTriggerUniqueId(sceneDevice.uniqueId),
...sceneDevice, MQTT_TYPES.DEVICE_AUTOMATION,
uniqueId: `${sceneDevice.uniqueId}_trigger`, TOPIC_TYPES.CONFIG,
type: 'device_automation',
},
'config',
), ),
JSON.stringify(sceneTriggerConfigPayload), JSON.stringify(sceneTriggerConfigPayload),
{ {
@ -322,10 +341,14 @@ class MqttClient extends EventEmitter {
); );
setTimeout(() => { setTimeout(() => {
this.client.publish(getTopicName(sceneDevice, 'availability'), AVAILABLILITY.ONLINE, { this.client.publish(
retain: true, getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
qos: 1, AVAILABLILITY.ONLINE,
}); {
retain: true,
qos: 1,
},
);
}, 2000); }, 2000);
}); });
} }
@ -366,11 +389,16 @@ class MqttClient extends EventEmitter {
payload = JSON.stringify(payload); payload = JSON.stringify(payload);
} }
this.client.publish(getTopicName(device, 'state'), payload, { retain: true, qos: 1 }); const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
this.client.publish(getTopicName(device, 'availability'), AVAILABLILITY.ONLINE, { this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
retain: true, retain: true,
qos: 1, qos: 1,
}); });
// this.client.publish(
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
// AVAILABLILITY.ONLINE,
// { retain: true, qos: 1 },
// );
} }
/** /**

View file

@ -348,7 +348,13 @@ class PlejdApi {
const dimmable = device.traits === TRAITS.DIMMABLE; const dimmable = device.traits === TRAITS.DIMMABLE;
// dimmable = settings.dimCurve !== 'NonDimmable'; // dimmable = settings.dimCurve !== 'NonDimmable';
const { name: typeName, type } = this._getDeviceType(plejdDevice); const { name: typeName, type: deviceType } = this._getDeviceType(plejdDevice);
let loadType = deviceType;
if (device.outputType === 'RELAY') {
loadType = 'switch';
} else if (device.outputType === 'LIGHT') {
loadType = 'light';
}
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const outputDevice = { const outputDevice = {
@ -361,7 +367,7 @@ class PlejdApi {
output: deviceOutput, output: deviceOutput,
roomId: device.roomId, roomId: device.roomId,
state: undefined, state: undefined,
type, type: loadType,
typeName, typeName,
version: plejdDevice.firmware.version, version: plejdDevice.firmware.version,
uniqueId: uniqueOutputId, uniqueId: uniqueOutputId,

25
plejd/types/Mqtt.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
/* eslint-disable no-use-before-define */
export type TopicType = 'config' | 'state' | 'availability' | 'set';
export type TOPIC_TYPES = { [key: string]: TopicType };
export type MqttType = 'light' | 'scene' | 'switch' | 'device_automation';
export type MQTT_TYPES = { [key: string]: MqttType };
export interface OutputDevice {
bleOutputAddress: number;
deviceId: string;
dim?: number;
dimmable: boolean;
hiddenFromRoomList?: boolean;
hiddenFromIntegrations?: boolean;
hiddenFromSceneList?: boolean;
name: string;
output: number;
roomId: string;
state: boolean | undefined;
type: string;
typeName: string;
version: string;
uniqueId: string;
}