From 30422edaa0c4ffd7ff5eef3fda9a1281b03678af Mon Sep 17 00:00:00 2001 From: BigThunderSR <17056173+BigThunderSR@users.noreply.github.com> Date: Tue, 26 Mar 2024 20:09:40 -0500 Subject: [PATCH] Added MQTT button auto-discovery for HA --- src/commands.js | 3 +- src/index.js | 20 +++++- src/mqtt.js | 75 +++++++++++++++++++- src/vehicle.js | 9 +++ test/mqtt.spec.js | 163 ++++++++++++++++++++++++++++++++++++++----- test/vehicle.spec.js | 14 ++++ 6 files changed, 260 insertions(+), 24 deletions(-) diff --git a/src/commands.js b/src/commands.js index 974df08c..da7194ff 100644 --- a/src/commands.js +++ b/src/commands.js @@ -135,8 +135,7 @@ class Commands { } async diagnostics({ diagnosticItem = [ - Commands.CONSTANTS.DIAGNOSTICS.AMBIENT_AIR_TEMPERATURE, - Commands.CONSTANTS.DIAGNOSTICS.ENGINE_RPM, + Commands.CONSTANTS.DIAGNOSTICS.AMBIENT_AIR_TEMPERATURE, Commands.CONSTANTS.DIAGNOSTICS.LAST_TRIP_DISTANCE, Commands.CONSTANTS.DIAGNOSTICS.ODOMETER, Commands.CONSTANTS.DIAGNOSTICS.TIRE_PRESSURE, diff --git a/src/index.js b/src/index.js index 8dd77ef4..156d62b4 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ const MQTT = require('./mqtt'); const Commands = require('./commands'); const logger = require('./logger'); const fs = require('fs'); +//const Buttons = require('./buttons'); //const CircularJSON = require('circular-json'); @@ -516,6 +517,21 @@ logger.info('Starting OnStar2MQTT Polling'); const v = vehicle; logger.info('Requesting diagnostics'); logger.debug(`GetSupported: ${v.getSupported()}`); + + // Get supported commands + logger.info(`GetSupportedCommands: ${v.getSupportedCommands()}`); + // Get button configs and payloads + const { buttonConfigs, configPayloads } = mqttHA.createButtonConfigPayload(v); + // Publish button config and payload for each button + buttonConfigs.forEach((buttonConfig, index) => { + const configPayload = configPayloads[index]; + logger.warn(`Button Config Topic: ${JSON.stringify(buttonConfig)}`); + logger.debug(`Button Config Payload: ${JSON.stringify(configPayload)}`); + // Publish configPayload as the payload to the respective MQTT topic + logger.debug(`Publishing Button Config: ${buttonConfig} Payload: ${JSON.stringify(configPayload)}`); + client.publish(buttonConfig, JSON.stringify(configPayload), { retain: true }); + }); + const statsRes = await commands.diagnostics({ diagnosticItem: v.getSupported() }); logger.info('Diagnostic request status', { status: _.get(statsRes, 'status') }); const stats = _.map( @@ -559,10 +575,12 @@ logger.info('Starting OnStar2MQTT Polling'); client.publish(topic, JSON.stringify(state), { retain: true }) ); } + await Promise.all(publishes); - //client.publish(pollingStatusTopicState, JSON.stringify({"ok":{"message":"Data Polled Successfully"}}), {retain: false}) + const completionTimestamp = new Date().toISOString(); logger.debug(`Completion Timestamp: ${completionTimestamp}`); + client.publish(pollingStatusTopicState, JSON.stringify({"ok":{"message":"Data Polled Successfully"}}), {retain: false}); client.publish(pollingStatusTopicState, JSON.stringify({ "error": { diff --git a/src/mqtt.js b/src/mqtt.js index 3a100ad4..4085b6ab 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -1,4 +1,5 @@ const _ = require('lodash'); +//const Buttons = require('./buttons'); /** * Supports Home Assistant MQTT Discovery (https://www.home-assistant.io/docs/mqtt/discovery/) @@ -44,6 +45,28 @@ const _ = require('lodash'); * } */ class MQTT { + static CONSTANTS = { + BUTTONS: { + Alert: 'alert', + AlertFlash: 'alertFlash', + AlertHonk: 'alertHonk', + CancelAlert: 'cancelAlert', + LockDoor: 'lockDoor', + UnlockDoor: 'unlockDoor', + LockTrunk: 'lockTrunk', + UnlockTrunk: 'unlockTrunk', + Start: 'start', + CancelStart: 'cancelStart', + GetLocation: 'getLocation', + Diagnostics: 'diagnostics', + EngineRPM: 'enginerpm', + ChargeOverride: 'chargeOverride', + CancelChargeOverride: 'cancelChargeOverride', + GetChargingProfile: 'getChargingProfile', + SetChargingProfile: 'setChargingProfile', + } + }; + constructor(vehicle, prefix = 'homeassistant', namePrefix) { this.prefix = prefix; this.vehicle = vehicle; @@ -114,6 +137,54 @@ class MQTT { return `${this.prefix}/device_tracker/${this.instance}/config`; } + //getButtonConfigTopic() { + // return `${this.prefix}/button/${this.instance}/${this.buttonName}/config`; + //} + + createButtonConfigPayload(vehicle) { + const buttonInstances = []; + const buttonConfigs = []; + const configPayloads = []; + + for (const buttonName in MQTT.CONSTANTS.BUTTONS) { + const buttonConfig = `${this.prefix}/button/${this.instance}/${MQTT.convertName(buttonName)}/config`; + const button = { + name: buttonName, + config: buttonConfig + }; + + button.vehicle = vehicle; + buttonInstances.push(button); + + let unique_id = `${vehicle.vin}_Command_${button.name}`; + unique_id = unique_id.replace(/\s+/g, '-').toLowerCase(); + + configPayloads.push({ + "device": { + "identifiers": [vehicle.vin], + "manufacturer": vehicle.make, + "model": vehicle.year + ' ' + vehicle.model, + "name": vehicle.toString() + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": unique_id, + "name": `Command ${button.name}`, + "command_topic": this.getCommandTopic(), + "payload_press": JSON.stringify({ "command": MQTT.CONSTANTS.BUTTONS[button.name] }), + "qos": 2, + "enabled_by_default": false, + }); + + buttonConfigs.push(buttonConfig); + } + + return { buttonInstances, buttonConfigs, configPayloads }; + } + /** * * @param {DiagnosticElement} diag @@ -189,7 +260,7 @@ class MQTT { device: { identifiers: [this.vehicle.vin], manufacturer: this.vehicle.make, - model: this.vehicle.year, + model: this.vehicle.year + ' ' + this.vehicle.model, name: this.vehicle.toString() }, availability_topic: this.getAvailabilityTopic(), @@ -299,4 +370,4 @@ class MQTT { } } -module.exports = MQTT; \ No newline at end of file +module.exports = MQTT; diff --git a/src/vehicle.js b/src/vehicle.js index 23f8b7d6..4e507cbd 100644 --- a/src/vehicle.js +++ b/src/vehicle.js @@ -13,6 +13,8 @@ class Vehicle { ); this.supportedDiagnostics = _.get(diagCmd, 'commandData.supportedDiagnostics.supportedDiagnostic'); + + this.supportedCommands = _.get(vehicle, 'commands.command'); } isSupported(diag) { @@ -29,6 +31,13 @@ class Vehicle { toString() { return `${this.year} ${this.make} ${this.model}`; } + + getSupportedCommands(commandList = []) { + this.supportedCommands.forEach(command => { + commandList.push(command.name); + }); + return commandList; + } } module.exports = Vehicle; \ No newline at end of file diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index 1c43d42f..c17ae587 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -99,7 +99,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, //message: 'na', @@ -122,7 +122,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, //message: 'na', @@ -161,7 +161,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'total_increasing', @@ -183,7 +183,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'total_increasing', @@ -219,7 +219,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, //message: 'na', @@ -252,7 +252,7 @@ describe('MQTT', () => { }); }); - describe('attributes', () => { +/* describe('attributes', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]'))); it('should generate payloads with an attribute', () => { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { @@ -262,7 +262,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, //message: 'YELLOW', @@ -279,11 +279,86 @@ describe('MQTT', () => { value_template: '{{ value_json.tire_pressure_lf }}' }); }); - }); + }); */ describe('attributes', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]'))); - it('should generate payloads with an attribute', () => { + it('should generate payloads with an attribute for left front tire', () => { + assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { + availability_topic: 'homeassistant/XXX/available', + device: { + identifiers: [ + 'XXX' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar' + }, + + state_class: 'measurement', + device_class: 'pressure', + json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_front, 'message': value_json.tire_pressure_lf_message} | tojson }}", + name: 'Tire Pressure: Left Front', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unique_id: 'xxx-tire-pressure-lf', + json_attributes_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unit_of_measurement: 'kPa', + value_template: '{{ value_json.tire_pressure_lf }}' + }); + }); + it('should generate payloads with an attribute for right front tire', () => { + assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[4]), { + availability_topic: 'homeassistant/XXX/available', + device: { + identifiers: [ + 'XXX' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar' + }, + + state_class: 'measurement', + device_class: 'pressure', + json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_front, 'message': value_json.tire_pressure_rf_message} | tojson }}", + name: 'Tire Pressure: Right Front', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unique_id: 'xxx-tire-pressure-rf', + json_attributes_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unit_of_measurement: 'kPa', + value_template: '{{ value_json.tire_pressure_rf }}' + }); + }); + it('should generate payloads with an attribute for left rear tire', () => { + assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), { + availability_topic: 'homeassistant/XXX/available', + device: { + identifiers: [ + 'XXX' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar' + }, + + state_class: 'measurement', + device_class: 'pressure', + json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_rear, 'message': value_json.tire_pressure_lr_message} | tojson }}", + name: 'Tire Pressure: Left Rear', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unique_id: 'xxx-tire-pressure-lr', + json_attributes_topic: 'homeassistant/sensor/XXX/tire_pressure/state', + unit_of_measurement: 'kPa', + value_template: '{{ value_json.tire_pressure_lr }}' + }); + }); + it('should generate payloads with an attribute for right rear tire', () => { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[5]), { availability_topic: 'homeassistant/XXX/available', device: { @@ -291,10 +366,10 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, - //message: 'YELLOW', + state_class: 'measurement', device_class: 'pressure', json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_rear, 'message': value_json.tire_pressure_rr_message} | tojson }}", @@ -320,7 +395,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, //message: 'YELLOW', @@ -349,7 +424,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'measurement', @@ -371,7 +446,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'measurement', @@ -393,7 +468,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'measurement', @@ -439,7 +514,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'measurement', @@ -475,7 +550,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: 'total_increasing', @@ -511,7 +586,7 @@ describe('MQTT', () => { 'XXX' ], manufacturer: 'foo', - model: 2020, + model: '2020 bar', name: '2020 foo bar' }, state_class: undefined, @@ -536,6 +611,56 @@ describe('MQTT', () => { }); }); - + describe('createButtonConfigPayload', () => { + it('should generate button config payloads', () => { + const vehicle = new Vehicle({make: 'foo', model: 'bar', vin: 'XXX', year: 2020}); + const mqtt = new MQTT(vehicle); + + const expectedButtonInstances = []; + const expectedButtonConfigs = []; + const expectedConfigPayloads = []; + + for (const buttonName in MQTT.CONSTANTS.BUTTONS) { + const buttonConfig = `homeassistant/button/XXX/${MQTT.convertName(buttonName)}/config`; + const button = { + name: buttonName, + config: buttonConfig, + vehicle: vehicle + }; + + expectedButtonInstances.push(button); + expectedButtonConfigs.push(buttonConfig); + + let unique_id = `${vehicle.vin}_Command_${button.name}`; + unique_id = unique_id.replace(/\s+/g, '-').toLowerCase(); + + expectedConfigPayloads.push({ + "device": { + "identifiers": [vehicle.vin], + "manufacturer": vehicle.make, + "model": vehicle.year + ' ' + vehicle.model, + "name": vehicle.toString() + }, + "availability": { + "topic": mqtt.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": unique_id, + "name": `Command ${button.name}`, + "command_topic": mqtt.getCommandTopic(), + "payload_press": JSON.stringify({ "command": MQTT.CONSTANTS.BUTTONS[button.name] }), + "qos": 2, + "enabled_by_default": false, + }); + } + + const result = mqtt.createButtonConfigPayload(vehicle); + assert.deepStrictEqual(result.buttonInstances, expectedButtonInstances); + assert.deepStrictEqual(result.buttonConfigs, expectedButtonConfigs); + assert.deepStrictEqual(result.configPayloads, expectedConfigPayloads); + }); + }); + }); }); diff --git a/test/vehicle.spec.js b/test/vehicle.spec.js index bb8facd8..64bb130f 100644 --- a/test/vehicle.spec.js +++ b/test/vehicle.spec.js @@ -38,4 +38,18 @@ describe('Vehicle', () => { it('should toString() correctly', () => { assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV') }); + + it('should return the list of supported commands', () => { + const supported = v.getSupportedCommands(); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 29); + }); + + it('should return the list of supported commands with provided command list', () => { + const commandList = []; + const supported = v.getSupportedCommands(commandList); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 29); + assert.deepStrictEqual(supported, commandList); + }); });