teslams
Version:
Utilities for the Tesla Model S
1,058 lines (1,018 loc) • 60.7 kB
JavaScript
#!/usr/bin/env node
// create a local http server on port 8766 that visualizes the path of a Tesla Model S
// the data is taken from a MongoDB database
// the server can visualize / fast forward through past data in that database
// the client (as viewed in a browser) keeps updating real time until stopped
//
// You need a valid Google Maps v3 API key to use this script
// https://developers.google.com/maps/documentation/javascript/tutorial#api_key
//
require('pkginfo')(module, 'version');
console.log( module.exports.version );
var apiKey = 'AIzaSyBAQ9orToKfA-vAzbFjdyE-PIE86P2IKBY';
function argchecker( argv ) {
if (argv.db === true) throw 'MongoDB database name is unspecified. Use -d dbname or --db dbname';
}
var argv = require('optimist')
.usage('Usage: $0 --db <MongoDB database> [--port <http listen port>] [--silent] [--verbose]')
.check( argchecker )
.alias('p', 'port')
.describe('p', 'Listen port for the local http server')
.default('p', 8766)
.alias('d', 'db')
.describe('d', 'MongoDB database name')
.demand('d')
.alias('s', 'silent')
.describe('s', 'Silent mode: no output to console')
.boolean(['s'])
.alias('v', 'verbose')
.describe('v', 'Verbose mode: more output to console')
.boolean(['v'])
.alias('?', 'help')
.describe('?', 'Print usage information');
var creds = require('../config.js').config(argv);
argv = argv.argv;
if ( argv.help === true ) {
console.log( 'Usage: visualize.js --db <MongoDB database> [--silent] [--verbose]');
process.exit(1);
}
// set and check the validity of the HTTP listen port
// the environment variable $PORT is read for deployment on heroku
var httpport = process.env.PORT;
if ( !isNaN(httpport) && httpport >= 1) {
console.log('Using listen port (' + httpport + ') set by $PORT environment variable');
argv.port = httpport;
}
var MongoClient = require('mongodb').MongoClient;
var mongoUri = process.env.MONGOLAB_URI|| process.env.MONGOHQ_URI || 'mongodb://127.0.0.1:27017/' + argv.db;
console.log('Using MongoDB URI: ' + mongoUri);
var date = new Date();
var http = require('http');
var fs = require('fs');
var lastTime = 0;
var started = false;
var from, to;
var capacity;
var express = require('express');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
require('express-namespace');
var app = express();
var passport = require('passport'), LocalStrategy = require('passport-local').Strategy;
var speedup = 60000;
var nav = "";
var system = "";
var fwVersion = "";
var vin = "";
var name = "";
var optionString = "";
var rawoptions = "";
var baseString = "";
var baseText = {
RENA: "North American",
REEU: "European",
TM02: "Signature",
PBT85: "P85",
BT85: "85",
PX01: "PLUS",
BT60: "60",
BT40: "40",
P85D: "P85",
BT70: "70",
DV4W: "D",
BP01: "L"
};
var optionText = {
PPTI: "<li> titanium metallic</li>",
PBSB: "<li> black</li>",
PMBL: "<li> obsidian black</li>",
PBCW: "<li> solid white</li>",
PMSS: "<li> silver</li>",
PMNG: "<li> midnight silver metallic</li>",
PMTG: "<li> dolphin gray metallic</li>",
PMAB: "<li> metallic brown</li>",
PMMB: "<li> metallic blue</li>",
PPSB: "<li> metallic blue</li>",
PMSG: "<li> metallic green</li>",
PPSW: "<li> pearl white</li>",
PPMR: "<li> multi-coat red</li>",
PPSR: "<li> signature red</li>",
RFPO: "<li> panorama roof</li>",
RFP2: "<li> panorama roof</li>",
WTAR: "<li> aero 19\" wheels</li>",
WT19: "<li> silver 19\" wheels</li>",
WT21: "<li> silver 21\" wheels</li>",
WTSP: "<li> gray 21\" wheels</li>",
WTSG: "<li> gray performance 21\" wheels</li>",
TR01: "<li> third row seats</li>",
SU01: "<li> air suspension</li>",
SC01: "<li> super charger enabled</li>",
TP01: "<li> tech package</li>",
TP02: "<li> tech package with autopilot</li>",
AU01: "<li> audio upgrade</li>",
CH01: "<li> dual charger</li>",
PK01: "<li> parking sensors</li>",
CW01: "<li> cold weather package</li>",
CW02: "<li> cold weather package</li>",
LP01: "<li> premium interior lighting</li>",
PI01: "<li> premium interior and lighting</li>",
FG02: "<li> premium exterior lighting</li>",
SP01: "<li> security package</li>",
IDHG: "<li> obeche gloss wood decor</li>",
IDOG: "<li> obeche gloss wood decor</li>",
IDOM: "<li> obeche matte wood decor</li>",
IDHM: "<li> obeche matte wood decor</li>",
IDCF: "<li> carbon fiber decor</li>",
QZMB: "<li> black performance leather seats</li>",
QYMB: "<li> black performance leather seats</li>",
QXMB: "<li> black leather seats</li>",
QPMB: "<li> black leather seats</li>",
QREB: "<li> black next generation leather seats</li>",
QRLB: "<li> black next generation leather seats</li>",
QNEB: "<li> black next generation leather seats</li>",
QNLB: "<li> black next generation leather seats</li>",
QYMT: "<li> tan performance leather seats</li>",
QZMT: "<li> tan performance leather seats</li>",
QPMT: "<li> tan leather seats</li>",
QXMT: "<li> tan leather seats</li>",
QRET: "<li> tan next generation leather seats</li>",
QRLT: "<li> tan next generation leather seats</li>",
QNLT: "<li> tan next generation leather seats</li>",
QNET: "<li> tan next generation leather seats</li>",
QYMG: "<li> grey performance leather seats</li>",
QZMG: "<li> grey performance leather seats</li>",
QXMG: "<li> grey leather seats</li>",
QCMG: "<li> grey leather seats</li>",
QREG: "<li> grey next generation leather seats</li>",
QRLG: "<li> grey next generation leather seats</li>",
QNLG: "<li> grey next generation leather seats</li>",
QNEG: "<li> grey next generation leather seats</li>",
QRLG: "<li> grey next generation leather seats</li>",
SR02: "<li> executive rear seats</li>",
UTAW: "<li> white alcantara headliner</li>",
UTAB: "<li> black alcantara headliner</li>",
IX01: "<li> extended nappa leather</li>",
X019: "<li> carbon fibre spoiler</li>",
BC0R: "<li> red brake calipers</li>",
PA01: "<li> paint armor</li>",
YF01: "<li> matching yacht floor</li>",
DA02: "<li> autopilot</li>"
};
// this is super simplistic for now. Clear text passwords, nothing fancy, trivial to
// intercept - but it's a start
var users;
if (creds !== undefined && creds.hasOwnProperty('visualize') && creds.visualize.hasOwnProperty('webusers')) {
users = creds.visualize.webusers;
}
// Set the baseUrl property in your config.json if you want to run visualize in a
// path other than the root of the webserver. For example, to run visualize at
// http://example.com/teslavis, set visualize.baseUrl to "/teslavis"
var baseUrl;
if (creds !== undefined && creds.hasOwnProperty('visualize') && creds.visualize.hasOwnProperty('baseUrl')) {
console.log("Found a baseUrl (" + creds.visualize.baseUrl + ") in the config.json file");
baseUrl = creds.visualize.baseUrl;
} else {
baseUrl = "";
}
// please change this to have at least some trivial session hijacking protection
var localSecret = "please change this secret string";
// these two functions implement our simplistic user lookup
function findById(id, fn) {
var idx = id - 1;
if (users[idx]) {
fn(null, users[idx]);
} else {
fn(new Error('User ' + id + ' does not exist'));
}
}
function findByUsername(username, fn) {
if (!argv.silent) console.log("find user", username);
for (var i = 0, len = users.length; i < len; i++) {
var user = users[i];
if (user.username === username) {
return fn(null, user);
}
}
return fn(null, null);
}
// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// serialize users into and deserialize users out of the session. Typically,
// this will be as simple as storing the user ID when serializing, and finding
// the user by ID when deserializing.
passport.serializeUser(function(user, store) {
store(null, user.id);
});
passport.deserializeUser(findById);
//
// now setup passport and route it into our express setup
//
passport.use(new LocalStrategy(
function(username, password, done) {
findByUsername(username, function(err, user) {
if (err) {
return done(err);
}
if (!user || user.password != password) {
return done(null, false, { message: 'Invalid user or password' });
}
return done(null, user);
});
}
));
app.namespace(baseUrl, function() {
app.use(cookieParser());
app.use(bodyParser.urlencoded({
extended: true,
}));
app.use(bodyParser.json());
app.use(session({
secret: localSecret,
resave: false,
saveUninitialized: true,
}));
app.use(passport.initialize());
app.use(passport.session());
// simple login screen with correspoding POST setup
app.get('/login', function(req,res) {
fs.readFile(__dirname + "/login.html", "utf-8", function(err, data) {
if (err) throw err;
res.send(data);
});
});
app.post('/login', passport.authenticate('local', { successRedirect: baseUrl + '/',
failureRedirect: baseUrl + '/login' }));
// call this in every app.get() that should only be accessible when logged in
function ensureAuthenticated(req, res, next) {
if (users === undefined || req.isAuthenticated()) {
return next();
}
res.redirect(baseUrl + '/login');
}
// read in the shared navigation bar so we can insert this into every page
fs.readFile(__dirname + "/otherfiles/nav.html", "utf-8", function(err, data) {
if (err) throw err;
nav = data.replace(/BASE/g,baseUrl);
});
// shorthand to get leading zero when showing minutes
function lZ(mins) {
var zmins = '0' + mins;
return zmins.substr(zmins.length - 2);
}
function makeDate(string, offset) {
var args = string.split('-');
var date = new Date(args[0], args[1]-1, args[2], args[3], args[4], args[5]);
if (offset !== undefined )
date = new Date(date.getTime() + offset);
return date;
}
function dateString(time) {
return time.getFullYear() + '-' + (time.getMonth()+1) + '-' + time.getDate() + '-' +
time.getHours() + '-' + time.getMinutes() + '-' + time.getSeconds();
}
function dashDate(date, filler) { // date-time with all '-'
var c = date.replace('%20','-').replace(' ','-').split('-');
for (var i = c.length; i < 6; i++)
c.push(filler[i-3]);
return c[0] + '-' + c[1] + '-' + c[2] + '-' + c[3] + '-' + c[4] + '-' + c[5];
}
function parseDates(fromQ, toQ) {
if (toQ == undefined || toQ === null || toQ === "" || toQ.split('-').count < 2) // no valid to argument -> to = now
this.toQ = dateString(new Date());
else
this.toQ = dashDate(toQ,['00','00','00']);
if (fromQ == undefined || fromQ === null || fromQ === "" || fromQ.split('-').count < 2) // no valid from argument -> 12h before to
this.fromQ = dashDate(dateString(makeDate(this.toQ, -12 * 3600 * 1000)));
else
this.fromQ = dashDate(fromQ,['23','59','59']);
}
function weekNr(d) {
d = new Date(d);
d.setHours(0,0,0);
d.setDate(d.getDate() + 4 - (d.getDay()));
var yearStart = new Date(d.getFullYear(),0,1);
return Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
}
MongoClient.connect(mongoUri, function(err, db) {
// this is the first time we connect - if we get an error, just throw it
if(err) throw(err);
var collectionA = db.collection("tesla_aux");
// get the last stored entry that describes the vehicles
var query = {'vehicles': { '$exists': true } };
var options = { 'sort': [['ts', 'desc']], 'limit': 1};
collectionA.find(query, options).toArray(function(err, docs) {
if (argv.verbose) console.dir(docs);
if (docs.length === 0) {
console.log("missing vehicles data in db, assuming Model S 60");
capacity = 60;
} else {
if (docs.length > 1)
console.log("congratulations, you have more than one Tesla Model S - this only supports your first car");
vin = docs[0].vehicles.vin;
name = docs[0].vehicles.display_name;
optionString = "<ul>";
rawoptions = docs[0].vehicles.option_codes;
rawoptions = rawoptions.replace("PPSR", "COL0-PPSR");
rawoptions = rawoptions.replace("PBSB", "COL1-PBSB");
rawoptions = rawoptions.replace("PBCW", "COL1-PBCW");
rawoptions = rawoptions.replace("PMAB", "COL2-PMAB");
rawoptions = rawoptions.replace("PMSG", "COL2-PMSG");
rawoptions = rawoptions.replace("PMMB", "COL2-PMMB");
rawoptions = rawoptions.replace("PMNG", "COL2-PMNG");
rawoptions = rawoptions.replace("PMBL", "COL2-PMBL");
rawoptions = rawoptions.replace("PMSS", "COL2-PMSS");
rawoptions = rawoptions.replace("PMTG", "COL2-PMTG");
rawoptions = rawoptions.replace("PMTI", "COL2-PMTI");
rawoptions = rawoptions.replace("PPTI", "COL2-PPTI");
rawoptions = rawoptions.replace("PPSW", "COL3-PPSW");
rawoptions = rawoptions.replace("PPMR", "COL3-PPMR");
var options = docs[0].vehicles.option_codes.split(',');
for (var i = 0; i < options.length; i++) {
if (optionText[options[i]] !== undefined)
optionString += optionText[options[i]];
if (baseText[options[i]] !== undefined)
baseString += " " + baseText[options[i]];
if (options[i] == "PX01") {
baseString = baseString.replace("PLUS", "+");
}
if (options[i].substring(0,2) == "BT") {
if (options[i] == "BT85") {
capacity = 85;
} else if (options[i] == "BT60") {
capacity = 60;
} else if (options[i] == "BT40") {
capacity = 60;
}
}
}
optionString += "</ul>";
baseString = baseString.replace("PLUS", "");
if (argv.verbose) console.log(baseString);
if (argv.verbose) console.log(optionString);
}
if (argv.verbose) console.log("battery capacity", capacity);
});
query = {'guiSettings': { '$exists': true } };
options = { 'sort': [['ts', 'desc']], 'limit': 1};
collectionA.find(query,options).toArray(function(err, docs) {
if (docs.length == 0) {
console.log("missing GUI settings in db, assuming imperial");
system = "imperial";
} else {
if (docs[0].guiSettings.gui_distance_units == "mi/hr") { // hey Tesla - that's a speed, not a distance
system = "imperial";
} else {
system = "metric";
}
}
});
query = {'vehicleState': { '$exists': true } };
options = { 'sort': [['ts', 'desc']], 'limit': 1};
collectionA.find(query, options).toArray(function(err, docs) {
if (docs.length == 0) {
console.log("missing vehicleState settings in db");
} else {
var fwBuild = docs[0].vehicleState.car_version;
if (fwBuild != undefined) {
if (fwBuild.substr(0,4) == "1.25")
fwVersion = "4.3 ";
else if (fwBuild.substr(0,4) == "1.31")
fwVersion = "4.4 ";
else if (fwBuild.substr(0,4) == "1.33")
fwVersion = "4.5 ";
else if (fwBuild.substr(0,4) == "1.35")
fwVersion = "5.0 ";
else if (fwBuild.substr(0,4) == "1.45")
fwVersion = "5.6 ";
else if (fwBuild.substr(0,4) == "1.49")
fwVersion = "5.8 ";
else if (fwBuild.substr(0,4) == "1.51")
fwVersion = "5.9 ";
else if (fwBuild.substr(0,4) == "1.59")
fwVersion = "5.11 ";
else if (fwBuild.substr(0,4) == "1.64")
fwVersion = "5.12 ";
else if (fwBuild.substr(0,4) == "1.66")
fwVersion = "5.14 ";
else if (fwBuild.substr(0,4) == "1.67")
fwVersion = "6.0 ";
else if (fwBuild.substr(0,4) == "2.2.")
fwVersion = "6.1 ";
else if (fwBuild.substr(0,4) == "2.4.")
fwVersion = "6.2 ";
else if (fwBuild.substr(0,4) == "2.5.")
fwVersion = "6.2 ";
else if (fwBuild.substr(0,4) == "2.7.")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,4) == "2.8.")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,6) == "2.9.12")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,6) == "2.9.40")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,6) == "2.9.68")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,6) == "2.9.74")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,6) == "2.9.77")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,7) == "2.10.10")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,7) == "2.10.20")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,7) == "2.10.26")
fwVersion = "7.0 ";
else if (fwBuild.substr(0,7) == "2.9.154")
fwVersion = "7.1 ";
else if (fwBuild.substr(0,7) == "2.9.172")
fwVersion = "7.1 ";
fwVersion += "(" + fwBuild + ")";
} else {
fwVersion = 'unknown';
}
}
});
});
if (argv.verbose) app.use(express.logger('dev'));
app.get('/', ensureAuthenticated, function(req, res) {
// friendly welcome screen
fs.readFile(__dirname + "/welcome.html", "utf-8", function(err, data) {
if (err) throw err;
res.send(data.replace("MAGIC_NAV",nav)
.replace("MAGIC_RAWOPTIONS", rawoptions)
.replace("MAGIC_OPTIONS", baseString + optionString)
.replace("MAGIC_VIN", vin)
.replace("MAGIC_NAME", name)
.replace("MAGIC_FIRMWARE_VERSION", fwVersion)
.replace("MAGIC_DISPLAY_SYSTEM", system)
.replace("MAGIC_TESLAMS_VERSION", module.exports.version));
});
});
app.get('/getdata', ensureAuthenticated, function (req, res) {
var ts, options, vals;
if (argv.verbose) console.log('/getdata with',req.query.at);
MongoClient.connect(mongoUri, function(err, db) {
if(err) {
console.log('error connecting to database:', err);
return;
}
var collection = db.collection("tesla_stream");
if (req.query.at === null) {
if (argv.verbose) console.log("why is there no 'at' parameter???");
return;
}
// get the data at time 'at'
ts = +req.query.at;
options = { 'sort': [['ts', 'desc']], 'limit': 1};
collection.find({"ts": {"$lte": +ts}}, options).toArray(function(err,docs) {
if (argv.verbose) console.log("got datasets:", docs.length);
if (docs.length === 0) {
// that shouldn't happen unless the database is empty...
console.log("no data found for /getdata request at time", console.log(new Date(+ts).toString));
return;
}
res.setHeader("Content-Type", "application/json");
vals = docs[0].record.toString().replace(",,",",0,").split(",");
res.write("[" + JSON.stringify(vals) + "]", "utf-8");
res.end();
db.close();
});
});
});
app.get('/storetrip', ensureAuthenticated, function(req, res) {
MongoClient.connect(mongoUri, function(err, db) {
if (err) {
console.log('error connecting to database:', err);
return;
}
var collection = db.collection("trip_data");
collection.remove({ 'dist': '-1'}, function(err,docs) {
collection.insert(req.query, { 'safe': true }, function(err,docs) {
if (err) {
res.send(err);
} else {
res.send("OK");
}
});
});
});
});
app.get('/getlasttrip', ensureAuthenticated, function(req, res) {
MongoClient.connect(mongoUri, function(err, db) {
if (err) {
console.log('error connecting to database:', err);
return;
}
var collection = db.collection("trip_data");
var options = { 'sort': [['chargeState.battery_range', 'desc']] };
collection.find({},{ 'sort': [['from', 'desc']], 'limit': 1 }).toArray(function(err,docs) {
console.log(docs);
res.setHeader("Content-Type", "application/json");
res.send(docs);
});
});
});
app.get('/update', ensureAuthenticated, function (req, res) {
// we don't keep the database connection as that has caused occasional random issues while testing
if (!started)
return;
MongoClient.connect(mongoUri, function(err, db) {
if(err) {
console.log('error connecting to database:', err);
return;
}
var collection = db.collection("tesla_stream");
if (req.query.until === null) {
console.log("why is there no 'until' parameter???");
return;
}
// get the data until 'until'
// but not past the end of the requested segment and not past the current time
var endTime = +req.query.until;
if (to && +endTime > +to)
endTime = +to;
var currentTime = new Date().getTime();
if (+endTime > +currentTime)
endTime = +currentTime;
collection.find({"ts": {"$gt": +lastTime, "$lte": +endTime}}).toArray(function(err,docs) {
if (argv.verbose) console.log("got datasets:", docs.length);
if (docs.length === 0) {
// create one dummy entry so the map app knows the last time we looked at
docs = [ { "ts": +endTime, "record": [ +lastTime+"" ,"0","0","0","0","0","0","0","0","0","0","0"]} ];
}
res.setHeader("Content-Type", "application/json");
res.write("[", "utf-8");
var comma = "";
docs.forEach(function(doc) {
// the tesla streaming service replaces a few items with "" when the car is off
// the reg exp below replaces the two that are numerical with 0 (the shift_state stays unchanged)
var vals = doc.record.toString().replace(",,",",0,").split(",");
res.write(comma + JSON.stringify(vals), "utf-8");
lastTime = +doc.ts;
comma = ",";
});
res.end("]", "utf-8");
if (!argv.silent) {
var showTime = new Date(lastTime);
console.log("last timestamp:", lastTime, showTime.toString());
}
db.close();
});
});
});
app.get('/map', ensureAuthenticated, function(req, res) {
var params = "";
if (req.query.lang !== undefined)
params = "&lang=" + req.query.lang;
if (req.query.metric === "true")
params += "&metric=true";
var dates = new parseDates(req.query.from, req.query.to);
from = makeDate(dates.fromQ);
to = makeDate(dates.toQ);
if (req.query.speed !== null && req.query.speed !== "" && req.query.speed <= 120 && req.query.speed >= 1)
speedup = req.query.speed * 2000;
if (req.query.to === undefined || req.query.to.split('-').length < 6 ||
req.query.from === undefined || req.query.from.split('-').length < 6) {
var speedQ = speedup / 2000;
res.redirect(baseUrl + '/map?from=' + dates.fromQ + '&to=' + dates.toQ + '&speed=' + speedQ.toFixed(0) + params);
return;
}
MongoClient.connect(mongoUri, function(err, db) {
if(err) {
console.log('error connecting to database:', err);
return;
}
var collection = db.collection("tesla_stream");
var searchString = {$gte: +from, $lte: +to};
collection.find({"ts": searchString}).limit(1).toArray(function(err,docs) {
if (argv.verbose) console.log("got datasets:", docs.length);
docs.forEach(function(doc) {
var record = doc.record;
var vals = record.toString().replace(",,",",0,").split(/[,\n\r]/);
lastTime = +vals[0];
res.setHeader("Content-Type", "text/html");
fs.readFile(__dirname + "/map.html", "utf-8", function(err, data) {
if (err) throw err;
var response = data.replace("MAGIC_APIKEY", apiKey)
.replace("MAGIC_FIRST_LOC", vals[6] + "," + vals[7])
.replace("MAGIC_NAV", nav)
.replace("MAGIC_DISPLAY_SYSTEM", '"' + system + '"');
res.end(response, "utf-8");
});
});
db.close();
started = true;
});
});
if (!argv.silent) console.log('done sending the initial page');
});
app.get('/energy', ensureAuthenticated, function(req, res) {
var path = req.path;
var params = "";
if (req.query.lang !== undefined)
params = "&lang=" + req.query.lang;
if (req.query.metric === "true")
params += "&metric=true";
var dates = new parseDates(req.query.from, req.query.to);
from = makeDate(dates.fromQ);
to = makeDate(dates.toQ);
if (req.query.to === undefined || req.query.to.split('-').length < 6 ||
req.query.from === undefined || req.query.from.split('-').length < 6) {
res.redirect(baseUrl + '/energy?from=' + dates.fromQ + '&to=' + dates.toQ + params);
return;
}
// don't deliver more than 10000 data points (that's one BIG screen)
var halfIncrement = Math.round((+to - +from) / 20000);
var increment = 2 + halfIncrement;
var outputE = "", outputS = "", outputSOC = "", outputRange = "", firstDate = 0, lastDate = 0;
var minE = 1000, minS = 1000, minSOC = 1000;
var maxE = -1000, maxS = -1000, maxSOC = -1000;
var gMaxE = -1000, gMaxS = -1000;
var gMinE = 1000, gMinS = 1000;
var cumulE = 0, cumulR = 0, cumulES, cumulRS, prevTS;
MongoClient.connect(mongoUri, function(err, db) {
var speed, energy, soc, vals;
if(err) {
console.log('error connecting to database:', err);
return;
}
res.setHeader("Content-Type", "text/html");
var collection = db.collection("tesla_stream");
collection.find({"ts": {$gte: +from, $lte: +to}}).toArray(function(err,docs) {
docs.forEach(function(doc) {
vals = doc.record.toString().replace(",,",",0,").split(",");
speed = parseInt(vals[1]);
energy = parseInt(vals[8]);
soc = parseInt(vals[3]);
if (firstDate === 0) {
firstDate = lastDate = prevTS = doc.ts;
outputE = "[" + (+from) + ",0]";
outputS = "[" + (+from) + ",0]";
outputSOC = "[" + (+from) + "," + soc + "],null";
}
if (doc.ts >= lastDate) {
if (doc.ts > lastDate + increment) {
if (maxE != -1000) {
outputE += ",[" + (+lastDate + halfIncrement) + "," + maxE + "]";
outputE += ",[" + (+lastDate + increment) + "," + minE + "]";
}
if (maxS != -1000)
outputS += ",[" + (+lastDate + halfIncrement) + "," + (+maxS + minS) / 2 + "]";
if (maxSOC != -1000)
outputSOC += ",[" + (+lastDate + halfIncrement) + "," + (+maxSOC + minSOC) / 2 + "]";
lastDate = doc.ts;
if (+maxE > +gMaxE) gMaxE = maxE;
if (+minE < +gMinE) gMinE = minE;
if (+maxS > +gMaxS) gMaxS = maxS;
maxE = maxS = maxSOC = -1000;
minE = minS = minSOC = 1000;
}
if (energy > 0) cumulE += energy * (doc.ts - prevTS);
if (energy < 0) cumulR += energy * (doc.ts - prevTS);
if (energy > maxE) maxE = energy;
if (energy < minE) minE = energy;
if (speed > maxS) maxS = speed;
if (speed < minS) minS = speed;
if (soc > maxSOC) maxSOC = soc;
if (soc < minSOC) minSOC = soc;
prevTS = doc.ts;
}
});
cumulE = cumulE / 3600000;
cumulR = cumulR / 3600000;
if (cumulE > 1) {
cumulES = cumulE.toFixed(1) + "kWh";
cumulRS = (-cumulR).toFixed(1) + "kWh";
} else {
cumulES = (cumulE * 1000).toFixed(0) + "Wh";
cumulRS = (-cumulR * 1000).toFixed(0) + "Wh";
}
var chartEnd = lastDate;
// now look for data in the aux collection
var collection = db.collection("tesla_aux");
var maxAmp = 0, maxVolt = 0, maxMph = 0, maxPower = 0;
var outputAmp = "", outputVolt = "", outputPower = "";
var amp, volt, power;
lastDate = +from;
collection.find({"chargeState": {"$exists": true},
"ts": {$gte: +from, $lte: +to}}).toArray(function(err,docs) {
if (argv.verbose) console.log("Found " + docs.length + " entries in aux DB");
outputAmp = "[" + (+firstDate) + ",0]";
outputVolt = "[" + (+firstDate) + ",0]";
outputPower = "[" + (+firstDate) + ",0]";
var comma = "";
docs.forEach(function(doc) {
amp = volt = 0;
if(doc.chargeState.charging_state === 'Charging') {
if (doc.chargeState.charger_actual_current !== undefined) {
if (doc.chargeState.charger_actual_current !== 0) {
amp = doc.chargeState.charger_actual_current;
} else {
amp = doc.chargeState.battery_current;
}
outputAmp += ",[" + doc.ts + "," + amp + "]";
lastDate = doc.ts;
}
if (doc.chargeState.charger_voltage !== undefined) {
volt = doc.chargeState.charger_voltage;
outputVolt += ",[" + doc.ts + "," + volt + "]";
lastDate = doc.ts;
}
if (lastDate == doc.ts) { // we had valid values
power = parseFloat(volt) * parseFloat(amp) / 1000;
outputPower += ",[" + doc.ts + "," + power.toFixed(1) + "]";
if (power > maxPower) {
maxPower = power;
maxAmp = amp;
maxVolt = volt;
maxMph = doc.chargeState.charge_rate;
}
}
} else if (doc.chargeState.charging_state === 'Disconnected' ||
doc.chargeState.charging_state === 'Complete' ||
doc.chargeState.charging_state === 'Pending' ||
doc.chargeState.charging_state === 'Starting' ||
doc.chargeState.charging_state === 'Stopped') {
outputAmp += ",[" + doc.ts + ",0]";
outputVolt += ",[" + doc.ts + ",0]";
outputPower += ",[" + doc.ts + ",0]";
}
if (doc.chargeState.battery_range !== undefined) {
outputRange += comma + "[" + doc.ts + "," + doc.chargeState.battery_range + "]";
comma = ",";
}
});
outputAmp += ",[" + (lastDate + 60000) + ",0]";
outputVolt += ",[" + (lastDate + 60000) + ",0]";
outputPower += ",[" + (lastDate + 60000) + ",0]";
outputAmp += ",[" + (+chartEnd) + ",0]";
outputVolt += ",[" + (+chartEnd) + ",0]";
outputPower += ",[" + (+chartEnd) + ",0]";
db.close();
fs.readFile(__dirname + "/energy.html", "utf-8", function(err, data) {
if (err) throw err;
var fD = new Date(firstDate);
var startDate = (fD.getMonth() + 1) + "/" + fD.getDate() + "/" + fD.getFullYear();
gMinE = +gMinE - 10;
gMaxE = +gMaxE + 10;
if (2 * gMaxS > +gMaxE) {
gMaxS = +gMaxS + 5;
gMaxE = 2 * gMaxS;
} else {
gMaxS = gMaxE / 2;
}
gMinS = gMinE / 2;
var response = data.replace("MAGIC_NAV", nav)
.replace("MAGIC_ENERGY", outputE)
.replace("MAGIC_SPEED", outputS)
.replace("MAGIC_SOC", outputSOC)
.replace("MAGIC_START", startDate)
.replace("MAGIC_MAX_ENG", gMaxE)
.replace("MAGIC_MIN_ENG", gMinE)
.replace("MAGIC_MAX_SPD", gMaxS)
.replace("MAGIC_MIN_SPD", gMinS)
.replace("MAGIC_CUMUL_E", cumulES)
.replace("MAGIC_CUMUL_R", cumulRS)
.replace("MAGIC_VOLT", outputVolt)
.replace("MAGIC_AMP", outputAmp)
.replace("MAGIC_POWER", outputPower)
.replace("MAGIC_RANGE", outputRange)
.replace("MAGIC_MAX_VOLT", maxVolt)
.replace("MAGIC_MAX_AMP", maxAmp)
.replace("MAGIC_MAX_KW", maxPower.toFixed(1))
.replace("MAGIC_MAX_MPH", maxMph)
.replace("MAGIC_CAPACITY", capacity)
.replace("MAGIC_DISPLAY_SYSTEM", '"' + system + '"');
res.end(response, "utf-8");
if (argv.verbose) console.log("delivered", outputSOC.length,"records and", response.length, "bytes");
});
});
});
});
});
function countCharge(ts) {
if (!countCharge.start)
countCharge.start = ts;
}
function stopCountingCharge(ts) {
if (countCharge.start && ts - countCharge.start > 60000) // at least a minute
countCharge.chargeInt.push([countCharge.start,ts]);
countCharge.start = null;
}
function countVamp(ts) {
if (!countVamp.start)
countVamp.start = ts;
}
function stopCountingVamp(ts) {
if (countVamp.start && ts - countVamp.start > 60000) // at least a minute
countVamp.vampInt.push([countVamp.start,ts]);
countVamp.start = null;
}
function calculateDelta(d1, d2) {
var cS1 = d1.chargeState, cS2 = d2.chargeState;
if (!cS1 || !cS2 || !cS1.battery_level || !cS2.battery_level || cS2.battery_range > cS1.battery_range) {
return 0;
}
// var ratedWh = 5 * ((cS1.battery_level * capacity / cS1.battery_range) + (cS2.battery_level * capacity / cS2.battery_range));
// let's use the data that we seem to are converging on in the forums instead:
var ratedWh = (capacity == 85) ? 286 : 267;
var delta = ratedWh * (cS1.battery_range - cS2.battery_range);
// if (argv.verbose) { // great for debugging
// console.log(new Date(d1.ts), new Date(d2.ts), "ratedWh", ratedWh.toFixed(1),
// "delta range", (cS1.battery_range - cS2.battery_range).toFixed(1) ,"delta", delta.toFixed(1));
// }
return delta / 1000;
}
app.get('/test', ensureAuthenticated, function(req, res) {
MongoClient.connect(mongoUri, function(err, db) {
if(err) {
console.log('error connecting to database:', err);
return;
}
var output = "";
var collection = db.collection("tesla_aux");
var options = { 'sort': [['chargeState.battery_range', 'desc']] };
collection.find({"chargeState": {$exists: true}}).toArray(function(err,docs) {
var comma = "";
docs.forEach(function(doc) {
if (doc.chargeState.battery_level !== undefined) {
output += comma + "\n[" + doc.chargeState.battery_level + "," + doc.chargeState.battery_range + "]";
comma = ',';
}
});
db.close();
fs.readFile(__dirname + "/test.html", "utf-8", function(err, data) {
if (err) throw err;
res.send(data.replace("MAGIC_TEST", output));
});
});
});
});
app.get('/stats', ensureAuthenticated, function(req, res) {
var debugStartTime = new Date().getTime();
var path = req.path;
var params = "";
if (req.query.lang !== undefined)
params = "&lang=" + req.query.lang;
if (req.query.metric === "true")
params += "&metric=true";
var dates = new parseDates(req.query.from, req.query.to);
countVamp.vampInt = [];
countCharge.chargeInt = [];
countVamp.start = null;
from = makeDate(dates.fromQ);
to = makeDate(dates.toQ);
if (req.query.to === undefined || req.query.to.split('-').length < 6 ||
req.query.from === undefined || req.query.from.split('-').length < 6) {
res.redirect(baseUrl + '/stats?from=' + dates.fromQ + '&to=' + dates.toQ + params);
return;
}
var outputD = "", outputC = "", outputA = "", comma, firstDate = 0, lastDay = 0, lastDate = 0, distHash = {}, useHash = {};
var outputWD = "", outputWC = "", outputWA = "", commaW, distWHash = {}, useWHash ={};
MongoClient.connect(mongoUri, function(err, db) {
if(err) {
console.log('error connecting to database:', err);
return;
}
res.setHeader("Content-Type", "text/html");
var collection = db.collection("tesla_stream");
if (argv.verbose)
console.log("starting DB request after", new Date().getTime() - debugStartTime, "ms");
collection.find({"ts": {"$gte": from.getTime(), "$lte": to.getTime()}}).toArray(function(err,docs) {
if (argv.verbose)
console.log("processing data after", new Date().getTime() - debugStartTime, "ms");
// this is really annoying; the values from the database frequently aren't sorted by
// timestamp, even if you didn't edit the data. So we need to sort here - the mongoDB
// sort function fails if the amount of data becomes too large :-(
docs.sort(function(a,b){return a.ts - b.ts;});
var vals = [];
var odo, energy, state, soc;
var dist, kWh, ts, midnight, used;
var week, distW = 0, kWhW = 0, usedW = 0, chargeW = 0;
var startOdo, charge, minSOC, maxSOC, increment, kWs;
if (docs === null) {
if (argv.verbose) {
console.log(err);
console.log("no output for stream from", +from, "to", +to);
}
return;
}
function updateWValues(f) {
var lw = weekNr(lastDate);
var ld = new Date(lastDate);
if (lw == week && f !== true)
return;
var wts = ld.getTime() - 24 * 3600 * 1000 * ld.getDay() - 3600 * 1000 * ld.getHours() - 60 * 1000 * ld.getMinutes() - 1000 * ld.getSeconds() - ld.getMilliseconds();
outputWD += commaW + "[" + wts + "," + distW + "]";
outputWC += commaW + "[" + wts + "," + chargeW + "]";
commaW = ",";
distWHash[wts+""] = distW;
useWHash[wts+""] = kWhW;
distW = chargeW = distW = kWhW = 0;
}
function updateValues() {
stopCountingVamp(lastDate);
stopCountingCharge(lastDate);
charge += increment * capacity / 100;
chargeW += charge;
dist = odo - startOdo;
distW += dist;
kWh = kWs / 3600;
kWhW += kWh;
ts = new Date(lastDate);
midnight = new Date(ts.getFullYear(), ts.getMonth(), ts.getDate(), 0, 0, 0);
outputD += comma + "[" + (+midnight) + "," + dist + "]";
outputC += comma + "[" + (+midnight) + "," + charge + "]";
distHash[midnight.getTime()+""] = dist;
useHash[midnight.getTime()+""] = kWh;
}
docs.forEach(function(doc) {
var day = new Date(doc.ts).getDay();
week = weekNr(doc.ts);
vals = doc.record.toString().replace(",,",",0,").split(",");
odo = parseFloat(vals[2]);
soc = parseFloat(vals[3]); // sadly, this is an integer today :-(
energy = parseInt(vals[8]);
state = vals[9];
if (firstDate === 0) {
firstDate = doc.ts - 1;
lastDay = day;
startOdo = odo;
minSOC = 101; maxSOC = -1; kWs = 0; charge = 0; increment = 0; comma = ""; commaW = "";
}
if (day != lastDay) {
updateValues();
updateWValues();
lastDay = day;
startOdo = odo;
minSOC = 101; maxSOC = -1; kWs = 0; charge = 0; increment = 0; comma = ",";
}
// this is crude - it would be much better to get this from
// the aux database and use the actual charge info
if (state != 'R' && state != 'D') { // we are not driving
if (energy < 0) { // parked & charging
stopCountingVamp(doc.ts);
countCharge(doc.ts);
if (soc < minSOC) minSOC = soc;
if (soc > maxSOC) maxSOC = soc;
increment = maxSOC - minSOC;
} else { // parked & consuming
countVamp(doc.ts);
stopCountingCharge(doc.ts);
// if we were charging before, add the estimate to the total
// this a quite coarse as SOC is in full percent - bad granularity
if (increment > 0) {
charge += increment * capacity / 100;
increment = 0; minSOC = 101; maxSOC = -1;
}
}
} else {
// we're driving - add up the energy used / regen
if (lastDate > 0)
kWs += (doc.ts - lastDate) / 1000 * (energy - 0.12); // this correction is needed to match in car data???
stopCountingVamp(doc.ts);
}
lastDate = doc.ts;
});
// we still need to add the last day
updateValues();
updateWValues(true);
// now analyze the charging data
collection = db.collection("tesla_aux");
collection.find({"chargeState": {$exists: true}, "ts": {$gte: +from, $lte: +to}}).toArray(function(err,docs) {
var i = 0, vampirekWh = 0, day, lastDay = -1, lastDate = null, comma = "", outputY = "", outputWY = "", commaW = "";
var j = 0, chargekWh = 0, outputCN = "", usedkWh = 0, outputUsed = "", outputWCN = "", outputWUsed = "";
var chargekWhW = 0, vampirekWhW = 0, usedW = 0;
var vState1 = null;
var cState1 = null;
var uState1 = null;
var lastDoc = null;
var maxI = countVamp.vampInt.length;
var maxJ = countCharge.chargeInt.length;
function updateChargeWValues(f) {
var ld, lw = weekNr(lastDate);
if (lw == week && f !== true)
return;
// if we force the display we need to get the week from the last doc
// that we had, otherwise we are showing last weeks data, so get it from lastDate
if (f === true) {
if (lastDoc !== null && lastDoc.ts !== undefined) {
ld = new Date(lastDoc.ts);
} else { // we have no data for this range
outputWY += commaW + "null";
outputWCN += commaW + "null";
outputWUsed += commaW + "null";
commaW = ',';
return;
}
} else {
ld = new Date(lastDate);
}
var wts = ld.getTime() - 24 * 3600 * 1000 * ld.getDay() - 3600 * 1000 * ld.getHours() - 60 * 1000 * ld.getMinutes() - 1000 * ld.getSeconds() - ld.getMilliseconds();
outputWY += commaW + "[" + wts + "," + vampirekWhW + "]";
outputWCN += commaW + "[" + wts + "," + chargekWhW + "]";
outputWUsed += commaW + "[" + wts + "," + usedW + "]";
dist = distWHash[wts+""];
if (dist > 0) {
outputWA += commaW