MMM-Strava
MMM-Strava copied to clipboard
Displaying stats for multiple athletes using a single API key
Hi Ian, love your module and I have been a supporter for many years. However I am trying to add two separate athletes and rather than seeing two different charts i see two of the same chart. I tried implementing it first by:
{ module: "MMM-Strava", position: "top_right", config: { client_id: ["id1, "id2"], client_secret: ["secret1, secret2"], } }
To no avail. It ignored id2 and secret2. So then reading the closed thread you said that making an entire new module block for athlete 2 would work so that's what i did:
{ module: "MMM-Strava", //athlete1 position: "top_right", config: { client_id: "id1", client_secret: "secret1", activities: ["run", "ride"], } } { module: "MMM-Strava", //athlete2 position: "top_right", config: { client_id: "id2", client_secret: "secret2", activities: ["run"],
}
}
And this time it asked me to authorise athlete2 which is great news! After restarting though i now get TWO charts of athlete 1. (athlete1 comes first in the config.js). I then isolated athlete2 by commenting out athlete1's module and I was able to get athlete2's chart.
But I can't get it to display athlete 1 and athlete 2's chart simultaneously. Any tips or is this not supported?
Thanks for your hard work! Much Appreciated.
I believe the "client_id" and "client_secret" are for the API access, not for individual athletes. That said, I am having trouble as well, it is not saving separate tokens for each login - it is just saving a single token and is acting the same as in what you have showed.
I figured out how to fix it, but I am not confident enough with git to submit the fixes: in node_helper.js change saveToken function to write the moduleIdentifier instead of clientId. This way it stores a different athlete token for each module and not for each client ("client" means app for the API calls).
saveToken: function (moduleIdentifier, token, cb) { var self = this; this.readTokens(); // No token for moduleIdentifier- delete existing if (moduleIdentifierin this.tokens && !token) { delete this.tokens[moduleIdentifier]; } // No moduleIdentifierin tokens - create stub if (!(moduleIdentifierin this.tokens) && token) { this.tokens[moduleIdentifier] = {}; } // Add token for client if (token) { this.tokens[moduleIdentifier].token = token; } // Save tokens to file var json = JSON.stringify(this.tokens, null, 2); fs.writeFile(this.tokensFile, json, "utf8", function (error) { if (error && cb) { cb(error); } if (cb) { cb(null, self.tokens); } }); },
and change saveToken calls, refreshTokens, etc. to reflect this change.
Fantastic!
I figured out how to fix it, but I am not confident enough with git to submit the fixes: in node_helper.js change saveToken function to write the moduleIdentifier instead of clientId. This way it stores a different athlete token for each module and not for each client ("client" means app for the API calls).
saveToken: function (moduleIdentifier, token, cb) { var self = this; this.readTokens(); // No token for moduleIdentifier- delete existing if (moduleIdentifierin this.tokens && !token) { delete this.tokens[moduleIdentifier]; } // No moduleIdentifierin tokens - create stub if (!(moduleIdentifierin this.tokens) && token) { this.tokens[moduleIdentifier] = {}; } // Add token for client if (token) { this.tokens[moduleIdentifier].token = token; } // Save tokens to file var json = JSON.stringify(this.tokens, null, 2); fs.writeFile(this.tokensFile, json, "utf8", function (error) { if (error && cb) { cb(error); } if (cb) { cb(null, self.tokens); } }); },
and change saveToken calls, refreshTokens, etc. to reflect this change.
PLease can you help me where and instead of what I should add your lines.
Note, this may break other functionality. I just hacked it together to get it working as I wanted. This does double the number of Strava requests so every now and then the server rate-limits me so you may wish to slow down the update frequency.
node_helper.js:
``/**
* @file node_helper.js
*
* @author ianperrin
* @license MIT
*
* @see http://github.com/ianperrin/MMM-Strava
*/
/**
* @external node_helper
* @see https://github.com/MichMich/MagicMirror/blob/master/modules/node_modules/node_helper/index.js
*/
const NodeHelper = require("node_helper");
/**
* @external moment
* @see https://www.npmjs.com/package/moment
*/
const moment = require("moment");
/**
* @external strava-v3
* @see https://www.npmjs.com/package/strava-v3
*/
const strava = require("strava-v3");
/**
* @alias fs
* @see {@link http://nodejs.org/api/fs.html File System}
*/
const fs = require("fs");
/**
* @module node_helper
* @description Backend for the module to query data from the API provider.
*
* @requires external:node_helper
* @requires external:moment
* @requires external:strava-v3
* @requires alias:fs
*/
module.exports = NodeHelper.create({
/**
* @function start
* @description Logs a start message to the console.
* @override
*/
start: function () {
console.log("Starting module helper: " + this.name);
this.createRoutes();
},
// Set the minimum MagicMirror module version for this module.
requiresVersion: "2.2.0",
// Config store e.g. this.configs["identifier"])
configs: Object.create(null),
// Tokens file path
tokensFile: `${__dirname}/tokens.json`,
// Token store e.g. this.tokens["client_id"])
tokens: Object.create(null),
/**
* @function socketNotificationReceived
* @description receives socket notifications from the module.
* @override
*
* @param {string} notification - Notification name
* @param {Object.<string, Object>} payload - Detailed payload of the notification (key: module identifier, value: config object).
*/
socketNotificationReceived: function (notification, payload) {
var self = this;
this.log("Received notification: " + notification);
if (notification === "SET_CONFIG") {
// debug?
if (payload.config.debug) {
this.debug = true;
}
// Validate module config
if (payload.config.access_token || payload.config.strava_id) {
this.log(`Legacy config in use for ${payload.identifier}`);
this.sendSocketNotification("WARNING", { "identifier": payload.identifier, "data": { message: "Strava authorisation has changed. Please update your config." } });
}
// Initialise and store module config
if (!(payload.identifier in this.configs)) {
this.configs[payload.identifier] = {};
}
this.configs[payload.identifier].config = payload.config;
// Check for token authorisations
this.readTokens();
if (payload.config.client_id && (!(payload.config.client_id in this.tokens))) {
this.log(`Unauthorised client id for ${payload.identifier}`);
this.sendSocketNotification("ERROR", { "identifier": payload.identifier, "data": { message: `Client id unauthorised - please visit <a href="/${self.name}/auth/">/${self.name}/auth/</a>` } });
}
// Schedule API calls
this.getData(payload.identifier);
setInterval(function () {
self.getData(payload.identifier);
}, payload.config.reloadInterval);
}
},
/**
* @function createRoutes
* @description Creates the routes for the authorisation flow.
*/
createRoutes: function () {
this.expressApp.get(`/${this.name}/auth/modules`, this.authModulesRoute.bind(this));
this.expressApp.get(`/${this.name}/auth/request`, this.authRequestRoute.bind(this));
this.expressApp.get(`/${this.name}/auth/exchange`, this.authExchangeRoute.bind(this));
},
/**
* @function authModulesRoute
* @description returns a list of module identifiers
*
* @param {object} req
* @param {object} res - The HTTP response that an Express app sends when it gets an HTTP request.
*/
authModulesRoute: function (req, res) {
try {
var identifiers = Object.keys(this.configs);
identifiers.sort();
var text = JSON.stringify(identifiers);
res.contentType("application/json");
res.send(text);
} catch (error) {
this.log(error);
res.redirect(`/${this.name}/auth/?error=${JSON.stringify(error)}`);
}
},
/**
* @function authRequestRoute
* @description redirects to the Strava Request Access Url
*
* @param {object} req
* @param {object} res - The HTTP response the Express app sends when it gets an HTTP request.
*/
authRequestRoute: function (req, res) {
try {
const moduleIdentifier = req.query.module_identifier;
const clientId = this.configs[moduleIdentifier].config.client_id;
const redirectUri = `http://${req.headers.host}/${this.name}/auth/exchange`;
this.log(`Requesting access for ${clientId}`);
// Set Strava config
strava.config({
"client_id": clientId,
"redirect_uri": redirectUri
});
const args = {
"client_id": clientId,
"redirect_uri": redirectUri,
"approval_prompt": "force",
"scope": "read,activity:read,activity:read_all",
"state": moduleIdentifier
};
const url = strava.oauth.getRequestAccessURL(args);
res.redirect(url);
} catch (error) {
this.log(error);
res.redirect(`/${this.name}/auth/?error=${JSON.stringify(error)}`);
}
},
/**
* @function authExchangeRoute
* @description exchanges code obtained from the access request and stores the access token
*
* @param {object} req
* @param {object} res - The HTTP response that an Express app sends when it gets an HTTP request.
*/
authExchangeRoute: function (req, res) {
try {
const authCode = req.query.code;
const moduleIdentifier = req.query.state;
const clientId = this.configs[moduleIdentifier].config.client_id;
const clientSecret = this.configs[moduleIdentifier].config.client_secret;
this.log(`Getting token for ${clientId}`);
strava.config({
"client_id": clientId,
"client_secret": clientSecret
});
var self = this;
strava.oauth.getToken(authCode, function (err, payload, limits) {
if (err) {
console.error(err);
res.redirect(`/${self.name}/auth/?error=${err}`);
return;
}
// Store tokens
self.saveToken(moduleIdentifier, payload.body, (err, data) => {
// redirect route
res.redirect(`/${self.name}/auth/?status=success`);
});
});
} catch (error) {
this.log(error);
res.redirect(`/${this.name}/auth/?error=${JSON.stringify(error)}`);
}
},
/**
* @function refreshTokens
* @description refresh the authenitcation tokens from the API and store
*
* @param {string} moduleIdentifier - The module identifier.
*/
refreshTokens: function (moduleIdentifier) {
this.log(`Refreshing tokens for ${moduleIdentifier}`);
var self = this;
const clientId = this.configs[moduleIdentifier].config.client_id;
const clientSecret = this.configs[moduleIdentifier].config.client_secret;
const token = this.tokens[moduleIdentifier].token;
this.log(`Refreshing token for ${moduleIdentifier}`);
strava.config({
"client_id": clientId,
"client_secret": clientSecret
});
try {
strava.oauth.refreshToken(token.refresh_token).then(result => {
token.token_type = result.token_type || token.token_type;
token.access_token = result.access_token || token.access_token;
token.refresh_token = result.refresh_token || token.refresh_token;
token.expires_at = result.expires_at || token.expires_at;
// Store tokens
self.saveToken(moduleIdentifier, token, (err, data) => {
if (!err) {
self.getData(moduleIdentifier);
}
});
});
} catch (error) {
this.log(`Failed to refresh tokens for ${moduleIdentifier}. Check config or module authorisation.`);
}
},
/**
* @function getData
* @description gets data from the Strava API based on module mode
*
* @param {string} moduleIdentifier - The module identifier.
*/
getData: function (moduleIdentifier) {
this.log(`Getting data for ${moduleIdentifier}`);
const moduleConfig = this.configs[moduleIdentifier].config;
try {
// Get access token
const accessToken = this.tokens[moduleIdentifier].token.access_token;
if (moduleConfig.mode === "table") {
try {
// Get athelete Id
const athleteId = this.tokens[moduleIdentifier].token.athlete.id;
// Call api
this.getAthleteStats(moduleIdentifier, accessToken, athleteId);
} catch (error) {
this.log(`Athete id not found for ${moduleIdentifier}`);
}
} else if (moduleConfig.mode === "chart") {
// Get initial date
moment.locale(moduleConfig.locale);
var after = moment().subtract(1,moduleConfig.period === "ytd" ? "years" : "weeks")
.add(1,"days").unix(); // Call api
this.getAthleteActivities(moduleIdentifier, accessToken, after);
}
} catch (error) {
console.log(error);
this.log(`Access token not found for ${moduleIdentifier}`);
}
},
/**
* @function getAthleteStats
* @description get stats for an athlete from the API
*
* @param {string} moduleIdentifier - The module identifier.
* @param {string} accessToken
* @param {integer} athleteId
*/
getAthleteStats: function (moduleIdentifier, accessToken, athleteId) {
this.log("Getting athlete stats for " + moduleIdentifier + " using " + athleteId);
var self = this;
strava.athletes.stats({ "access_token": accessToken, "id": athleteId }, function (err, payload, limits) {
var data = self.handleApiResponse(moduleIdentifier, err, payload, limits);
if (data) {
self.sendSocketNotification("DATA", { "identifier": moduleIdentifier, "data": data });
}
});
},
/**
* @function getAthleteActivities
* @description get logged in athletes activities from the API
*
* @param {string} moduleIdentifier - The module identifier.
* @param {string} accessToken
* @param {string} after
*/
getAthleteActivities: function (moduleIdentifier, accessToken, after) {
this.log("Getting athlete activities for " + moduleIdentifier + " after " + moment.unix(after).format("YYYY-MM-DD"));
var self = this;
strava.athlete.listActivities({ "access_token": accessToken, "after": after, "per_page": 200 }, function (err, payload, limits) {
var activityList = self.handleApiResponse(moduleIdentifier, err, payload, limits);
if (activityList) {
var data = {
"identifier": moduleIdentifier,
"data": self.summariseActivities(moduleIdentifier, activityList)
};
self.sendSocketNotification("DATA", data);
}
});
},
/**
* @function handleApiResponse
* @description handles the response from the API to catch errors and faults.
*
* @param {string} moduleIdentifier - The module identifier.
* @param {Object} err
* @param {Object} payload
* @param {Object} limits
*/
handleApiResponse: function (moduleIdentifier, err, payload, limits) {
try {
// Strava-v3 errors
if (err) {
if (err.error && err.error.errors[0].field === "access_token" && err.error.errors[0].code === "invalid") {
this.refreshTokens(moduleIdentifier);
} else {
this.log({ module: moduleIdentifier, error: err });
this.sendSocketNotification("ERROR", { "identifier": moduleIdentifier, "data": { "message": err.message } });
}
}
// Strava Data
if (payload) {
return payload;
}
} catch (error) {
// Unknown response
this.log(`Unable to handle API response for ${moduleIdentifier}`);
}
return false;
},
/**
* @function summariseActivities
* @description summarises a list of activities for display in the chart.
*
* @param {string} moduleIdentifier - The module identifier.
*/
summariseActivities: function (moduleIdentifier, activityList) {
this.log("Summarising athlete activities for " + moduleIdentifier);
var moduleConfig = this.configs[moduleIdentifier].config;
var activitySummary = Object.create(null);
var activityName;
// Initialise activity summary
var periodIntervals = moduleConfig.period === "ytd" ? moment.monthsShort() : moment.weekdaysShort();
for (var activity in moduleConfig.activities) {
if (Object.prototype.hasOwnProperty.call(moduleConfig.activities, activity)) {
activityName = moduleConfig.activities[activity].toLowerCase();
activitySummary[activityName] = {
total_distance: 0,
total_elevation_gain: 0,
total_moving_time: 0,
max_interval_distance: 0,
intervals: Array(periodIntervals.length).fill(0)
};
}
}
// Summarise activity totals and interval totals
for (var i = 0; i < Object.keys(activityList).length; i++) {
// Merge virtual activities
activityName = activityList[i].type.toLowerCase().replace("virtual", "");
var activityTypeSummary = activitySummary[activityName];
// Update activity summaries
if (activityTypeSummary) {
var distance = activityList[i].distance;
activityTypeSummary.total_distance += distance;
activityTypeSummary.total_elevation_gain += activityList[i].total_elevation_gain;
activityTypeSummary.total_moving_time += activityList[i].moving_time;
const activityDate = moment(activityList[i].start_date_local);
const intervalIndex = 6-moment().startOf(moduleConfig.period==="ytd"?"year":"day").diff(
activityDate.startOf(moduleConfig.period==="ytd"?"year":"day"),moduleConfig.period === "ytd" ? "months":"days");
activityTypeSummary.intervals[intervalIndex] += distance;
// Update max interval distance
if (activityTypeSummary.intervals[intervalIndex] > activityTypeSummary.max_interval_distance) {
activityTypeSummary.max_interval_distance = activityTypeSummary.intervals[intervalIndex];
}
}
}
return activitySummary;
},
/**
* @function saveToken
* @description save token for specified moduleIdentifier to file
*
* @param {integer} moduleIdentifier - The module's identifier.
* @param {object} token - The token response.
*/
saveToken: function (moduleIdentifier, token, cb) {
var self = this;
this.readTokens();
// No token for moduleIdentifier - delete existing
if (moduleIdentifier in this.tokens && !token) {
delete this.tokens[moduleIdentifier];
}
// No moduleIdentifier in tokens - create stub
if (!(moduleIdentifier in this.tokens) && token) {
this.tokens[moduleIdentifier] = {};
}
// Add token for client
if (token) {
this.tokens[moduleIdentifier].token = token;
}
// Save tokens to file
var json = JSON.stringify(this.tokens, null, 2);
fs.writeFile(this.tokensFile, json, "utf8", function (error) {
if (error && cb) { cb(error); }
if (cb) { cb(null, self.tokens); }
});
},
/**
* @function readTokens
* @description reads the current tokens file
*/
readTokens: function () {
if (this.tokensFile) {
try {
const tokensData = fs.readFileSync(this.tokensFile, "utf8");
this.tokens = JSON.parse(tokensData);
} catch (error) {
this.tokens = {};
}
return this.tokens;
}
},
/**
* @function log
* @description logs the message, prefixed by the Module name, if debug is enabled.
* @param {string} msg the message to be logged
*/
log: function (msg) {
if (this.debug) {
console.log(this.name + ":", JSON.stringify(msg));
}
}
});
MMM-Strava.js:
`/**
* @file MMM-Strava.js
*
* @author ianperrin
* @license MIT
*
* @see https://github.com/ianperrin/MMM-Strava
*/
/* global Module, config, Log, moment */
/**
* @external Module
* @see https://github.com/MichMich/MagicMirror/blob/master/js/module.js
*/
/**
* @external config
* @see https://github.com/MichMich/MagicMirror/blob/master/config/config.js.sample
*/
/**
* @external Log
* @see https://github.com/MichMich/MagicMirror/blob/master/js/logger.js
*/
/**
* @external moment
* @see https://www.npmjs.com/package/moment
*/
/**
* @module MMM-Strava
* @description Frontend of the MagicMirror² module.
*
* @requires external:Module
* @requires external:config
* @requires external:Log
* @requires external:moment
*/
Module.register("MMM-Strava", {
// Set the minimum MagicMirror module version for this module.
requiresVersion: "2.2.0",
// Default module config.
defaults: {
client_id: "",
client_secret: "",
mode: "table", // Possible values "table", "chart"
chartType: "bar", // Possible values "bar", "radial"
activities: ["ride", "run", "swim"], // Possible values "ride", "run", "swim"
period: "recent", // Possible values "recent", "ytd", "all"
stats: ["count", "distance", "achievements"], // Possible values "count", "distance", "elevation", "moving_time", "elapsed_time", "achievements"
auto_rotate: false, // Rotate stats through each period starting from specified period
locale: config.language,
units: config.units,
reloadInterval: 5 * 60 * 1000, // every 5 minutes
updateInterval: 10 * 1000, // 10 seconds
animationSpeed: 2.5 * 1000, // 2.5 seconds
debug: false, // Set to true to enable extending logging
},
/**
* @member {boolean} loading - Flag to indicate the loading state of the module.
*/
loading: true,
/**
* @member {boolean} rotating - Flag to indicate the rotating state of the module.
*/
rotating: false,
/**
* @function getStyles
* @description Style dependencies for this module.
* @override
*
* @returns {string[]} List of the style dependency filepaths.
*/
getStyles: function() {
return ["font-awesome.css", "MMM-Strava.css"];
},
/**
* @function getScripts
* @description Script dependencies for this module.
* @override
*
* @returns {string[]} List of the script dependency filepaths.
*/
getScripts: function() {
return ["moment.js"];
},
/**
* @function getTranslations
* @description Translations for this module.
* @override
*
* @returns {Object.<string, string>} Available translations for this module (key: language code, value: filepath).
*/
getTranslations: function() {
return {
en: "translations/en.json",
nl: "translations/nl.json",
de: "translations/de.json",
id: "translations/id.json",
hu: "translations/hu.json",
gr: "translations/gr.json"
};
},
/**
* @function start
* @description Validates config values, adds nunjuck filters and initialises requests for data.
* @override
*/
start: function() {
Log.info("Starting module: " + this.name);
// Validate config
this.config.mode = this.config.mode.toLowerCase();
this.config.period = this.config.period.toLowerCase();
this.config.chartType = this.config.chartType.toLowerCase();
// Add custom filters
this.addFilters();
// Initialise helper and schedule api calls
this.sendSocketNotification("SET_CONFIG", {"identifier": this.identifier, "config": this.config});
this.scheduleUpdates();
},
/**
* @function socketNotificationReceived
* @description Handles incoming messages from node_helper.
* @override
*
* @param {string} notification - Notification name
* @param {Object,<string,*} payload - Detailed payload of the notification.
*/
socketNotificationReceived: function(notification, payload) {
this.log(`Receiving notification: ${notification} for ${payload.identifier}`);
if (payload.identifier === this.identifier) {
if (notification === "DATA") {
this.stravaData = payload.data;
this.loading = false;
this.updateDom(this.config.animationSpeed);
} else if (notification === "ERROR") {
this.loading = false;
this.error = payload.data.message;
this.updateDom(this.config.animationSpeed);
} else if (notification === "WARNING") {
this.loading = false;
this.sendNotification("SHOW_ALERT", {type: "notification", title: payload.data.message});
}
}
},
/**
* @function getTemplate
* @description Nunjuck template.
* @override
*
* @returns {string} Path to nunjuck template.
*/
getTemplate: function() {
return "templates\\MMM-Strava." + this.config.mode + ".njk";
},
/**
* @function getTemplateData
* @description Data that gets rendered in the nunjuck template.
* @override
*
* @returns {string} Data for the nunjuck template.
*/
getTemplateData: function() {
const dayList = Array.from(Array(7),(e,i)=>
moment().startOf('day').subtract(i,"days").format('dd'))
moment.locale(this.config.locale);
return {
config: this.config,
loading: this.loading,
error: this.error || null,
data: this.stravaData || {},
chart: {bars: dayList },
};
},
/**
* @function scheduleUpdates
* @description Schedules table rotation
*/
scheduleUpdates: function() {
var self = this;
// Schedule table rotation
if (!this.rotating && this.config.mode === "table") {
this.rotating = true;
if (this.config.auto_rotate && this.config.updateInterval) {
setInterval(function() {
// Get next period
self.config.period = ((self.config.period === "recent") ? "ytd" : ((self.config.period === "ytd") ? "all" : "recent"));
self.updateDom(self.config.animationSpeed);
}, this.config.updateInterval);
}
}
},
/**
* @function log
* @description logs the message, prefixed by the Module name, if debug is enabled.
* @param {string} msg the message to be logged
*/
log: function(msg) {
if (this.config && this.config.debug) {
Log.info(`${this.name}: ` + JSON.stringify(msg));
}
},
/**
* @function addFilters
* @description adds filters to the Nunjucks environment.
*/
addFilters() {
var env = this.nunjucksEnvironment();
env.addFilter("getIntervalClass", this.getIntervalClass.bind(this));
env.addFilter("getLabel", this.getLabel.bind(this));
env.addFilter("formatTime", this.formatTime.bind(this));
env.addFilter("formatDistance", this.formatDistance.bind(this));
env.addFilter("formatElevation", this.formatElevation.bind(this));
env.addFilter("roundValue", this.roundValue.bind(this));
env.addFilter("getRadialLabelTransform", this.getRadialLabelTransform.bind(this));
env.addFilter("getRadialDataPath", this.getRadialDataPath.bind(this));
},
getIntervalClass: function(interval)
{
moment.locale(this.config.locale);
var currentInterval = 6;//this.config.period === "ytd" ? moment().month() : moment().weekday();
var className = "future";
if (currentInterval === interval) {
className = "current";
} else if (currentInterval < interval) {
className = "past";
}
return className;
},
getLabel: function(interval) {
moment.locale(this.config.locale);
return moment().startOf("day").subtract(6-interval,"days").format("dd").slice(0,1).toUpperCase();
const startUnit = this.config.period === "ytd" ? "year" : "week";
const intervalUnit = this.config.period === "ytd" ? "months" : "days";
const labelUnit = this.config.period === "ytd" ? "MMM" : "dd";
var intervalDate = moment().startOf(startUnit).add(interval, intervalUnit);
return intervalDate.format(labelUnit).slice(0,1).toUpperCase();
},
formatTime: function(timeInSeconds) {
var duration = moment.duration(timeInSeconds, "seconds");
return Math.floor(duration.asHours()) + "h " + duration.minutes() + "m";
},
// formatDistance
formatDistance: function(value, digits, showUnits) {
const distanceMultiplier = this.config.units === "imperial" ? 0.0006213712 : 0.001;
const distanceUnits = this.config.units === "imperial" ? " mi" : " km";
return this.formatNumber(value, distanceMultiplier, digits, (showUnits ? distanceUnits : null));
},
// formatElevation
formatElevation: function(value, digits, showUnits) {
const elevationMultiplier = this.config.units === "imperial" ? 3.28084 : 1;
const elevationUnits = this.config.units === "imperial" ? " ft" : " m";
return this.formatNumber(value, elevationMultiplier, digits, (showUnits ? elevationUnits : null));
},
// formatNumber
formatNumber: function(value, multipler, digits, units) {
// Convert value
value = value * multipler;
// Round value
value = this.roundValue(value, digits);
// Append units
if (units) {
value += units;
}
return value;
},
// getRadialLabelTransform
getRadialLabelTransform(index, count) {
const degrees = (360/count/2) + (index * (360/count));
const rotation = ((index < count/2) ? -90 : 90) + degrees;
const labelRadius = 96;
const translation = this.polarToCartesian(0, 0, labelRadius, degrees);
return `translate(${translation.x}, ${translation.y}) rotate(${rotation})`;
},
// getRadialDataPath
getRadialDataPath(index, count, value) {
const gap = 5;
const startAngle = (gap / 2) + (index * (360 / count));
const endAngle = startAngle + ((360 - (count * gap)) / count);
const radius = { inner: 109, outer: 109 + (value * 100) };
if (value > 0) {
// identify points
var p1 = this.polarToCartesian(0, 0, radius.inner, startAngle);
var p2 = this.polarToCartesian(0, 0, radius.outer - 10, startAngle);
var p3 = this.polarToCartesian(0, 0, radius.outer, startAngle + 5/2);
var p4 = this.polarToCartesian(0, 0, radius.outer, endAngle - 5/2);
var p5 = this.polarToCartesian(0, 0, radius.outer - 10 , endAngle);
var p6 = this.polarToCartesian(0, 0, radius.inner, endAngle);
// describe path
var d = [
"M", p1.x, p1.y,
"L", p2.x, p2.y,
"A", 10, 10, 0, 0, 1, p3.x, p3.y,
"A", radius.outer, radius.outer, 0, 0, 1, p4.x, p4.y,
"A", 10, 10, 0, 0, 1, p5.x, p5.y,
"L", p6.x, p6.y,
"A", radius.inner, radius.inner, 0, 0, 0, p1.x, p1.y,
"L", p1.x, p1.y
].join(" ");
return d;
} else {
return "";
}
},
/**
* @function polarToCartesian
* @description Calculates the coordinates of a point on the circumference of a circle.
* @param {integer} centerX x
* @param {integer} centerY y
* @param {integer} radius radius of the circle
* @param {integer} angleInDegrees angle to the new point in degrees
* @see https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
*/
polarToCartesian: function(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
},
/**
* @function roundValue
* @description rounds the value to number of digits.
* @param {decimal} value the value to be rounded
* @param {integer} digits the number of digits to round the value to
*/
roundValue: function(value, digits) {
var rounder = Math.pow(10, digits);
return (Math.round(value * rounder) / rounder).toFixed(digits);
},
});
`
In node_helper.js also change if (payload.config.client_id && (!(payload.config.client_id in this.tokens))) {
to if (payload.identifier && (!(payload.identifier in this.tokens))) {
Hi all
Sorry for the delayed response - this pandemic lockdown hasn't been so quiet for me.
Without any changes to the module, I am able to display information for multiple athletes.
Here's my config
{
module: "MMM-Strava",
header: 'Strava: Account 1',
position: "top_right",
config: {
client_id: "00000",
client_secret: "xxxxx",
activities: ["run", "ride"],
}
},
{
module: "MMM-Strava",
header: 'Strava: Account 2',
position: "top_right",
config: {
client_id: "11111",
client_secret: "zzzzz",
activities: ["ride"],
}
},
Starting the mirror, I then complete the authorisation process for each athlete (making sure I select the right module identifier each time athlete being authorised), then restart the mirror.

If this is not working for you, can you check that a file called tokens.json
is created in the module folder ~/magicmirror/modules/MMM-Strava
and that it contains tokens for both client ids?
PLEASE DON'T SHARE THE TOKENS FILE HERE!
It looks like the problem arises when you try to use the same API key for both modules as that is how your implementation keeps track of access tokens (by API key and not by module id). Having separate API keys does provide extra headroom for rate limiting but I prefer to only have a single key.
So anyone that wishes to do this in the future make sure you have separate API keys for each user.
Just a warning - using my hack above - if you move modules around then you will have to change the respective IDs in tokens.json as needed (MM assigns IDs based on the config order).
Hi @scottdrichards is it possible to show on this module my activity on Strava and also of the people I am following? maybe one or two followers? thanks
No idea, sorry!
Responded to by @ianperrin in https://github.com/ianperrin/MMM-Strava/issues/45#issuecomment-662119625