diff --git a/src/index.js b/src/index.js index 328a4a62..6126fed0 100644 --- a/src/index.js +++ b/src/index.js @@ -54,6 +54,7 @@ const mqttConfig = { prefix: process.env.MQTT_PREFIX || 'homeassistant', namePrefix: process.env.MQTT_NAME_PREFIX || '', pollingStatusTopic: process.env.MQTT_ONSTAR_POLLING_STATUS_TOPIC, + listAllSensorsTogether: process.env.MQTT_LIST_ALL_SENSORS_TOGETHER === 'true', ca: process.env.MQTT_CA_FILE ? [fs.readFileSync(process.env.MQTT_CA_FILE)] : undefined, cert: process.env.MQTT_CERT_FILE ? fs.readFileSync(process.env.MQTT_CERT_FILE) : undefined, key: process.env.MQTT_KEY_FILE ? fs.readFileSync(process.env.MQTT_KEY_FILE) : undefined, @@ -146,9 +147,9 @@ const configureMQTT = async (commands, client, mqttHA) => { const topicArray = _.concat({ topic }, '/', { command }.command, '/', 'state'); const commandStatusTopic = topicArray.map(item => item.topic || item).join(''); - const commandStatusSensorConfig = mqttHA.createCommandStatusSensorConfigPayload(command); + const commandStatusSensorConfig = mqttHA.createCommandStatusSensorConfigPayload(command, mqttConfig.listAllSensorsTogether); logger.debug("Command Status Sensor Config:", commandStatusSensorConfig); - const commandStatusSensorTimestampConfig = mqttHA.createCommandStatusSensorTimestampConfigPayload(command); + const commandStatusSensorTimestampConfig = mqttHA.createCommandStatusSensorTimestampConfigPayload(command, mqttConfig.listAllSensorsTogether); logger.debug("Command Status Sensor Timestamp Config:", commandStatusSensorTimestampConfig); const commandFn = cmd.bind(commands); @@ -516,11 +517,11 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); const pollingStatusTopicState = topicArray.map(item => item.topic || item).join(''); logger.info(`pollingStatusTopicState: ${pollingStatusTopicState}`); - const pollingStatusMessagePayload = mqttHA.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState); + const pollingStatusMessagePayload = mqttHA.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState, mqttConfig.listAllSensorsTogether); logger.debug("pollingStatusMessagePayload:", pollingStatusMessagePayload); - const pollingStatusCodePayload = mqttHA.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState); + const pollingStatusCodePayload = mqttHA.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState, mqttConfig.listAllSensorsTogether); logger.debug("pollingStatusCodePayload:", pollingStatusCodePayload); - const pollingStatusMessageTimestampPayload = mqttHA.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState); + const pollingStatusMessageTimestampPayload = mqttHA.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState, mqttConfig.listAllSensorsTogether); logger.debug("pollingStatusMessageTimestampPayload:", pollingStatusMessageTimestampPayload); client.publish(pollingStatusTopicState, @@ -552,7 +553,7 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); logger.info(`pollingStatusTopicTF, ${pollingStatusTopicTF}`); if (!buttonConfigsPublished) { - const pollingStatusTFPayload = mqttHA.createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF); + const pollingStatusTFPayload = mqttHA.createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF, mqttConfig.listAllSensorsTogether); logger.debug("pollingStatusTFPayload:", pollingStatusTFPayload); client.publish(pollingStatusTFPayload.topic, JSON.stringify(pollingStatusTFPayload.payload), { retain: true }); logger.info(`Polling Status TF Sensor Published!`); @@ -572,10 +573,17 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); logger.debug(`Supported Commands: ${v.getSupportedCommands()}`); // Get button configs and payloads - const tasks = [ - mqttHA.createButtonConfigPayload(v), - mqttHA.createButtonConfigPayloadCSMG(v) - ]; + let tasks; + if (mqttConfig.listAllSensorsTogether === true) { + tasks = [ + mqttHA.createButtonConfigPayload(v), + ]; + } else { + tasks = [ + mqttHA.createButtonConfigPayload(v), + mqttHA.createButtonConfigPayloadCSMG(v), + ]; + } tasks.forEach(({ buttonConfigs, configPayloads }, taskIndex) => { // Publish button config and payload for each button in first set @@ -590,9 +598,23 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); client.publish(buttonConfig, JSON.stringify(configPayload), { retain: true }); }); }); + logger.info(`Button Configs Published!`); + const sensors = [ + { name: 'oil_life', component: null, icon: 'mdi:oil-level' }, + { name: 'tire_pressure', component: 'tire_pressure_lf_message', icon: 'mdi:car-tire-alert' }, + { name: 'tire_pressure', component: 'tire_pressure_lr_message', icon: 'mdi:car-tire-alert' }, + { name: 'tire_pressure', component: 'tire_pressure_rf_message', icon: 'mdi:car-tire-alert' }, + { name: 'tire_pressure', component: 'tire_pressure_rr_message', icon: 'mdi:car-tire-alert' }, + ]; + + for (let sensor of sensors) { + const sensorMessagePayload = mqttHA.createSensorMessageConfigPayload(sensor.name, sensor.component, sensor.icon); + logger.debug('Sensor Message Payload:', sensorMessagePayload); + client.publish(sensorMessagePayload.topic, JSON.stringify(sensorMessagePayload.payload), { retain: true }); + } + logger.info(`Sensor Message Configs Published!`); buttonConfigsPublished = 'true'; - logger.info(`Button Configs Published!`); } } @@ -732,7 +754,7 @@ logger.info('!-- Starting OnStar2MQTT Polling --!'); logger.info(`refreshIntervalCurrentValTopic: ${refreshIntervalCurrentValTopic}`); if (!buttonConfigsPublished) { - const pollingRefreshIntervalPayload = mqttHA.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic); + const pollingRefreshIntervalPayload = mqttHA.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic, mqttConfig.listAllSensorsTogether); logger.debug("pollingRefreshIntervalSensorConfigPayload:", pollingRefreshIntervalPayload); client.publish(pollingRefreshIntervalPayload.topic, pollingRefreshIntervalPayload.payload, { retain: true }); logger.info(`Polling Refresh Interval Sensor Published!`); diff --git a/src/mqtt.js b/src/mqtt.js index fc1a25a1..2d4d0a6f 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -141,17 +141,31 @@ class MQTT { // return `${this.prefix}/sensor/${this.instance}`; // } - createCommandStatusSensorConfigPayload(command) { + createCommandStatusSensorConfigPayload(command, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/${command}_status_monitor/config`; let commandStatusTopic = `${this.prefix}/${this.instance}/command/${command}/state`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -163,20 +177,35 @@ class MQTT { "value_template": "{{ value_json.command.error.message }}", "icon": "mdi:message-alert", }; + return { topic, payload }; } - createCommandStatusSensorTimestampConfigPayload(command) { + createCommandStatusSensorTimestampConfigPayload(command, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/${command}_status_timestamp/config`; let commandStatusTopic = `${this.prefix}/${this.instance}/command/${command}/state`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -287,16 +316,30 @@ class MQTT { return { buttonInstances, buttonConfigs, configPayloads }; } - createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState) { + createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/polling_status_message/config`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -311,16 +354,30 @@ class MQTT { return { topic, payload }; } - createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState) { + createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/polling_status_code/config`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -335,16 +392,30 @@ class MQTT { return { topic, payload }; } - createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState) { + createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/polling_status_timestamp/config`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -360,16 +431,30 @@ class MQTT { return { topic, payload }; } - createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic) { + createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic, listAllSensorsTogether) { let topic = `${this.prefix}/sensor/${this.instance}/polling_refresh_interval/config`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -387,16 +472,30 @@ class MQTT { return { topic, payload }; } - createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF) { + createPollingStatusTFSensorConfigPayload(pollingStatusTopicTF, listAllSensorsTogether) { let topic = `${this.prefix}/binary_sensor/${this.instance}/polling_status_tf/config`; - let payload = { - "device": { + + let device; + if (listAllSensorsTogether === true) { + device = { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }; + } else { + 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', - }, + }; + } + + let payload = { + "device": device, "availability": { "topic": this.getAvailabilityTopic(), "payload_available": 'true', @@ -413,6 +512,51 @@ class MQTT { return { topic, payload }; } + createSensorMessageConfigPayload(sensor, component, icon) { + //let topic = `${this.prefix}/sensor/${this.instance}/${sensor}_message/config`; + + let topic, unique_id, sensor_name, value_template; + if (!component) { + topic = `${this.prefix}/sensor/${this.instance}/${sensor}_message/config`; + unique_id = MQTT.convertName(this.vehicle.vin) + '_' + sensor; + sensor_name = `${sensor.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')} Message`; + value_template = `{{ value_json.${sensor}_message }}`; + } else { + topic = `${this.prefix}/sensor/${this.instance}/${sensor}_${component}_message/config`; + unique_id = MQTT.convertName(this.vehicle.vin) + '_' + sensor + '_' + component; + let component_words = component.split('_'); + component_words = component_words.map(component_word => { + if (component_word === 'lf' || component_word === 'rf' || component_word === 'lr' || component_word === 'rr') { + return component_word.toUpperCase(); + } else { + return component_word.charAt(0).toUpperCase() + component_word.slice(1); + } + }); + sensor_name = component_words.join(' '); + value_template = `{{ value_json.${component} }}`; + } + + let payload = { + "device": { + "identifiers": [this.vehicle.vin], + "manufacturer": this.vehicle.make, + "model": this.vehicle.year + ' ' + this.vehicle.model, + "name": this.vehicle.toString(), + "suggested_area": this.vehicle.toString(), + }, + "availability": { + "topic": this.getAvailabilityTopic(), + "payload_available": 'true', + "payload_not_available": 'false', + }, + "unique_id": unique_id, + "name": sensor_name, + "state_topic": `${this.prefix}/sensor/${this.instance}/${sensor}/state`, + "value_template": value_template, + "icon": icon, + }; + return { topic, payload }; + } /** * diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index 2c41b58c..522ed1be 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -627,9 +627,196 @@ describe('MQTT', () => { }); }); + describe('attributes', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[1]'))); + it('should generate payloads with an attribute', () => { + 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', + suggested_area: "2020 foo bar Sensors", + }, + state_class: undefined, + device_class: undefined, + json_attributes_template: undefined, + name: 'Charger Power Level', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/charger_power_level/state', + unique_id: 'xxx-charger-power-level', + unit_of_measurement: undefined, + json_attributes_topic: undefined, + value_template: '{{ value_json.charger_power_level }}' + }); + }); + it('should generate state payloads', () => { + assert.deepStrictEqual(mqtt.getStatePayload(d), { + charger_power_level: 'NO_REDUCTION', + charger_power_level_message: 'na' + }); + }); + }); + + describe('attributes', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[2]'))); + it('should generate payloads with an attribute', () => { + 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', + suggested_area: "2020 foo bar Sensors", + }, + state_class: 'measurement', + device_class: undefined, + json_attributes_template: undefined, + name: 'Electric Economy', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/energy_efficiency/state', + unique_id: 'xxx-electric-economy', + json_attributes_topic: undefined, + unit_of_measurement: 'kWh', + value_template: '{{ value_json.electric_economy }}' + }); + }); + it('should generate state payloads', () => { + assert.deepStrictEqual(mqtt.getStatePayload(d), { + electric_economy: 21.85, + electric_economy_message: "na", + lifetime_efficiency: 21.85, + lifetime_efficiency_message: "na", + lifetime_mpge: 40.73, + lifetime_mpge_message: "na", + lifetime_mpge_mpge: 95.8, + lifetime_mpge_mpge_message: "na", + odometer: 6013.8, + odometer_message: "na", + odometer_mi: 3736.8, + odometer_mi_message: "na", + }); + }); + }); + + describe('attributes', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[9]'))); + it('should generate payloads with an attribute', () => { + 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', + suggested_area: "2020 foo bar Sensors", + }, + state_class: 'measurement', + device_class: 'distance', + json_attributes_template: undefined, + name: 'Ev Range', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/vehicle_range/state', + unique_id: 'xxx-ev-range', + unit_of_measurement: 'km', + json_attributes_topic: undefined, + value_template: '{{ value_json.ev_range }}' + }); + }); + it('should generate state payloads', () => { + assert.deepStrictEqual(mqtt.getStatePayload(d), { + ev_range: 341, + ev_range_message: 'na', + ev_range_mi: 211.9, + ev_range_mi_message: 'na', + }); + }); + }); + + describe('attributes', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[9]'))); + it('should generate payloads with an attribute', () => { + 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', + suggested_area: "2020 foo bar Sensors", + }, + state_class: 'measurement', + device_class: 'distance', + json_attributes_template: undefined, + name: 'Ev Range', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/sensor/XXX/vehicle_range/state', + unique_id: 'xxx-ev-range', + unit_of_measurement: 'km', + json_attributes_topic: undefined, + value_template: '{{ value_json.ev_range }}' + }); + }); + it('should generate state payloads', () => { + assert.deepStrictEqual(mqtt.getStatePayload(d), { + ev_range: 341, + ev_range_message: 'na', + ev_range_mi: 211.9, + ev_range_mi_message: 'na', + }); + }); + }); + + describe('createCommandStatusSensorConfigPayload', () => { - it('should generate command status sensor config payload', () => { + it('should generate command status sensor config payload when listAllSensorsTogether is true', () => { const command = 'lock'; + const listAllSensorsTogether = true; + 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' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: "2020 foo bar", + }, + icon: 'mdi:message-alert', + name: 'Command lock Status Monitor', + state_topic: 'homeassistant/XXX/command/lock/state', + unique_id: 'xxx_lock_command_status_monitor', + value_template: '{{ value_json.command.error.message }}', + } + }; + const result = mqtt.createCommandStatusSensorConfigPayload(command, listAllSensorsTogether); + assert.deepStrictEqual(result, expectedConfigPayload); + }); + + it('should generate command status sensor config payload when listAllSensorsTogether is false', () => { + const command = 'lock'; + const listAllSensorsTogether = false; const expectedConfigPayload = { topic: "homeassistant/sensor/XXX/lock_status_monitor/config", payload: { @@ -654,15 +841,48 @@ describe('MQTT', () => { value_template: '{{ value_json.command.error.message }}', } }; - const result = mqtt.createCommandStatusSensorConfigPayload(command); + const result = mqtt.createCommandStatusSensorConfigPayload(command, listAllSensorsTogether); assert.deepStrictEqual(result, expectedConfigPayload); }); }); describe('createCommandStatusSensorTimestampConfigPayload', () => { - it('should generate command status sensor timestamp config payload', () => { + it('should generate command status sensor timestamp config payload when listAllSensorsTogether is true', () => { + const command = 'lock'; + const listAllSensorsTogether = true; + 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' + ], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: "2020 foo bar", + }, + device_class: "timestamp", + icon: "mdi:calendar-clock", + name: 'Command lock Status Monitor Timestamp', + state_topic: 'homeassistant/XXX/command/lock/state', + unique_id: 'xxx_lock_command_status_timestamp_monitor', + value_template: '{{ value_json.completionTimestamp }}', + } + }; + const result = mqtt.createCommandStatusSensorTimestampConfigPayload(command, listAllSensorsTogether); + assert.deepStrictEqual(result, expectedConfigPayload); + }); + + it('should generate command status sensor timestamp config payload when listAllSensorsTogether is false', () => { const command = 'lock'; + const listAllSensorsTogether = false; const expectedConfigPayload = { topic: "homeassistant/sensor/XXX/lock_status_timestamp/config", payload: { @@ -688,14 +908,45 @@ describe('MQTT', () => { value_template: '{{ value_json.completionTimestamp }}', } }; - const result = mqtt.createCommandStatusSensorTimestampConfigPayload(command); + const result = mqtt.createCommandStatusSensorTimestampConfigPayload(command, listAllSensorsTogether); assert.deepStrictEqual(result, expectedConfigPayload); }); }); describe('createPollingStatusMessageSensorConfigPayload', () => { - it('should generate the correct sensor config payload for polling status message', () => { + it('should generate the correct sensor config payload for polling status message when listAllSensorsTogether is true', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status/state'; + const listAllSensorsTogether = true; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_message/config'; + const expectedPayload = { + "device": { + "identifiers": ['XXX'], + "manufacturer": 'foo', + "model": '2020 bar', + "name": '2020 foo bar', + "suggested_area": '2020 foo bar', + }, + "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, listAllSensorsTogether); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + + it('should generate the correct sensor config payload for polling status message when listAllSensorsTogether is false', () => { const pollingStatusTopicState = 'homeassistant/XXX/polling_status/state'; + const listAllSensorsTogether = false; const expectedTopic = 'homeassistant/sensor/XXX/polling_status_message/config'; const expectedPayload = { "device": { @@ -717,7 +968,7 @@ describe('MQTT', () => { "icon": "mdi:message-alert", }; - const result = mqtt.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState); + const result = mqtt.createPollingStatusMessageSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether); assert.deepStrictEqual(result.topic, expectedTopic); assert.deepStrictEqual(result.payload, expectedPayload); @@ -725,8 +976,39 @@ describe('MQTT', () => { }); describe('createPollingStatusCodeSensorConfigPayload', () => { - it('should generate the correct config payload for polling status code sensor', () => { + it('should generate the correct config payload for polling status code sensor when listAllSensorsTogether is true', () => { const pollingStatusTopicState = 'homeassistant/XXX/polling_status_code/state'; + const listAllSensorsTogether = true; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_code/config'; + const expectedPayload = { + device: { + identifiers: ['XXX'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: '2020 foo bar', + }, + 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, listAllSensorsTogether); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + + it('should generate the correct config payload for polling status code sensor when listAllSensorsTogether is false', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_code/state'; + const listAllSensorsTogether = false; const expectedTopic = 'homeassistant/sensor/XXX/polling_status_code/config'; const expectedPayload = { device: { @@ -748,7 +1030,7 @@ describe('MQTT', () => { icon: 'mdi:sync-alert', }; - const result = mqtt.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState); + const result = mqtt.createPollingStatusCodeSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether); assert.deepStrictEqual(result.topic, expectedTopic); assert.deepStrictEqual(result.payload, expectedPayload); @@ -756,8 +1038,40 @@ describe('MQTT', () => { }); describe('createPollingStatusTimestampSensorConfigPayload', () => { - it('should generate the correct config payload', () => { + it('should generate the correct config payload when listAllSensorsTogether is true', () => { const pollingStatusTopicState = 'homeassistant/XXX/polling_status_timestamp/state'; + const listAllSensorsTogether = true; + const expectedTopic = 'homeassistant/sensor/XXX/polling_status_timestamp/config'; + const expectedPayload = { + device: { + identifiers: ['XXX'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: '2020 foo bar', + }, + 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, listAllSensorsTogether); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + + it('should generate the correct config payload when listAllSensorsTogether is false', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_timestamp/state'; + const listAllSensorsTogether = false; const expectedTopic = 'homeassistant/sensor/XXX/polling_status_timestamp/config'; const expectedPayload = { device: { @@ -780,7 +1094,7 @@ describe('MQTT', () => { icon: 'mdi:calendar-clock', }; - const result = mqtt.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState); + const result = mqtt.createPollingStatusTimestampSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether); assert.deepStrictEqual(result.topic, expectedTopic); assert.deepStrictEqual(result.payload, expectedPayload); @@ -788,8 +1102,42 @@ describe('MQTT', () => { }); describe('createPollingRefreshIntervalSensorConfigPayload', () => { - it('should generate the correct config payload for polling refresh interval sensor', () => { + it('should generate the correct config payload for polling refresh interval sensor when listAllSensorsTogether is true', () => { + const refreshIntervalCurrentValTopic = 'homeassistant/XXX/polling_refresh_interval/state'; + const listAllSensorsTogether = true; + const expectedTopic = 'homeassistant/sensor/XXX/polling_refresh_interval/config'; + const expectedPayload = { + device: { + identifiers: ['XXX'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: '2020 foo bar', + }, + 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, listAllSensorsTogether); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + + it('should generate the correct config payload for polling refresh interval sensor when listAllSensorsTogether is false', () => { const refreshIntervalCurrentValTopic = 'homeassistant/XXX/polling_refresh_interval/state'; + const listAllSensorsTogether = false; const expectedTopic = 'homeassistant/sensor/XXX/polling_refresh_interval/config'; const expectedPayload = { device: { @@ -814,7 +1162,7 @@ describe('MQTT', () => { device_class: 'duration', }; - const result = mqtt.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic); + const result = mqtt.createPollingRefreshIntervalSensorConfigPayload(refreshIntervalCurrentValTopic, listAllSensorsTogether); assert.deepStrictEqual(result.topic, expectedTopic); assert.deepStrictEqual(result.payload, expectedPayload); @@ -822,32 +1170,65 @@ describe('MQTT', () => { }); describe('createPollingStatusTFSensorConfigPayload', () => { - it('should generate the correct config payload for polling status TF sensor', () => { + it('should generate the correct config payload for polling status TF sensor when listAllSensorsTogether is true', () => { const pollingStatusTopicState = 'homeassistant/XXX/polling_status_tf/state'; + const listAllSensorsTogether = true; 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', + device: { + identifiers: ['XXX'], + manufacturer: 'foo', + model: '2020 bar', + name: '2020 foo bar', + suggested_area: '2020 foo bar', }, - "availability": { - "topic": 'homeassistant/XXX/available', - "payload_available": 'true', - "payload_not_available": 'false', + 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", + 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); + const result = mqtt.createPollingStatusTFSensorConfigPayload(pollingStatusTopicState, listAllSensorsTogether); + + assert.deepStrictEqual(result.topic, expectedTopic); + assert.deepStrictEqual(result.payload, expectedPayload); + }); + + it('should generate the correct config payload for polling status TF sensor when listAllSensorsTogether is false', () => { + const pollingStatusTopicState = 'homeassistant/XXX/polling_status_tf/state'; + const listAllSensorsTogether = false; + 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, listAllSensorsTogether); assert.deepStrictEqual(result.topic, expectedTopic); assert.deepStrictEqual(result.payload, expectedPayload); @@ -913,5 +1294,148 @@ describe('MQTT', () => { }); }); + describe('createButtonConfigPayloadCSMG', () => { + it('should generate the correct button config payload for a given vehicle', () => { + const vehicle = { + make: 'foo', + model: 'bar', + year: 2020, + vin: '1G1FY6S07N4100000', + toString: function () { return `${this.year} ${this.make} ${this.model}`; } + }; + + const result = mqtt.createButtonConfigPayloadCSMG(vehicle); + + // Check the length of the arrays + assert.strictEqual(result.buttonInstances.length, Object.keys(MQTT.CONSTANTS.BUTTONS).length); + assert.strictEqual(result.buttonConfigs.length, Object.keys(MQTT.CONSTANTS.BUTTONS).length); + assert.strictEqual(result.configPayloads.length, Object.keys(MQTT.CONSTANTS.BUTTONS).length); + + // Check the first button instance + const firstButton = result.buttonInstances[0]; + assert.strictEqual(firstButton.name, Object.keys(MQTT.CONSTANTS.BUTTONS)[0]); + assert.strictEqual(firstButton.config, `homeassistant/button/XXX/${MQTT.convertName(firstButton.name)}_monitor/config`); + assert.strictEqual(firstButton.vehicle, vehicle); + + // Check the first config payload + const firstPayload = result.configPayloads[0]; + assert.strictEqual(firstPayload.device.identifiers[0], `${vehicle.vin}_Command_Status_Monitor`); + assert.strictEqual(firstPayload.device.manufacturer, vehicle.make); + assert.strictEqual(firstPayload.device.model, `${vehicle.year} ${vehicle.model}`); + assert.strictEqual(firstPayload.device.name, `${vehicle.toString()} Command Status Monitor Sensors`); + assert.strictEqual(firstPayload.device.suggested_area, `${vehicle.toString()} Command Status Monitor Sensors`); + assert.strictEqual(firstPayload.unique_id, `${vehicle.vin}_Command_${firstButton.name}_Monitor`.replace(/\s+/g, '-').toLowerCase()); + assert.strictEqual(firstPayload.name, `Command ${firstButton.name}`); + assert.strictEqual(firstPayload.command_topic, mqtt.getCommandTopic()); + assert.strictEqual(firstPayload.payload_press, JSON.stringify({ "command": MQTT.CONSTANTS.BUTTONS[firstButton.name] })); + assert.strictEqual(firstPayload.qos, 2); + assert.strictEqual(firstPayload.enabled_by_default, false); + }); + }); + + describe('createSensorMessageConfigPayload', () => { + beforeEach(() => { + // Set up the MQTT instance and vehicle details + mqtt.vehicle = { + vin: '1234', + make: 'TestMake', + model: 'TestModel', + year: '2022', + toString: () => 'TestVehicle' + }; + mqtt.prefix = 'testPrefix'; + mqtt.instance = 'testInstance'; + }); + + it('should create sensor message config payload when component is not provided', () => { + const sensor = 'testSensor'; + const icon = 'testIcon'; + const expected = { + topic: 'testPrefix/sensor/testInstance/testSensor_message/config', + payload: { + device: { + identifiers: ['1234'], + manufacturer: 'TestMake', + model: '2022 TestModel', + name: 'TestVehicle', + suggested_area: 'TestVehicle', + }, + availability: { + topic: mqtt.getAvailabilityTopic(), + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: '1234_testSensor', + name: 'TestSensor Message', + state_topic: 'testPrefix/sensor/testInstance/testSensor/state', + value_template: '{{ value_json.testSensor_message }}', + icon: 'testIcon', + } + }; + const result = mqtt.createSensorMessageConfigPayload(sensor, undefined, icon); + assert.deepStrictEqual(result, expected); + }); + + it('should create sensor message config payload when component is provided', () => { + const sensor = 'testSensor'; + const component = 'testComponent'; + const icon = 'testIcon'; + const expected = { + topic: 'testPrefix/sensor/testInstance/testSensor_testComponent_message/config', + payload: { + device: { + identifiers: ['1234'], + manufacturer: 'TestMake', + model: '2022 TestModel', + name: 'TestVehicle', + suggested_area: 'TestVehicle', + }, + availability: { + topic: mqtt.getAvailabilityTopic(), + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: '1234_testSensor_testComponent', + name: 'TestComponent', + state_topic: 'testPrefix/sensor/testInstance/testSensor/state', + value_template: '{{ value_json.testComponent }}', + icon: 'testIcon', + } + }; + const result = mqtt.createSensorMessageConfigPayload(sensor, component, icon); + assert.deepStrictEqual(result, expected); + }); + + it('should create sensor message config payload when component is provided', () => { + const sensor = 'oil_life'; + const component = undefined; + const icon = 'testIcon'; + const expected = { + topic: 'testPrefix/sensor/testInstance/oil_life_message/config', + payload: { + device: { + identifiers: ['1234'], + manufacturer: 'TestMake', + model: '2022 TestModel', + name: 'TestVehicle', + suggested_area: 'TestVehicle', + }, + availability: { + topic: mqtt.getAvailabilityTopic(), + payload_available: 'true', + payload_not_available: 'false', + }, + unique_id: '1234_oil_life', + name: 'Oil Life Message', + state_topic: 'testPrefix/sensor/testInstance/oil_life/state', + value_template: '{{ value_json.oil_life_message }}', + icon: 'testIcon', + } + }; + const result = mqtt.createSensorMessageConfigPayload(sensor, component, icon); + assert.deepStrictEqual(result, expected); + }); + }); + }); });