onstar2mqtt/src/mqtt.js

255 lines
10 KiB
JavaScript
Raw Normal View History

const _ = require('lodash');
/**
* 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 }}"
* }
*/
class MQTT {
constructor(vehicle, prefix = 'homeassistant', namePrefix) {
this.prefix = prefix;
2020-12-28 02:55:49 +00:00
this.vehicle = vehicle;
this.instance = vehicle.vin;
this.namePrefix = namePrefix
}
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';
2021-11-16 17:01:29 +00:00
case 'getLocation':
return 'device_tracker';
2020-12-02 19:11:50 +00:00
default:
return 'sensor';
}
}
/**
* @param {string} name
* @returns {string}
*/
addNamePrefix(name) {
if (!this.namePrefix) return name
return `${this.namePrefix} ${name}`
}
/**
2021-11-16 17:01:29 +00:00
* @param {'sensor'|'binary_sensor'|'device_tracker'} type
* @returns {string}
*/
getBaseTopic(type = 'sensor') {
return `${this.prefix}/${type}/${this.instance}`;
}
getAvailabilityTopic() {
return `${this.prefix}/${this.instance}/available`;
}
getCommandTopic() {
return `${this.prefix}/${this.instance}/command`;
}
/**
*
2020-12-02 19:11:50 +00:00
* @param {DiagnosticElement} diag
*/
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-02 19:11:50 +00:00
* @param {Diagnostic} diag
*/
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-02 19:11:50 +00:00
* @param {Diagnostic} diag
* @param {DiagnosticElement} diagEl
*/
2020-12-02 19:11:50 +00:00
getConfigPayload(diag, diagEl) {
return this.getConfigMapping(diag, diagEl);
}
/**
* Return the state payload for this diagnostic
2020-12-02 19:11:50 +00:00
* @param {Diagnostic} diag
*/
2020-12-02 19:11:50 +00:00
getStatePayload(diag) {
const state = {};
2020-12-02 19:11:50 +00:00
_.forEach(diag.diagnosticElements, e => {
// massage the binary_sensor values
let value;
2020-12-02 19:11:50 +00:00
switch (e.name) {
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
const num = _.toNumber(e.value);
value = _.isNaN(num) ? e.value : num;
break;
}
state[MQTT.convertName(e.name)] = value;
});
return state;
}
mapBaseConfigPayload(diag, diagEl, device_class, name, attr) {
name = name || MQTT.convertFriendlyName(diagEl.name);
name = this.addNamePrefix(name);
2022-05-20 12:51:51 +00:00
// Generate the unique id from the vin and name
let unique_id = `${this.vehicle.vin}-${diagEl.name}`
unique_id = unique_id.replace(/\s+/g, '-').toLowerCase();
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()
},
availability_topic: this.getAvailabilityTopic(),
payload_available: 'true',
payload_not_available: 'false',
state_topic: this.getStateTopic(diag),
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
json_attributes_topic: _.isUndefined(attr) ? undefined : this.getStateTopic(diag),
2022-05-20 10:10:18 +00:00
json_attributes_template: attr,
2022-05-20 12:51:51 +00:00
unique_id: unique_id
};
}
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});
}
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});
}
/**
*
* @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':
return this.mapSensorConfigPayload(diag, diagEl, 'energy');
case 'INTERM VOLT BATT VOLT':
case 'EV PLUG VOLTAGE':
return this.mapSensorConfigPayload(diag, diagEl, 'voltage');
case 'HYBRID BATTERY MINIMUM TEMPERATURE':
case 'AMBIENT AIR TEMPERATURE':
2021-06-11 14:15:14 +00:00
case 'AMBIENT AIR TEMPERATURE F':
case 'ENGINE COOLANT TEMP':
case 'ENGINE COOLANT TEMP F':
return this.mapSensorConfigPayload(diag, diagEl, 'temperature');
case 'EV BATTERY LEVEL':
return this.mapSensorConfigPayload(diag, diagEl, 'battery');
case 'TIRE PRESSURE LF':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
2021-06-11 14:15:14 +00:00
case 'TIRE PRESSURE LF PSI':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT_PSI')}} | tojson }}`);
case 'TIRE PRESSURE LR':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
2021-06-11 14:15:14 +00:00
case 'TIRE PRESSURE LR PSI':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR_PSI')}} | tojson }}`);
case 'TIRE PRESSURE RF':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`);
2021-06-11 14:15:14 +00:00
case 'TIRE PRESSURE RF PSI':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT_PSI')}} | tojson }}`);
case 'TIRE PRESSURE RR':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`);
2021-06-11 14:15:14 +00:00
case 'TIRE PRESSURE RR PSI':
return this.mapSensorConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear PSI', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR_PSI')}} | tojson }}`);
// binary sensor
case 'EV PLUG STATE': // unplugged/plugged
return this.mapBinarySensorConfigPayload(diag, diagEl, 'plug');
case 'EV CHARGE STATE': // not_charging/charging
return this.mapBinarySensorConfigPayload(diag, diagEl, 'battery_charging');
2020-12-02 19:11:50 +00:00
// binary_sensor, but no applicable device_class
case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
return this.mapBinarySensorConfigPayload(diag, diagEl);
// 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:
return this.mapSensorConfigPayload(diag, diagEl);
}
}
}
module.exports = MQTT;