onstar2mqtt/src/index.js
2022-08-16 18:48:31 -05:00

188 lines
7.6 KiB
JavaScript

const OnStar = require('onstarjs');
const mqtt = require('async-mqtt');
const uuidv4 = require('uuid').v4;
const _ = require('lodash');
const Vehicle = require('./vehicle');
const {Diagnostic} = require('./diagnostic');
const MQTT = require('./mqtt');
const Commands = require('./commands');
const logger = require('./logger');
const uuid = process.env.npm_config_uuid;
const vin = process.env.npm_config_vin;
const osuser = process.env.npm_config_osuser;
const ospass = process.env.npm_config_ospass;
const ospin = process.env.npm_config_ospin;
const haip = process.env.npm_config_haip;
const mquser = process.env.npm_config_mquser;
const mqpass = process.env.npm_config_mqpass;
const onstarConfig = {
deviceId: process.env.ONSTAR_DEVICEID || uuid,
vin: process.env.ONSTAR_VIN || vin,
username: process.env.ONSTAR_USERNAME || osuser,
password: process.env.ONSTAR_PASSWORD || ospass,
onStarPin: process.env.ONSTAR_PIN || ospin,
checkRequestStatus: process.env.ONSTAR_SYNC === "true" || true,
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000), // 30 min
allowCommands: _.toLower(_.get(process, 'env.ONSTAR_ALLOW_COMMANDS', 'true')) === 'true'
};
logger.info('OnStar Config', {onstarConfig});
const mqttConfig = {
host: process.env.MQTT_HOST || haip,
username: process.env.MQTT_USERNAME || mquser,
password: process.env.MQTT_PASSWORD || mqpass,
port: parseInt(process.env.MQTT_PORT) || 1883,
tls: process.env.MQTT_TLS || false,
prefix: process.env.MQTT_PREFIX || 'homeassistant',
namePrefix: process.env.MQTT_NAME_PREFIX || '',
};
logger.info('MQTT Config', {mqttConfig});
const init = () => new Commands(OnStar.create(onstarConfig));
const getVehicles = async commands => {
logger.info('Requesting vehicles');
const vehiclesRes = await commands.getAccountVehicles();
logger.info('Vehicle request status', {status: _.get(vehiclesRes, 'status')});
const vehicles = _.map(
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
v => new Vehicle(v)
);
logger.debug('Vehicle request response', {vehicles: _.map(vehicles, v => v.toString())});
return vehicles;
}
const getCurrentVehicle = async commands => {
const vehicles = await getVehicles(commands);
const currentVeh = _.find(vehicles, v => v.vin.toLowerCase() === onstarConfig.vin.toLowerCase());
if (!currentVeh) {
throw new Error(`Configured vehicle VIN ${onstarConfig.vin} not available in account vehicles`);
}
return currentVeh;
}
const connectMQTT = async availabilityTopic => {
const url = `${mqttConfig.tls ? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`;
const config = {
username: mqttConfig.username,
password: mqttConfig.password,
will: {topic: availabilityTopic, payload: 'false', retain: true}
};
logger.info('Connecting to MQTT', {url, config: _.omit(config, 'password')});
const client = await mqtt.connectAsync(url, config);
logger.info('Connected to MQTT');
return client;
}
const configureMQTT = async (commands, client, mqttHA) => {
if (!onstarConfig.allowCommands)
return;
client.on('message', (topic, message) => {
logger.debug('Subscription message', {topic, message});
const {command, options} = JSON.parse(message);
const cmd = commands[command];
if (!cmd) {
logger.error('Command not found', {command});
return;
}
const commandFn = cmd.bind(commands);
logger.info('Command sent', {command});
commandFn(options || {})
.then(data => {
// TODO refactor the response handling for commands
logger.info('Command completed', {command});
const location = _.get(data, 'response.data.commandResponse.body.location');
if (data && location) {
logger.info('Command response data', {location});
const topic = mqttHA.getStateTopic({name: command});
// TODO create device_tracker entity. MQTT device tracker doesn't support lat/lon and mqtt_json
// doesn't have discovery
client.publish(topic,
JSON.stringify({latitude: location.lat, longitude: location.long}), {retain: true})
.then(() => logger.info('Published location to topic.', {topic}));
}
})
.catch(err=> logger.error('Command error', {command, err}));
});
const topic = mqttHA.getCommandTopic();
logger.info('Subscribed to command topic', {topic});
await client.subscribe(topic);
};
(async () => {
try {
const commands = init();
const vehicle = await getCurrentVehicle(commands);
const mqttHA = new MQTT(vehicle, mqttConfig.prefix, mqttConfig.namePrefix);
const availTopic = mqttHA.getAvailabilityTopic();
const client = await connectMQTT(availTopic);
client.publish(availTopic, 'true', {retain: true})
.then(() => logger.debug('Published availability'));
await configureMQTT(commands, client, mqttHA);
const configurations = new Map();
const run = async () => {
const states = new Map();
const v = vehicle;
logger.info('Requesting diagnostics');
const statsRes = await commands.diagnostics({diagnosticItem: v.getSupported()});
logger.info('Diagnostic request status', {status: _.get(statsRes, 'status')});
const stats = _.map(
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
d => new Diagnostic(d)
);
logger.debug('Diagnostic request response', {stats: _.map(stats, s => s.toString())});
for (const s of stats) {
if (!s.hasElements()) {
continue;
}
// configure once, then set or update states
for (const d of s.diagnosticElements) {
const topic = mqttHA.getConfigTopic(d)
const payload = mqttHA.getConfigPayload(s, d);
configurations.set(topic, {configured: false, payload});
}
const topic = mqttHA.getStateTopic(s);
const payload = mqttHA.getStatePayload(s);
states.set(topic, payload);
}
const publishes = [];
// publish sensor configs
for (let [topic, config] of configurations) {
// configure once
if (!config.configured) {
config.configured = true;
const {payload} = config;
logger.info('Publishing message', {topic, payload});
publishes.push(
client.publish(topic, JSON.stringify(payload), {retain: true})
);
}
}
// update sensor states
for (let [topic, state] of states) {
logger.info('Publishing message', {topic, state});
publishes.push(
client.publish(topic, JSON.stringify(state), {retain: true})
);
}
await Promise.all(publishes);
};
const main = async () => run()
.then(() => logger.info('Updates complete, sleeping.'))
.catch(e => logger.error('Error', {error: e}))
await main();
setInterval(main, onstarConfig.refreshInterval);
} catch (e) {
logger.error('Main function error.', {error: e});
}
})();