2020-12-01 03:52:38 +00:00
|
|
|
const _ = require('lodash');
|
2020-11-30 21:26:14 +00:00
|
|
|
|
2020-12-01 03:52:38 +00:00
|
|
|
/**
|
|
|
|
* Supports Home Assistant MQTT Discovery (https://www.home-assistant.io/docs/mqtt/discovery/)
|
|
|
|
*
|
|
|
|
* Supplies sensor configuration data and initialize sensors in HA.
|
|
|
|
*
|
|
|
|
* Topic format: prefix/type/instance/name
|
|
|
|
* Examples:
|
|
|
|
* - homeassistant/sensor/VIN/TIRE_PRESSURE/state -- Diagnostic
|
|
|
|
* - payload: {
|
|
|
|
* TIRE_PRESSURE_LF: 244.0,
|
|
|
|
* TIRE_PRESSURE_LR: 240.0,
|
|
|
|
* TIRE_PRESSURE_PLACARD_FRONT: 262.0,
|
|
|
|
* TIRE_PRESSURE_PLACARD_REAR: 262.0,
|
|
|
|
* TIRE_PRESSURE_RF: 240.0,
|
|
|
|
* TIRE_PRESSURE_RR: 236.0,
|
|
|
|
* }
|
|
|
|
* - homeassistant/sensor/VIN/TIRE_PRESSURE_LF/config -- Diagnostic Element
|
|
|
|
* - payload: {
|
|
|
|
* device_class: "pressure",
|
|
|
|
* name: "Tire Pressure: Left Front",
|
|
|
|
* state_topic: "homeassistant/sensor/VIN/TIRE_PRESSURE/state",
|
|
|
|
* unit_of_measurement: "kPa",
|
|
|
|
* value_template: "{{ value_json.TIRE_PRESSURE_LF }}",
|
|
|
|
* json_attributes_template: "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}"
|
|
|
|
* }
|
|
|
|
* - homeassistant/sensor/VIN/TIRE_PRESSURE_RR/config -- Diagnostic Element
|
|
|
|
* - payload: {
|
|
|
|
* device_class: "pressure",
|
|
|
|
* name: "Tire Pressure: Right Rear",
|
|
|
|
* state_topic: "homeassistant/sensor/VIN/TIRE_PRESSURE/state",
|
|
|
|
* unit_of_measurement: "kPa",
|
|
|
|
* value_template: "{{ value_json.TIRE_PRESSURE_RR }}",
|
|
|
|
* json_attributes_template: "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}"
|
|
|
|
* }
|
|
|
|
*/
|
2020-11-30 21:26:14 +00:00
|
|
|
class MQTT {
|
2020-12-28 02:55:49 +00:00
|
|
|
constructor(vehicle, prefix = 'homeassistant') {
|
2020-12-01 03:52:38 +00:00
|
|
|
this.prefix = prefix;
|
2020-12-28 02:55:49 +00:00
|
|
|
this.vehicle = vehicle;
|
|
|
|
this.instance = vehicle.vin;
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static convertName(name) {
|
|
|
|
return _.toLower(_.replace(name, / /g, '_'));
|
|
|
|
}
|
|
|
|
|
|
|
|
static convertFriendlyName(name) {
|
|
|
|
return _.startCase(_.lowerCase(name));
|
|
|
|
}
|
|
|
|
|
2020-12-02 19:11:50 +00:00
|
|
|
static determineSensorType(name) {
|
|
|
|
switch (name) {
|
|
|
|
case 'EV CHARGE STATE':
|
|
|
|
case 'EV PLUG STATE':
|
|
|
|
case 'PRIORITY CHARGE INDICATOR':
|
|
|
|
case 'PRIORITY CHARGE STATUS':
|
|
|
|
return 'binary_sensor';
|
|
|
|
default:
|
|
|
|
return 'sensor';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-01 03:52:38 +00:00
|
|
|
/**
|
|
|
|
* @param {'sensor'|'binary_sensor'} type
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
getBaseTopic(type = 'sensor') {
|
|
|
|
return `${this.prefix}/${type}/${this.instance}`;
|
|
|
|
}
|
|
|
|
|
2020-12-28 02:21:13 +00:00
|
|
|
getAvailabilityTopic() {
|
|
|
|
return `${this.prefix}/${this.instance}/available`;
|
|
|
|
}
|
|
|
|
|
2021-06-10 17:29:46 +00:00
|
|
|
getCommandTopic() {
|
|
|
|
return `${this.prefix}/${this.instance}/command`;
|
|
|
|
}
|
|
|
|
|
2020-12-01 03:52:38 +00:00
|
|
|
/**
|
|
|
|
*
|
2020-12-02 19:11:50 +00:00
|
|
|
* @param {DiagnosticElement} diag
|
2020-12-01 03:52:38 +00:00
|
|
|
*/
|
2020-12-02 19:11:50 +00:00
|
|
|
getConfigTopic(diag) {
|
|
|
|
let sensorType = MQTT.determineSensorType(diag.name);
|
|
|
|
return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diag.name)}/config`;
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
2020-12-02 19:11:50 +00:00
|
|
|
* @param {Diagnostic} diag
|
2020-12-01 03:52:38 +00:00
|
|
|
*/
|
2020-12-02 19:11:50 +00:00
|
|
|
getStateTopic(diag) {
|
|
|
|
let sensorType = MQTT.determineSensorType(diag.name);
|
|
|
|
return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diag.name)}/state`;
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
2020-12-02 19:11:50 +00:00
|
|
|
* @param {Diagnostic} diag
|
|
|
|
* @param {DiagnosticElement} diagEl
|
2020-12-01 03:52:38 +00:00
|
|
|
*/
|
2020-12-02 19:11:50 +00:00
|
|
|
getConfigPayload(diag, diagEl) {
|
|
|
|
return this.getConfigMapping(diag, diagEl);
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the state payload for this diagnostic
|
2020-12-02 19:11:50 +00:00
|
|
|
* @param {Diagnostic} diag
|
2020-12-01 03:52:38 +00:00
|
|
|
*/
|
2020-12-02 19:11:50 +00:00
|
|
|
getStatePayload(diag) {
|
2020-12-01 03:52:38 +00:00
|
|
|
const state = {};
|
2020-12-02 19:11:50 +00:00
|
|
|
_.forEach(diag.diagnosticElements, e => {
|
2020-12-01 03:52:38 +00:00
|
|
|
// massage the binary_sensor values
|
|
|
|
let value;
|
2020-12-02 19:11:50 +00:00
|
|
|
switch (e.name) {
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'EV PLUG STATE': // unplugged/plugged
|
|
|
|
value = e.value === 'plugged';
|
|
|
|
break;
|
|
|
|
case 'EV CHARGE STATE': // not_charging/charging
|
|
|
|
value = e.value === 'charging';
|
|
|
|
break;
|
|
|
|
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
|
|
|
|
value = e.value === 'TRUE';
|
|
|
|
break;
|
|
|
|
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
|
|
|
|
value = e.value === 'ACTIVE';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// coerce to number if possible, API uses strings :eyeroll:
|
2021-06-11 03:44:31 +00:00
|
|
|
// eslint-disable-next-line no-case-declarations
|
2020-12-01 03:52:38 +00:00
|
|
|
const num = _.toNumber(e.value);
|
|
|
|
value = _.isNaN(num) ? e.value : num;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
state[MQTT.convertName(e.name)] = value;
|
|
|
|
});
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2020-12-28 19:35:38 +00:00
|
|
|
mapBaseConfigPayload(diag, diagEl, device_class, name, attr) {
|
2020-12-01 03:52:38 +00:00
|
|
|
name = name || MQTT.convertFriendlyName(diagEl.name);
|
|
|
|
return {
|
|
|
|
device_class,
|
|
|
|
name,
|
2020-12-28 02:55:49 +00:00
|
|
|
device: {
|
|
|
|
identifiers: [this.vehicle.vin],
|
|
|
|
manufacturer: this.vehicle.make,
|
|
|
|
model: this.vehicle.year,
|
|
|
|
name: this.vehicle.toString()
|
|
|
|
},
|
2020-12-28 02:21:13 +00:00
|
|
|
availability_topic: this.getAvailabilityTopic(),
|
|
|
|
payload_available: 'true',
|
|
|
|
payload_not_available: 'false',
|
2020-12-28 19:35:38 +00:00
|
|
|
state_topic: this.getStateTopic(diag),
|
|
|
|
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
|
2020-12-28 19:51:34 +00:00
|
|
|
json_attributes_topic: _.isUndefined(attr) ? undefined : this.getStateTopic(diag),
|
2020-12-28 19:35:38 +00:00
|
|
|
json_attributes_template: attr
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
mapSensorConfigPayload(diag, diagEl, device_class, name, attr) {
|
|
|
|
name = name || MQTT.convertFriendlyName(diagEl.name);
|
|
|
|
return _.extend(
|
|
|
|
this.mapBaseConfigPayload(diag, diagEl, device_class, name, attr),
|
|
|
|
{unit_of_measurement: diagEl.unit});
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
|
2020-12-28 19:35:38 +00:00
|
|
|
mapBinarySensorConfigPayload(diag, diagEl, device_class, name, attr) {
|
|
|
|
name = name || MQTT.convertFriendlyName(diagEl.name);
|
|
|
|
return _.extend(
|
|
|
|
this.mapBaseConfigPayload(diag, diagEl, device_class, name, attr),
|
|
|
|
{payload_on: true, payload_off: false});
|
|
|
|
}
|
|
|
|
|
2020-12-01 03:52:38 +00:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {Diagnostic} diag
|
|
|
|
* @param {DiagnosticElement} diagEl
|
|
|
|
*/
|
|
|
|
getConfigMapping(diag, diagEl) {
|
|
|
|
// TODO: this sucks, find a better way to map these diagnostics and their elements for discovery.
|
|
|
|
switch (diagEl.name) {
|
|
|
|
case 'LIFETIME ENERGY USED':
|
|
|
|
case 'LIFETIME EFFICIENCY':
|
|
|
|
case 'ELECTRIC ECONOMY':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'energy');
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'INTERM VOLT BATT VOLT':
|
|
|
|
case 'EV PLUG VOLTAGE':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'voltage');
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
|
|
|
|
case 'AMBIENT AIR TEMPERATURE':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'temperature');
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'EV BATTERY LEVEL':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'battery');
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'TIRE PRESSURE LF':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'TIRE PRESSURE LR':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'TIRE PRESSURE RF':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'TIRE PRESSURE RR':
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
|
2020-12-01 03:52:38 +00:00
|
|
|
// binary sensor
|
|
|
|
case 'EV PLUG STATE': // unplugged/plugged
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapBinarySensorConfigPayload(diag, diagEl, 'plug');
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'EV CHARGE STATE': // not_charging/charging
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapBinarySensorConfigPayload(diag, diagEl, 'battery_charging');
|
2020-12-02 19:11:50 +00:00
|
|
|
// binary_sensor, but no applicable device_class
|
2020-12-01 03:52:38 +00:00
|
|
|
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
|
|
|
|
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapBinarySensorConfigPayload(diag, diagEl);
|
2020-12-01 03:52:38 +00:00
|
|
|
// no device class, camel case name
|
|
|
|
case 'EV RANGE':
|
|
|
|
case 'ODOMETER':
|
|
|
|
case 'LAST TRIP TOTAL DISTANCE':
|
|
|
|
case 'LAST TRIP ELECTRIC ECON':
|
|
|
|
case 'LIFETIME MPGE':
|
|
|
|
case 'CHARGER POWER LEVEL':
|
|
|
|
default:
|
2020-12-28 19:35:38 +00:00
|
|
|
return this.mapSensorConfigPayload(diag, diagEl);
|
2020-12-01 03:52:38 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-30 21:26:14 +00:00
|
|
|
}
|
2020-12-01 03:52:38 +00:00
|
|
|
|
|
|
|
module.exports = MQTT;
|