diff --git a/JastUtils.js b/JastUtils.js new file mode 100644 index 0000000..a80e9cc --- /dev/null +++ b/JastUtils.js @@ -0,0 +1,82 @@ +class JastUtils { + static getCurrentValue(stock, exchangeData) { + let currentValue = "-"; + if (stock.current) { + currentValue = stock.current; + if ( + exchangeData && + stock.tradeCurrency && + stock.displayCurrency && + stock.tradeCurrency !== stock.displayCurrency + ) { + const exchange = exchangeData.find( + (exchange) => + exchange.from === stock.tradeCurrency && + exchange.to === stock.displayCurrency + ); + if (exchange) { + currentValue = currentValue * exchange.rate; + } + } + currentValue = currentValue.toFixed(2); + } + return currentValue; + } + + static getCurrency(stock, exchangeData, config) { + let currency = config.defaultCurrency; + if (stock.displayCurrency) { + const exchange = exchangeData.find( + (exchange) => + exchange.from === stock.tradeCurrency && + exchange.to === stock.displayCurrency + ); + if (exchange) { + currency = stock.displayCurrency; + } else if (stock.tradeCurrency) { + currency = stock.tradeCurrency; + } + } + return currency; + } + + static getStockChange(stock) { + if (stock.current && stock.last) { + return (((stock.current - stock.last) / stock.last) * 100).toFixed(1); + } else { + return 0; + } + } + + static getDepotGrowth(config, exchangeData) { + let growth = 0; + let errors = false; + config.stocks.forEach((stock) => { + if (stock.current && stock.last) { + let change = + stock.current * stock.quantity - stock.last * stock.quantity; + + if ( + stock.tradeCurrency && + stock.tradeCurrency !== config.defaultCurrency + ) { + const exchange = exchangeData.find( + (exchange) => + exchange.from === stock.tradeCurrency && + exchange.to === stock.displayCurrency + ); + if (exchange) { + change = change * exchange.rate; + } else { + errors = true; + } + } else { + errors = true; + } + growth = growth + change; + } + }); + + return { value: growth.toFixed(2), errors }; + } +} diff --git a/MMM-Jast.css b/MMM-Jast.css index d925f13..b2aa27e 100644 --- a/MMM-Jast.css +++ b/MMM-Jast.css @@ -1,53 +1,56 @@ @keyframes tickerh { 0% { - transform: translate3d(0, 0, 0); + transform: translate(0, 0); } 100% { - transform: translate3d(-100%, 0, 0); + transform: translate(-100%, 0); } } -.ticker-wrap { +.jast-wrapper { overflow: hidden; } -.ticker-wrap.vertical { - height: 26px; -} -.ticker-wrap.horizontal { - padding-left: 100%; +.jast-wrapper.horizontal { + margin: 0 auto; + white-space: nowrap; } -.ticker-wrap ul { +.jast-wrapper ul { padding: 0; margin: 0; list-style-type: none; +} +.jast-wrapper.vertical { + height: 26px; +} +.jast-wrapper.vertical > ul { + animation-name: tickerv; animation-iteration-count: infinite; animation-timing-function: cubic-bezier(1, 0, 0.5, 0); } -.ticker-wrap.vertical ul { - animation-name: tickerv; +.jast-wrapper.horizontal .jast-hticker { + margin: 0 auto; + white-space: nowrap; + overflow: hidden; + position: absolute; } -.ticker-wrap.horizontal ul { - padding-right: 100%; + +.jast-wrapper.horizontal span.jast-tickerframe { display: inline-block; - white-space: nowrap; - animation-iteration-count: infinite; - animation-timing-function: linear; - animation-name: tickerh; + padding-left: 100%; } -.ticker-wrap.horizontal ul li:before { + +.jast-wrapper.horizontal span.jast-stock:before { content: "\2022"; margin-right: 0.4em; + margin-left: 0.4em; } -.ticker-wrap ul li { +.jast-wrapper .jast-stock { font-size: 18px; line-height: 26px; } -.ticker-wrap.horizontal ul li { - display: inline-block; - padding: 0 0.3rem; -} -.ticker-wrap ul li span.high { + +.jast-wrapper .jast-stock span.high { color: green; } -.ticker-wrap ul li span.low { +.jast-wrapper .jast-stock span.low { color: red; } diff --git a/MMM-Jast.js b/MMM-Jast.js index 6ec45a8..4a01aef 100644 --- a/MMM-Jast.js +++ b/MMM-Jast.js @@ -1,91 +1,72 @@ "use strict"; Module.register("MMM-Jast", { - result: {}, defaults: { debug: false, + header: null, updateIntervalInSeconds: 1800, requestIntervalInSeconds: 62, - fadeSpeedInSeconds: 3.5, + fadeSpeedInSeconds: 3.5, // Higher value: vertical -> faster // horizontal -> slower stocks: [ { name: "BASF", symbol: "BAS.DE" }, { name: "SAP", symbol: "SAP.DE" }, { name: "Henkel", symbol: "HEN3.DE" }, { name: "AbbVie", symbol: "4AB.DE" }, - { name: "Alibaba", symbol: "BABA", tradeCurrency: "USD", displayCurrency: "EUR", quantity: 10 }, + { + name: "Alibaba", + symbol: "BABA", + tradeCurrency: "USD", + displayCurrency: "EUR", + quantity: 10 + } ], defaultCurrency: "EUR", baseURL: "https://www.alphavantage.co/", apiKey: "", scroll: "vertical", - maxWidth: "300px", - showDepotGrowth: false, + //maxWidth: "300px", + showDepotGrowth: false }, - getStyles: function () { + getStyles() { return ["MMM-Jast.css"]; }, - getTranslations: function () { + getScripts() { + return [this.file("JastUtils.js")]; + }, + + getTranslations() { return { en: "translations/en.json", - de: "translations/de.json", + de: "translations/de.json" + }; + }, + + getTemplate() { + return "templates/MMM-Jast.njk"; + }, + + getTemplateData() { + return { + config: this.config, + exchangeData: this.exchangeData, + utils: JastUtils }; }, - start: function () { - this.exchangeData = {}; + getHeader() { + return this.config.header; + }, + + start() { + this.exchangeData = []; this.getExchangeRate(); this.getStocks(); this.scheduleUpdate(); }, - getDom: function () { - this.setVerticalScrollingKeyframes(); - let app = document.createElement("div"); - let depotChange = 0; - let entryCount = this.config.showDepotGrowth ? this.config.stocks.length + 1 : this.config.stocks.length; - let ticker = `
`; - ticker += ``; - ticker += `
`; - app.innerHTML = ticker; - return app; - }, - - scheduleUpdate: function () { + scheduleUpdate() { const self = this; setInterval(function () { self.getExchangeRate(); @@ -93,38 +74,33 @@ Module.register("MMM-Jast", { }, this.config.requestIntervalInSeconds * 1000); }, - getStocks: function () { + getStocks() { this.sendSocketNotification("GET_STOCKS", this.config); }, - getExchangeRate: function () { - this.sendSocketNotification("GET_EXCHANGE", { config: this.config, rates: this.exchangeData }); - }, - - getColorClass: function (depotChange) { - if (depotChange > 0) { - return "high"; - } else if (depotChange < 0) { - return "low"; - } else { - return "neutral"; - } + getExchangeRate() { + this.sendSocketNotification("GET_EXCHANGE", { + config: this.config, + rates: this.exchangeData + }); }, - socketNotificationReceived: function (notification, payload) { + socketNotificationReceived(notification, payload) { if (notification === "STOCK_RESULT") { - let { symbol, current, last } = payload; - let stockIndex = this.config.stocks.findIndex((stock) => stock.symbol === symbol); - if (stockIndex >= 0) { - this.config.stocks[stockIndex].current = current; - this.config.stocks[stockIndex].last = last; - this.config.stocks[stockIndex].lastUpdate = Date.now(); + const { symbol, current, last } = payload; + const currentStock = this.config.stocks.find( + (stock) => stock.symbol === symbol + ); + if (currentStock) { + currentStock.current = current; + currentStock.last = last; + currentStock.lastUpdate = Date.now(); this.updateDom(); } } else if (notification === "EXCHANGE_RESULT") { let { from, to, rate } = payload; - this.exchangeData[from + to] = { from, to, rate }; - this.exchangeData[from + to].lastUpdate = Date.now(); + this.exchangeData.push({ from, to, rate, lastUpdate: Date.now() }); + this.updateDom(); } }, @@ -142,12 +118,15 @@ Module.register("MMM-Jast", { document.head.appendChild(vkf); } let innerText = `@keyframes tickerv {`; - const itemCount = this.config.stocks.length > 0 ? this.config.stocks.length + offset : 1; // avoid divition by zero + const itemCount = + this.config.stocks.length > 0 ? this.config.stocks.length + offset : 1; // avoid divition by zero const percentPerItem = 100 / itemCount; for (let i = 0; i <= itemCount; i++) { - innerText += ` ${i * percentPerItem}% { margin-top: ${i == 0 || i == itemCount ? "0" : i * -26 + "px"}; }`; + innerText += ` ${i * percentPerItem}% { margin-top: ${ + i === 0 || i === itemCount ? "0" : i * -26 + "px" + }; }`; } innerText += `}`; vkf.innerText = innerText; - }, + } }); diff --git a/node_helper.js b/node_helper.js index 5a3e16a..528e99a 100644 --- a/node_helper.js +++ b/node_helper.js @@ -1,30 +1,44 @@ -var NodeHelper = require("node_helper"); -var request = require("request"); +const NodeHelper = require("node_helper"); +const request = require("request"); module.exports = NodeHelper.create({ - start: function () { + start() { console.log(`${this.name} helper method started...`); }, - getRandomApiKey: function (config) { - const key = config.apiKeys[Math.floor(Math.random() * config.apiKeys.length)]; - console.log("Using key", key); - return key; - }, - - sendStocksRequest: function (config) { + sendStocksRequest(config) { const self = this; if (config.debug) { - self.sendSocketNotification("STOCK_RESULT", { symbol: "BAS.DE", current: 50.2, last: 44.9 }); - self.sendSocketNotification("STOCK_RESULT", { symbol: "SAP.DE", current: 100.2, last: 100.9 }); - self.sendSocketNotification("STOCK_RESULT", { symbol: "HEN3.DE", current: 66.2, last: 70.9 }); - self.sendSocketNotification("STOCK_RESULT", { symbol: "BABA", current: 180.2, last: 188.9 }); + self.sendSocketNotification("STOCK_RESULT", { + symbol: "BAS.DE", + current: 50.2, + last: 44.9 + }); + self.sendSocketNotification("STOCK_RESULT", { + symbol: "SAP.DE", + current: 100.2, + last: 100.9 + }); + self.sendSocketNotification("STOCK_RESULT", { + symbol: "HEN3.DE", + current: 66.2, + last: 70.9 + }); + self.sendSocketNotification("STOCK_RESULT", { + symbol: "BABA", + current: 180.2, + last: 188.9 + }); return; } + config.stocks.forEach((stock) => { - if (!stock.lastUpdate || Date.now() - stock.lastUpdate >= config.updateIntervalInSeconds * 1000) { + if ( + !stock.lastUpdate || + Date.now() - stock.lastUpdate >= config.updateIntervalInSeconds * 1000 + ) { const url = `${config.baseURL}query?function=TIME_SERIES_DAILY&outputsize=compact&apikey=${config.apiKey}&symbol=${stock.symbol}`; - request(url, { json: true }, (err, res, body) => { + request(url, { json: true }, (err, _res, body) => { if (err) { console.error(`Error requesting Stock data`); } @@ -35,7 +49,11 @@ module.exports = NodeHelper.create({ const last = parseFloat(values[1]["4. close"]); console.log("Sending Stock result:", { symbol, current, last }); - self.sendSocketNotification("STOCK_RESULT", { symbol, current, last }); + self.sendSocketNotification("STOCK_RESULT", { + symbol, + current, + last + }); } catch (err) { console.error(`Error processing Stock response`, body); } @@ -44,20 +62,34 @@ module.exports = NodeHelper.create({ }); }, - sendExchangeRequest: function (payload) { + sendExchangeRequest(payload) { const self = this; const { config, rates } = payload; if (config.debug) { - self.sendSocketNotification("EXCHANGE_RESULT", { from: "USD", to: "EUR", rate: 0.923 }); + self.sendSocketNotification("EXCHANGE_RESULT", { + from: "USD", + to: "EUR", + rate: 0.923 + }); return; } config.stocks.forEach((stock) => { - if (stock.tradeCurrency && stock.displayCurrency && stock.tradeCurrency != stock.displayCurrency) { - const currentChange = rates ? rates[stock.tradeCurrency + stock.displayCurrency] : null; + if ( + stock.tradeCurrency && + stock.displayCurrency && + stock.tradeCurrency !== stock.displayCurrency + ) { + const currentChange = rates.find( + (rate) => + rate.from === stock.tradeCurrency && + rate.to === stock.displayCurrency + ); + if ( !currentChange || !currentChange.lastUpdate || - Date.now() - currentChange.lastUpdate >= config.updateIntervalInSeconds * 1000 + Date.now() - currentChange.lastUpdate >= + config.updateIntervalInSeconds * 1000 ) { const url = `${config.baseURL}query?function=CURRENCY_EXCHANGE_RATE&from_currency=${stock.tradeCurrency}&to_currency=${stock.displayCurrency}&apikey=${config.apiKey}`; request(url, { json: true }, (err, res, body) => { @@ -65,12 +97,22 @@ module.exports = NodeHelper.create({ console.error(`Error requesting Exchange rate`); } try { - const from = body["Realtime Currency Exchange Rate"]["1. From_Currency Code"]; - const to = body["Realtime Currency Exchange Rate"]["3. To_Currency Code"]; - const rate = parseFloat(body["Realtime Currency Exchange Rate"]["5. Exchange Rate"]); + const from = + body["Realtime Currency Exchange Rate"][ + "1. From_Currency Code" + ]; + const to = + body["Realtime Currency Exchange Rate"]["3. To_Currency Code"]; + const rate = parseFloat( + body["Realtime Currency Exchange Rate"]["5. Exchange Rate"] + ); console.log("Sending Exchange result:", { from, to, rate }); - self.sendSocketNotification("EXCHANGE_RESULT", { from, to, rate }); + self.sendSocketNotification("EXCHANGE_RESULT", { + from, + to, + rate + }); } catch (err) { console.error(`Error processing Exchange response`, body); } @@ -80,7 +122,7 @@ module.exports = NodeHelper.create({ }); }, - socketNotificationReceived: function (notification, payload) { + socketNotificationReceived(notification, payload) { if (notification === "GET_STOCKS") { this.sendStocksRequest(payload); } else if (notification === "GET_EXCHANGE") { @@ -88,5 +130,5 @@ module.exports = NodeHelper.create({ } else { console.warn(`${notification} is invalid notification`); } - }, + } }); diff --git a/templates/HorizontalScrollStyle.njk b/templates/HorizontalScrollStyle.njk new file mode 100755 index 0000000..e2b12bb --- /dev/null +++ b/templates/HorizontalScrollStyle.njk @@ -0,0 +1,11 @@ +{% macro render(config) %} + +{% endmacro %} diff --git a/templates/HorizontalStockList.njk b/templates/HorizontalStockList.njk new file mode 100755 index 0000000..7f75282 --- /dev/null +++ b/templates/HorizontalStockList.njk @@ -0,0 +1,39 @@ + + +{% macro render(config, exchangeData, utils, num="1") %} +

+ + {% for stock in config.stocks %} + {{ stock.name }}: + {% if utils.getStockChange(stock) > 0 %} + {% set colorClass = "high" %} + {% elif utils.getStockChange(stock) < 0 %} + {% set colorClass = "low " %} + {% else %} + {% set colorClass = "" %} + {% endif %} + + {{ utils.getCurrentValue(stock, exchangeData) }} + {{ utils.getCurrency(stock, exchangeData, config) }} + {% if colorClass %} + ({{ utils.getStockChange(stock) }}%) + {% endif %} + + + {% endfor %} + {% if config.showDepotGrowth %} + {% set depotGrowth = utils.getDepotGrowth(config, exchangeData) %} + {% if depotGrowth.value > 0 %} + {% set colorClass = "high" %} + {% elif depotGrowth.value < 0 %} + {% set colorClass = "low " %} + {% else %} + {% set colorClass = "" %} + {% endif %} + {{ "depotGrowth" | translate | safe }} + {% if depotGrowth.errors%}≈ {% endif %}{{ depotGrowth.value }} {{ config.defaultCurrency }} + + {% endif %} + +

+{% endmacro %} diff --git a/templates/MMM-Jast.njk b/templates/MMM-Jast.njk new file mode 100755 index 0000000..c209c45 --- /dev/null +++ b/templates/MMM-Jast.njk @@ -0,0 +1,23 @@ +{% import "templates/VerticalStockList.njk" as verticalStockList %} +{% import "templates/HorizontalScrollStyle.njk" as hss %} +{% import "templates/HorizontalStockList.njk" as horizontalStockList %} + +{% if config.stocks %} +{% if config.scroll == "vertical" %} + +{% elif config.scroll == "horizontal" %} +{% endif %} +
+ {% if config.scroll == "horizontal" %} + {{ hss.render(config) }} + {{ horizontalStockList.render(config, exchangeData, utils) }} + {{ horizontalStockList.render(config, exchangeData, utils, "2") }} + {% else %} + {{ verticalStockList.render(config, exchangeData, utils) }} + {% endif %} +
+{% else %} +
+ {{ "LOADING" | translate | safe }} +
+{% endif %} diff --git a/templates/VerticalScrollStyle.njk b/templates/VerticalScrollStyle.njk new file mode 100755 index 0000000..b5b5efb --- /dev/null +++ b/templates/VerticalScrollStyle.njk @@ -0,0 +1,17 @@ +{% macro render(config) %} + {% if config.showDepotGrowth %} + {% set itemCount = config.stocks.length + 1 %} + {% else %} + {% set itemCount = config.stocks.length %} + {% endif %} + {% set percentPerItem = 100 / itemCount %} + +{% endmacro %} diff --git a/templates/VerticalStockList.njk b/templates/VerticalStockList.njk new file mode 100755 index 0000000..52439fe --- /dev/null +++ b/templates/VerticalStockList.njk @@ -0,0 +1,46 @@ +{% import "templates/VerticalScrollStyle.njk" as vss %} + +{% macro render(config, exchangeData, utils)%} + {% if config.scroll == "vertical" %} + {{ vss.render(config) }} + {% if config.showDepotGrowth %} + {% set fadeSpeed = config.stocks.length + 1 %} + {% else %} + {% set fadeSpeed = config.stocks.length %} + {% endif %} + {% set animationStyle = ' style="animation-duration: ' + fadeSpeed * config.fadeSpeedInSeconds + 's"' %} + {% endif %} + +{% endmacro %}