ianperrin / MMM-Strava

A MagicMirror Module for your Strava data.
https://forum.magicmirror.builders/topic/457/mmm-strava?_=1616840157932
MIT License
49 stars 15 forks source link

Displaying stats for multiple athletes using a single API key #42

Open jckringer opened 4 years ago

jckringer commented 4 years ago

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.

2020-05-07 14_07_22-Lacey's Mirror (raspberrypi) - VNC Viewer

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.

scottdrichards commented 4 years ago

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.

scottdrichards commented 4 years ago

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.

jckringer commented 4 years ago

Fantastic!

Raven4150 commented 4 years ago

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.

scottdrichards commented 4 years ago

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);
    },

});
`
scottdrichards commented 4 years ago

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))) {

ianperrin commented 4 years ago

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.

Screenshot 2020-07-01 at 21 02 59

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!

scottdrichards commented 4 years ago

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.

scottdrichards commented 4 years ago

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).

bachoo786 commented 4 years ago

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

scottdrichards commented 4 years ago

No idea, sorry!

ianperrin commented 4 years ago

Responded to by @ianperrin in https://github.com/ianperrin/MMM-Strava/issues/45#issuecomment-662119625