From c97350bb3cd206904ad87abbd93dc9ea1bfd1a62 Mon Sep 17 00:00:00 2001 From: BigThunderSR <17056173+BigThunderSR@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:45:58 -0500 Subject: [PATCH] Add polling status sensors to MQTT Auto-discovery --- src/index.js | 136 ++++++++++----------- src/mqtt.js | 231 ++++++++++++++++++++++++++++++++++- test/commands.spec.js | 120 +++++++++++++++++++ test/measurement.spec.js | 96 +++++++++++++++ test/mqtt.spec.js | 251 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 748 insertions(+), 86 deletions(-) create mode 100644 test/commands.spec.js create mode 100644 test/measurement.spec.js diff --git a/src/index.js b/src/index.js index fc5576eb..328a4a62 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ const Commands = require('./commands'); const logger = require('./logger'); const fs = require('fs'); //const CircularJSON = require('circular-json'); - +let buttonConfigsPublished = '' const onstarConfig = { deviceId: process.env.ONSTAR_DEVICEID || uuidv4(), @@ -146,70 +146,18 @@ const configureMQTT = async (commands, client, mqttHA) => { const topicArray = _.concat({ topic }, '/', { command }.command, '/', 'state'); const commandStatusTopic = topicArray.map(item => item.topic || item).join(''); - function createCommandStatusSensorConfigPayload() { - return { - "device": { - "identifiers": [mqttHA.vehicle.vin + "_Command_Status_Monitor"], - "manufacturer": mqttHA.vehicle.make, - "model": mqttHA.vehicle.year + ' ' + mqttHA.vehicle.model, - "name": mqttHA.vehicle.toString() + ' Command Status Monitor Sensors', - "suggested_area": mqttHA.vehicle.toString() + ' Command Status Monitor Sensors', - }, - "availability": { - "topic": mqttHA.getAvailabilityTopic(), - "payload_available": 'true', - "payload_not_available": 'false', - }, - "unique_id": (MQTT.convertName(mqttHA.vehicle.vin)) + "_" + (MQTT.convertName(command)) + "_command_status_monitor", - "name": 'Command ' + command + ' Status Monitor', - "state_topic": commandStatusTopic, - "value_template": "{{ value_json.command.error.message }}", - "icon": "mdi:message-alert", - "suggested_area": mqttHA.vehicle.toString() + ' Command Status Monitor Sensors', - - }; - } - - function createCommandStatusSensorTimestampConfigPayload() { - return { - "device": { - "identifiers": [mqttHA.vehicle.vin + "_Command_Status_Monitor"], - "manufacturer": mqttHA.vehicle.make, - "model": mqttHA.vehicle.year + ' ' + mqttHA.vehicle.model, - "name": mqttHA.vehicle.toString() + ' Command Status Monitor Sensors', - "suggested_area": mqttHA.vehicle.toString() + ' Sensors', - }, - "availability": { - "topic": mqttHA.getAvailabilityTopic(), - "payload_available": 'true', - "payload_not_available": 'false', - }, - "unique_id": (MQTT.convertName(mqttHA.vehicle.vin)) + "_" + (MQTT.convertName(command)) + "_command_status_timestamp_monitor", - "name": 'Command ' + command + ' Status Monitor Timestamp', - "state_topic": commandStatusTopic, - "value_template": "{{ value_json.completionTimestamp }}", - "device_class": "timestamp", - "icon": "mdi:calendar-clock", - }; - } - - const commandStatusSensorConfigTopic = mqttHA.getCommandStatusSensorConfigTopic() + '/' + command + '_status_monitor' + '/' + 'config'; - logger.debug(`Command Status Sensor Config Topic: ${commandStatusSensorConfigTopic}`); - const commandStatusSensorConfigPayload = createCommandStatusSensorConfigPayload({ command }); - logger.debug("Command Status Sensor Config Payload:", commandStatusSensorConfigPayload); - - const commandStatusSensorTimestampConfigTopic = mqttHA.getCommandStatusSensorConfigTopic() + '/' + command + '_status_timestamp' + '/' + 'config'; - logger.debug(`Command Status Sensor Timestamp Config Topic: ${commandStatusSensorTimestampConfigTopic}`); - const commandStatusSensorTimestampConfigPayload = createCommandStatusSensorTimestampConfigPayload({ command }); - logger.debug("Command Status Sensor Timestamp Config Payload:", commandStatusSensorTimestampConfigPayload); + const commandStatusSensorConfig = mqttHA.createCommandStatusSensorConfigPayload(command); + logger.debug("Command Status Sensor Config:", commandStatusSensorConfig); + const commandStatusSensorTimestampConfig = mqttHA.createCommandStatusSensorTimestampConfigPayload(command); + logger.debug("Command Status Sensor Timestamp Config:", commandStatusSensorTimestampConfig); const commandFn = cmd.bind(commands); logger.debug(`List of const: Command: ${command}, cmd: ${cmd}, commandFn: ${commandFn.toString()}, options: ${options}`); if (command === 'diagnostics' || command === 'enginerpm') { logger.warn('Command sent:', { command }); logger.warn(`Command Status Topic: ${commandStatusTopic}`); - client.publish(commandStatusSensorConfigTopic, JSON.stringify(commandStatusSensorConfigPayload), { retain: true }); - client.publish(commandStatusSensorTimestampConfigTopic, JSON.stringify(commandStatusSensorTimestampConfigPayload), { retain: true }); + client.publish(commandStatusSensorConfig.topic, JSON.stringify(commandStatusSensorConfig.payload), { retain: true }); + client.publish(commandStatusSensorTimestampConfig.topic, JSON.stringify(commandStatusSensorTimestampConfig.payload), { retain: true }); client.publish(commandStatusTopic, JSON.stringify({ "command": { @@ -354,8 +302,8 @@ const configureMQTT = async (commands, client, mqttHA) => { //logger.debug(`Command sent: Command: ${ command }, ModifiedOptions: ${ modifiedOptions }`); logger.warn(`Command Status Topic: ${commandStatusTopic}`); - client.publish(commandStatusSensorConfigTopic, JSON.stringify(commandStatusSensorConfigPayload), { retain: true }); - client.publish(commandStatusSensorTimestampConfigTopic, JSON.stringify(commandStatusSensorTimestampConfigPayload), { retain: true }); + client.publish(commandStatusSensorConfig.topic, JSON.stringify(commandStatusSensorConfig.payload), { retain: true }); + client.publish(commandStatusSensorTimestampConfig.topic, JSON.stringify(commandStatusSensorTimestampConfig.payload), { retain: true }); client.publish(commandStatusTopic, JSON.stringify({ "command": { @@ -432,12 +380,11 @@ const configureMQTT = async (commands, client, mqttHA) => { }); } - else { logger.warn('Command sent:', { command }, { options }); logger.warn(`Command Status Topic: ${commandStatusTopic}`); - client.publish(commandStatusSensorConfigTopic, JSON.stringify(commandStatusSensorConfigPayload), { retain: true }); - client.publish(commandStatusSensorTimestampConfigTopic, JSON.stringify(commandStatusSensorTimestampConfigPayload), { retain: true }); + client.publish(commandStatusSensorConfig.topic, JSON.stringify(commandStatusSensorConfig.payload), { retain: true }); + client.publish(commandStatusSensorTimestampConfig.topic, JSON.stringify(commandStatusSensorTimestampConfig.payload), { retain: true }); client.publish(commandStatusTopic, JSON.stringify({ "command": { @@ -568,6 +515,14 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); } const pollingStatusTopicState = topicArray.map(item => item.topic || item).join(''); logger.info(`pollingStatusTopicState: ${pollingStatusTopicState}`); + + const pollingStatusMessagePayload = mqttHA.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState); + logger.debug("pollingStatusMessagePayload:", pollingStatusMessagePayload); + const pollingStatusCodePayload = mqttHA.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState); + logger.debug("pollingStatusCodePayload:", pollingStatusCodePayload); + const pollingStatusMessageTimestampPayload = mqttHA.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState); + logger.debug("pollingStatusMessageTimestampPayload:", pollingStatusMessageTimestampPayload); + client.publish(pollingStatusTopicState, JSON.stringify({ "error": { @@ -580,6 +535,13 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); "completionTimestamp": new Date().toISOString() }), { retain: false }) + if (!buttonConfigsPublished) { + client.publish(pollingStatusMessagePayload.topic, JSON.stringify(pollingStatusMessagePayload.payload), { retain: true }); + client.publish(pollingStatusCodePayload.topic, JSON.stringify(pollingStatusCodePayload.payload), { retain: true }); + client.publish(pollingStatusMessageTimestampPayload.topic, JSON.stringify(pollingStatusMessageTimestampPayload.payload), { retain: true }); + logger.info(`Polling Status Message Sensors Published!`); + } + let topicArrayTF; if (!mqttConfig.pollingStatusTopic) { topicArrayTF = _.concat(mqttHA.getPollingStatusTopic(), '/', 'lastpollsuccessful'); @@ -588,6 +550,14 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); } const pollingStatusTopicTF = topicArrayTF.map(item => item.topic || item).join(''); logger.info(`pollingStatusTopicTF, ${pollingStatusTopicTF}`); + + if (!buttonConfigsPublished) { + const pollingStatusTFPayload = mqttHA.createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF); + logger.debug("pollingStatusTFPayload:", pollingStatusTFPayload); + client.publish(pollingStatusTFPayload.topic, JSON.stringify(pollingStatusTFPayload.payload), { retain: true }); + logger.info(`Polling Status TF Sensor Published!`); + } + client.publish(pollingStatusTopicTF, "false", { retain: true }); const states = new Map(); @@ -595,24 +565,32 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); logger.info('Requesting diagnostics'); logger.debug(`GetSupported: ${v.getSupported()}`); - let buttonConfigsPublished = '' - function publishButtonConfigs() { // Only run on initial startup if (!buttonConfigsPublished) { // Get supported commands logger.debug(`Supported Commands: ${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 tasks = [ + mqttHA.createButtonConfigPayload(v), + mqttHA.createButtonConfigPayloadCSMG(v) + ]; + + tasks.forEach(({ buttonConfigs, configPayloads }, taskIndex) => { + // Publish button config and payload for each button in first set + buttonConfigs.forEach((buttonConfig, index) => { + const configPayload = configPayloads[index]; + const buttonType = taskIndex === 0 ? "Button" : "Button for Command Status"; + logger.warn(`${buttonType} Config Topic: ${JSON.stringify(buttonConfig)}`); + logger.debug(`${buttonType} Config Payload: ${JSON.stringify(configPayload)}`); + + // Publish configPayload as the payload to the respective MQTT topic + logger.debug(`Publishing ${buttonType} Config: ${buttonConfig} Payload: ${JSON.stringify(configPayload)}`); + client.publish(buttonConfig, JSON.stringify(configPayload), { retain: true }); + }); }); + buttonConfigsPublished = 'true'; logger.info(`Button Configs Published!`); } @@ -752,6 +730,14 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); const refreshIntervalCurrentValTopic = mqttHA.getRefreshIntervalCurrentValTopic(); logger.info(`refreshIntervalTopic: ${refreshIntervalTopic}`); logger.info(`refreshIntervalCurrentValTopic: ${refreshIntervalCurrentValTopic}`); + + if (!buttonConfigsPublished) { + const pollingRefreshIntervalPayload = mqttHA.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic); + logger.debug("pollingRefreshIntervalSensorConfigPayload:", pollingRefreshIntervalPayload); + client.publish(pollingRefreshIntervalPayload.topic, pollingRefreshIntervalPayload.payload, { retain: true }); + logger.info(`Polling Refresh Interval Sensor Published!`); + } + // Subscribe to the topic client.subscribe(refreshIntervalTopic); // Set initial interval diff --git a/src/mqtt.js b/src/mqtt.js index 0a6eca45..047e4e5e 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -137,8 +137,60 @@ class MQTT { return `${this.prefix}/device_tracker/${this.instance}/config`; } - getCommandStatusSensorConfigTopic() { - return `${this.prefix}/sensor/${this.instance}`; + // getCommandStatusSensorConfigTopic() { + // return `${this.prefix}/sensor/${this.instance}`; + // } + + createCommandStatusSensorConfigPayload(command) { + let topic = `${this.prefix}/sensor/${this.instance}/${command}_status_monitor/config`; + let commandStatusTopic = `${this.prefix}/sensor/${this.instance}/${MQTT.convertName(command)}/state`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_" + (MQTT.convertName(command)) + "_command_status_monitor", + "name": 'Command ' + command + ' Status Monitor', + "state_topic": commandStatusTopic, + "value_template": "{{ value_json.command.error.message }}", + "icon": "mdi:message-alert", + }; + return { topic, payload }; + } + + createCommandStatusSensorTimestampConfigPayload(command) { + let topic = `${this.prefix}/sensor/${this.instance}/${command}_status_timestamp/config`; + let commandStatusTopic = `${this.prefix}/sensor/${this.instance}/${MQTT.convertName(command)}/state`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_" + (MQTT.convertName(command)) + "_command_status_timestamp_monitor", + "name": 'Command ' + command + ' Status Monitor Timestamp', + "state_topic": commandStatusTopic, + "value_template": "{{ value_json.completionTimestamp }}", + "device_class": "timestamp", + "icon": "mdi:calendar-clock", + } + + return { topic, payload }; } //getButtonConfigTopic() { @@ -168,7 +220,8 @@ class MQTT { "identifiers": [vehicle.vin], "manufacturer": vehicle.make, "model": vehicle.year + ' ' + vehicle.model, - "name": vehicle.toString() + "name": vehicle.toString(), + "suggested_area": vehicle.toString(), }, "availability": { "topic": this.getAvailabilityTopic(), @@ -189,6 +242,178 @@ class MQTT { return { buttonInstances, buttonConfigs, configPayloads }; } + createButtonConfigPayloadCSMG(vehicle) { + const buttonInstances = []; + const buttonConfigs = []; + const configPayloads = []; + + for (const buttonName in MQTT.CONSTANTS.BUTTONS) { + const buttonConfig = `${this.prefix}/button/${this.instance}/${MQTT.convertName(buttonName)}_monitor/config`; + const button = { + name: buttonName, + config: buttonConfig + }; + + button.vehicle = vehicle; + buttonInstances.push(button); + + let unique_id = `${vehicle.vin}_Command_${button.name}_Monitor`; + unique_id = unique_id.replace(/\s+/g, '-').toLowerCase(); + + configPayloads.push({ + "device": { + "identifiers": [vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": vehicle.make, + "model": vehicle.year + ' ' + vehicle.model, + "name": vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "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 }; + } + + createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState) { + let topic = `${this.prefix}/sensor/${this.instance}/polling_status_message/config`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_polling_status_message", + "name": 'Polling Status Message', + "state_topic": pollingStatusTopicState, + "value_template": "{{ value_json.error.message }}", + "icon": "mdi:message-alert", + }; + return { topic, payload }; + } + + createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState) { + let topic = `${this.prefix}/sensor/${this.instance}/polling_status_code/config`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_polling_status_code", + "name": 'Polling Status Code', + "state_topic": pollingStatusTopicState, + "value_template": "{{ value_json.error.response.status | int(0) }}", + "icon": "mdi:sync-alert", + }; + return { topic, payload }; + } + + createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState) { + let topic = `${this.prefix}/sensor/${this.instance}/polling_status_timestamp/config`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_polling_status_timestamp", + "name": 'Polling Status Timestamp', + "state_topic": pollingStatusTopicState, + "value_template": "{{ value_json.completionTimestamp }}", + "device_class": "timestamp", + "icon": "mdi:calendar-clock", + }; + return { topic, payload }; + } + + createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic) { + let topic = `${this.prefix}/sensor/${this.instance}/polling_refresh_interval/config`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_polling_refresh_interval", + "name": 'Polling Refresh Interval', + "state_topic": refreshIntervalCurrentValTopic, + "value_template": "{{ value | int(0) }}", + "icon": "mdi:timer-check-outline", + "unit_of_measurement": "ms", + "state_class": "measurement", + "device_class": "duration", + }; + return { topic, payload }; + } + + createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF) { + let topic = `${this.prefix}/binary_sensor/${this.instance}/polling_status_tf/config`; + let payload = { + "device": { + "identifiers": [this.vehicle.vin + "_Command_Status_Monitor"], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString() + ' Command Status Monitor Sensors', + "suggested_area": this.vehicle.toString() + ' Command Status Monitor Sensors', + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": (MQTT.convertName(this.vehicle.vin)) + "_onstar_polling_status_successful", + "name": 'Polling Status Successful', + "state_topic": pollingStatusTopicTF, + "payload_on": "false", + "payload_off": "true", + "device_class": "problem", + "icon": "mdi:sync-alert", + }; + return { topic, payload }; + } + + /** * * @param {DiagnosticElement} diag diff --git a/test/commands.spec.js b/test/commands.spec.js new file mode 100644 index 00000000..58169fcc --- /dev/null +++ b/test/commands.spec.js @@ -0,0 +1,120 @@ +const assert = require('assert'); +const Commands = require('../src/commands'); + +describe('Commands', () => { + let commands; + + beforeEach(() => { + // Create a mock onstar object + const onstarMock = { + getAccountVehicles: () => Promise.resolve(), + start: () => Promise.resolve(), + cancelStart: () => Promise.resolve(), + alert: () => Promise.resolve(), + cancelAlert: () => Promise.resolve(), + lockDoor: () => Promise.resolve(), + unlockDoor: () => Promise.resolve(), + lockTrunk: () => Promise.resolve(), + unlockTrunk: () => Promise.resolve(), + chargeOverride: () => Promise.resolve(), + cancelChargeOverride: () => Promise.resolve(), + getChargingProfile: () => Promise.resolve(), + setChargingProfile: () => Promise.resolve(), + location: () => Promise.resolve(), + diagnostics: () => Promise.resolve(), + enginerpm: () => Promise.resolve(), + }; + + commands = new Commands(onstarMock); + }); + + it('should call getAccountVehicles method', async () => { + const result = await commands.getAccountVehicles(); + assert.strictEqual(result, undefined); + }); + + it('should call startVehicle method', async () => { + const result = await commands.startVehicle(); + assert.strictEqual(result, undefined); + }); + + it('should call cancelStartVehicle method', async () => { + const result = await commands.cancelStartVehicle(); + assert.strictEqual(result, undefined); + }); + + it('should call alert method', async () => { + const result = await commands.alert({}); + assert.strictEqual(result, undefined); + }); + + it('should call alertFlash method', async () => { + const result = await commands.alertFlash({}); + assert.strictEqual(result, undefined); + }); + + it('should call alertHonk method', async () => { + const result = await commands.alertHonk({}); + assert.strictEqual(result, undefined); + }); + + it('should call cancelAlert method', async () => { + const result = await commands.cancelAlert(); + assert.strictEqual(result, undefined); + }); + + it('should call lockDoor method', async () => { + const result = await commands.lockDoor({}); + assert.strictEqual(result, undefined); + }); + + it('should call unlockDoor method', async () => { + const result = await commands.unlockDoor({}); + assert.strictEqual(result, undefined); + }); + + it('should call lockTrunk method', async () => { + const result = await commands.lockTrunk({}); + assert.strictEqual(result, undefined); + }); + + it('should call unlockTrunk method', async () => { + const result = await commands.unlockTrunk({}); + assert.strictEqual(result, undefined); + }); + + it('should call chargeOverride method', async () => { + const result = await commands.chargeOverride({}); + assert.strictEqual(result, undefined); + }); + + it('should call cancelChargeOverride method', async () => { + const result = await commands.cancelChargeOverride({}); + assert.strictEqual(result, undefined); + }); + + it('should call getChargingProfile method', async () => { + const result = await commands.getChargingProfile(); + assert.strictEqual(result, undefined); + }); + + it('should call setChargingProfile method', async () => { + const result = await commands.setChargingProfile({}); + assert.strictEqual(result, undefined); + }); + + it('should call getLocation method', async () => { + const result = await commands.getLocation(); + assert.strictEqual(result, undefined); + }); + + it('should call diagnostics method', async () => { + const result = await commands.diagnostics({}); + assert.strictEqual(result, undefined); + }); + + it('should call enginerpm method', async () => { + const result = await commands.enginerpm({}); + assert.strictEqual(result, undefined); + }); +}); \ No newline at end of file diff --git a/test/measurement.spec.js b/test/measurement.spec.js new file mode 100644 index 00000000..97704104 --- /dev/null +++ b/test/measurement.spec.js @@ -0,0 +1,96 @@ +const assert = require('assert'); +const Measurement = require('../src/measurement'); + +describe('Measurement', () => { + describe('constructor', () => { + it('should set the value and unit correctly', () => { + const measurement = new Measurement(10, 'km'); + assert.strictEqual(measurement.value, 10); + assert.strictEqual(measurement.unit, 'km'); + }); + + it('should correct the unit name', () => { + const measurement = new Measurement(20, 'Cel'); + assert.strictEqual(measurement.unit, '°C'); + }); + + it('should determine if the unit is convertible', () => { + const measurement1 = new Measurement(30, 'km'); + assert.strictEqual(measurement1.isConvertible, true); + + const measurement2 = new Measurement(40, 'V'); + assert.strictEqual(measurement2.isConvertible, false); + }); + }); + + describe('convertValue', () => { + it('should convert the value correctly for °C to °F', () => { + const convertedValue = Measurement.convertValue(25, '°C'); + assert.strictEqual(convertedValue, 77); + }); + + it('should convert the value correctly for km to mi', () => { + const convertedValue = Measurement.convertValue(100, 'km'); + assert.strictEqual(convertedValue, 62.1); + }); + + it('should convert the value correctly for kPa to psi', () => { + const convertedValue = Measurement.convertValue(200, 'kPa'); + assert.strictEqual(convertedValue, 29); + }); + + it('should convert the value correctly for km/L(e) to mpg(e)', () => { + const convertedValue = Measurement.convertValue(10, 'km/L(e)'); + assert.strictEqual(convertedValue, 23.5); + }); + + it('should convert the value correctly for km/L to mpg', () => { + const convertedValue = Measurement.convertValue(15, 'km/L'); + assert.strictEqual(convertedValue, 35.3); + }); + + it('should convert the value correctly for L to gal', () => { + const convertedValue = Measurement.convertValue(50, 'L'); + assert.strictEqual(convertedValue, 13.2); + }); + }); + + describe('convertUnit', () => { + it('should convert the unit correctly for °C to °F', () => { + const convertedUnit = Measurement.convertUnit('°C'); + assert.strictEqual(convertedUnit, '°F'); + }); + + it('should convert the unit correctly for km to mi', () => { + const convertedUnit = Measurement.convertUnit('km'); + assert.strictEqual(convertedUnit, 'mi'); + }); + + it('should convert the unit correctly for kPa to psi', () => { + const convertedUnit = Measurement.convertUnit('kPa'); + assert.strictEqual(convertedUnit, 'psi'); + }); + + it('should convert the unit correctly for km/L(e) to mpg(e)', () => { + const convertedUnit = Measurement.convertUnit('km/L(e)'); + assert.strictEqual(convertedUnit, 'mpg(e)'); + }); + + it('should convert the unit correctly for km/L to mpg', () => { + const convertedUnit = Measurement.convertUnit('km/L'); + assert.strictEqual(convertedUnit, 'mpg'); + }); + + it('should convert the unit correctly for L to gal', () => { + const convertedUnit = Measurement.convertUnit('L'); + assert.strictEqual(convertedUnit, 'gal'); + }); + }); + + describe('toString', () => { + it('should return the string representation of the measurement', () => { + const measurement = new Measurement(50, 'km'); + assert.strictEqual(measurement.toString(), '50km'); + }); + }); +}); \ No newline at end of file diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index 3d8f0138..3b166d20 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -627,10 +627,242 @@ describe('MQTT', () => { }); }); + describe('createCommandStatusSensorConfigPayload', () => { + it('should generate command status sensor config payload', () => { + const command = 'lock'; + const expectedConfigPayload = { + topic: "homeassistant/sensor/XXX/lock_status_monitor/config", + payload: { + availability: { + payload_available: 'true', + payload_not_available: 'false', + topic: "homeassistant/XXX/available", + }, + device: { + identifiers: [ + 'XXX_Command_Status_Monitor' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar Command Status Monitor Sensors', + suggested_area: "2020 foo bar Command Status Monitor Sensors", + }, + icon: 'mdi:message-alert', + name: 'Command lock Status Monitor', + state_topic: 'homeassistant/sensor/XXX/lock/state', + unique_id: 'xxx_lock_command_status_monitor', + value_template: '{{ value_json.command.error.message }}', + } + }; + const result = mqtt.createCommandStatusSensorConfigPayload(command); + assert.deepStrictEqual(result, expectedConfigPayload); + }); + }); + + + describe('createCommandStatusSensorTimestampConfigPayload', () => { + it('should generate command status sensor timestamp config payload', () => { + const command = 'lock'; + const expectedConfigPayload = { + topic: "homeassistant/sensor/XXX/lock_status_timestamp/config", + payload: { + availability: { + payload_available: 'true', + payload_not_available: 'false', + topic: "homeassistant/XXX/available" + }, + device: { + identifiers: [ + 'XXX_Command_Status_Monitor' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar Command Status Monitor Sensors', + suggested_area: "2020 foo bar Command Status Monitor Sensors", + }, + device_class: "timestamp", + icon: "mdi:calendar-clock", + name: 'Command lock Status Monitor Timestamp', + state_topic: 'homeassistant/sensor/XXX/lock/state', + unique_id: 'xxx_lock_command_status_timestamp_monitor', + value_template: '{{ value_json.completionTimestamp }}', + } + }; + const result = mqtt.createCommandStatusSensorTimestampConfigPayload(command); + assert.deepStrictEqual(result, expectedConfigPayload); + }); + }); + + describe('createPollingStatusMessageSensorConfigPayload', () => { + it('should generate the correct sensor config payload for polling status message', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status/state'; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_message/config'; + const expectedPayload = { + "device": { + "identifiers": ['XXX_Command_Status_Monitor'], + "manufacturer": 'foo', + "model": '2020 bar', + "name": '2020 foo bar Command Status Monitor Sensors', + "suggested_area": '2020 foo bar Command Status Monitor Sensors', + }, + "availability": { + "topic": 'homeassistant/XXX/available', + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": 'xxx_polling_status_message', + "name": 'Polling Status Message', + "state_topic": pollingStatusTopicState, + "value_template": "{{ value_json.error.message }}", + "icon": "mdi:message-alert", + }; + + const result = mqtt.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + }); + + describe('createPollingStatusCodeSensorConfigPayload', () => { + it('should generate the correct config payload for polling status code sensor', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_code/state'; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_code/config'; + const expectedPayload = { + device: { + identifiers: ['XXX_Command_Status_Monitor'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar Command Status Monitor Sensors', + suggested_area: '2020 foo bar Command Status Monitor Sensors', + }, + availability: { + topic: 'homeassistant/XXX/available', + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: 'xxx_polling_status_code', + name: 'Polling Status Code', + state_topic: pollingStatusTopicState, + value_template: '{{ value_json.error.response.status | int(0) }}', + icon: 'mdi:sync-alert', + }; + + const result = mqtt.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + }); + + describe('createPollingStatusTimestampSensorConfigPayload', () => { + it('should generate the correct config payload', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_timestamp/state'; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_timestamp/config'; + const expectedPayload = { + device: { + identifiers: ['XXX_Command_Status_Monitor'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar Command Status Monitor Sensors', + suggested_area: '2020 foo bar Command Status Monitor Sensors', + }, + availability: { + topic: 'homeassistant/XXX/available', + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: 'xxx_polling_status_timestamp', + name: 'Polling Status Timestamp', + state_topic: pollingStatusTopicState, + value_template: '{{ value_json.completionTimestamp }}', + device_class: 'timestamp', + icon: 'mdi:calendar-clock', + }; + + const result = mqtt.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + }); + + describe('createPollingRefreshIntervalSensorConfigPayload', () => { + it('should generate the correct config payload for polling refresh interval sensor', () => { + const refreshIntervalCurrentValTopic = 'homeassistant/XXX/polling_refresh_interval/state'; + const expectedTopic = 'homeassistant/sensor/XXX/polling_refresh_interval/config'; + const expectedPayload = { + device: { + identifiers: ['XXX_Command_Status_Monitor'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar Command Status Monitor Sensors', + suggested_area: '2020 foo bar Command Status Monitor Sensors', + }, + availability: { + topic: 'homeassistant/XXX/available', + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: 'xxx_polling_refresh_interval', + name: 'Polling Refresh Interval', + state_topic: refreshIntervalCurrentValTopic, + value_template: '{{ value | int(0) }}', + icon: 'mdi:timer-check-outline', + unit_of_measurement: 'ms', + state_class: 'measurement', + device_class: 'duration', + }; + + const result = mqtt.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + }); + + describe('createPollingStatusTFSensorConfigPayload', () => { + it('should generate the correct config payload for polling status TF sensor', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_tf/state'; + const expectedTopic = 'homeassistant/binary_sensor/XXX/polling_status_tf/config'; + const expectedPayload = { + "device": { + "identifiers": ['XXX_Command_Status_Monitor'], + "manufacturer": 'foo', + "model": '2020 bar', + "name": '2020 foo bar Command Status Monitor Sensors', + "suggested_area": '2020 foo bar Command Status Monitor Sensors', + }, + "availability": { + "topic": 'homeassistant/XXX/available', + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": 'xxx_onstar_polling_status_successful', + "name": 'Polling Status Successful', + "state_topic": pollingStatusTopicState, + "payload_on": "false", + "payload_off": "true", + "device_class": "problem", + "icon": "mdi:sync-alert", + }; + + const result = mqtt.createPollingStatusTFSensorConfigPayload(pollingStatusTopicState); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + }); + 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); + it('should create button config payload for a given vehicle', () => { + const vehicle = { + make: 'Chevrolet', + model: 'Bolt', + year: 2022, + vin: '1G1FY6S07N4100000', + toString: () => `${vehicle.year} ${vehicle.make} ${vehicle.model}`, + }; const expectedButtonInstances = []; const expectedButtonConfigs = []; @@ -641,11 +873,10 @@ describe('MQTT', () => { const button = { name: buttonName, config: buttonConfig, - vehicle: vehicle + vehicle: vehicle, }; expectedButtonInstances.push(button); - expectedButtonConfigs.push(buttonConfig); let unique_id = `${vehicle.vin}_Command_${button.name}`; unique_id = unique_id.replace(/\s+/g, '-').toLowerCase(); @@ -655,23 +886,27 @@ describe('MQTT', () => { "identifiers": [vehicle.vin], "manufacturer": vehicle.make, "model": vehicle.year + ' ' + vehicle.model, - "name": vehicle.toString() + "name": vehicle.toString(), + "suggested_area": vehicle.toString(), }, "availability": { - "topic": mqtt.getAvailabilityTopic(), + "topic": 'homeassistant/XXX/available', "payload_available": 'true', "payload_not_available": 'false', }, "unique_id": unique_id, "name": `Command ${button.name}`, - "command_topic": mqtt.getCommandTopic(), + "command_topic": 'homeassistant/XXX/command', "payload_press": JSON.stringify({ "command": MQTT.CONSTANTS.BUTTONS[button.name] }), "qos": 2, "enabled_by_default": false, }); + + expectedButtonConfigs.push(buttonConfig); } const result = mqtt.createButtonConfigPayload(vehicle); + assert.deepStrictEqual(result.buttonInstances, expectedButtonInstances); assert.deepStrictEqual(result.buttonConfigs, expectedButtonConfigs); assert.deepStrictEqual(result.configPayloads, expectedConfigPayloads);