From 0dfa2586ce247b1b221b9c2c1f808e3c2f300af4 Mon Sep 17 00:00:00 2001 From: mtatsuma Date: Thu, 26 Nov 2020 23:00:12 +0900 Subject: [PATCH] First release of this module --- MMM-NatureRemo.js | 278 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 49 +++++++- 2 files changed, 326 insertions(+), 1 deletion(-) create mode 100755 MMM-NatureRemo.js diff --git a/MMM-NatureRemo.js b/MMM-NatureRemo.js new file mode 100755 index 0000000..0e2ac2e --- /dev/null +++ b/MMM-NatureRemo.js @@ -0,0 +1,278 @@ +/* Magic Mirror + * Module: MMM-NatureRemo + * + * By Tatsuma Matsuki + * MIT Licensed. + * Some code is borrowed from + * https://github.com/roramirez/MagicMirror-Module-Template + */ + +Module.register("MMM-NatureRemo", { + defaults: { + updateInterval: 10 * 60 * 1000, + retryDelay: 5000, + title: "Nature Remo", + apiBase: "https://api.nature.global/", + apiEndpoint: "1/devices", + apiToken: "", + deviceName: "", + deviceID: "", + height: "300px", + width: "500px", + showTemperature: true, + showHumidity: true, + showIllumination: true, + showMotion: true, + temperatureTitle: "Temperature: ", + humidityTitle: "Humidity: ", + illuminationTitle: "Illumination: ", + motionTitle: "Motion Detect: ", + temperatureUnit: "Celsius", + motionDateOffsetSeconds: 0, + motionDateHourFormat: "24h" + }, + + requiresVersion: "2.12.0", + + start: function () { + var self = this; + var dataRequest = null; + var dataNotification = null; + + //Flag for check if module is loaded + this.loaded = false; + + // Schedule update timer. + this.getData(); + setInterval(function () { + self.updateDom(); + }, this.config.updateInterval); + }, + + /* + * getData + * function example return data and show it in the module wrapper + * get a URL request + * + */ + getData: function () { + const self = this; + + if (this.config.apiToken === "") { + Log.error(self.name + ": apiToken must be specified"); + return; + } + + const url = `${this.config.apiBase}${this.config.apiEndpoint}`; + var retry = true; + let delay = self.config.retryDelay; + + fetch(url, { + headers: { "Authorization": `Bearer ${this.config.apiToken}` } + }) + .then((res) => { + if (res.status == 401) { + retry = false; + throw new Error(self.name + ": apiToken is invalid"); + } else if (res.status == 429) { + delay = 60 * 1000; + throw new Error(self.name + ": too many requests and got 429 status"); + } else if (!res.ok) { + throw new Error(self.name + ": failed to get api response"); + } + return res.json(); + }) + .then((json) => { + self.processData(json); + }) + .catch((msg) => { + Log.error(msg); + }) + .finally(() => { + if (retry) { + self.scheduleUpdate((self.loaded) ? -1 : delay); + } + }) + }, + + /* scheduleUpdate() + * Schedule next update. + * + * argument delay number - Milliseconds before next update. + * If empty, this.config.updateInterval is used. + */ + scheduleUpdate: function (delay) { + var nextLoad = this.config.updateInterval; + if (typeof delay !== "undefined" && delay >= 0) { + nextLoad = delay; + } + nextLoad = nextLoad; + var self = this; + setTimeout(function () { + self.getData(); + }, nextLoad); + }, + + getIllumiString: function (value) { + if (value <= 50) { + return `Dark (${value})`; + } else if (value > 50 && value <= 127) { + return `Dim (${value})`; + } else if (value > 127 && value <= 205) { + return `Medium (${value})`; + } else { + return `Light (${value})`; + } + }, + + getHourString: function (hour, minute) { + let m = ("0" + minute).slice(-2); + if (this.config.motionDateHourFormat == "12h") { + let ampm = hour < 12 ? 'a.m.' : 'p.m.'; + let h = hour % 12; + h = h ? h : 12; + h = ("0" + h).slice(-2); + return `${h}:${m} ${ampm}`; + } else { + let h = ("0" + hour).slice(-2); + return `${h}:${m}`; + } + }, + + getDevice: function (data) { + const self = this; + let devices; + if (this.config.deviceID !== "") { + devices = data.filter(function (value) { + return value.id == self.config.deviceID; + }); + } else if (this.config.deviceName !== "") { + devices = data.filter(function (value) { + return value.name == self.config.deviceName; + }); + } + if (devices.length) { + return devices[0]; + } else { + return NaN; + } + }, + + getDom: function () { + const wrapper = document.createElement("div"); + const title = document.createElement("span"); + title.style.fontSize = "0.5em"; + title.innerHTML = this.config.title; + wrapper.appendChild(title); + wrapper.style.width = this.config.width; + wrapper.style.height = this.config.height; + wrapper.style.lineHeight = "1em"; + if (this.sensorData) { + const dev = this.getDevice(this.sensorData); + if (!dev) { + const error = document.createElement("div"); + error.innerHTML = "No devices are found"; + wrapper.appendChild(error); + return wrapper; + } + if (this.config.showTemperature) { + const tempWrapper = document.createElement("div"); + const tempTitle = document.createElement("span"); + const temp = document.createElement("span"); + tempTitle.style.fontSize = "0.8em"; + tempTitle.innerHTML = this.config.temperatureTitle; + temp.style.color = "#fff"; + if (this.config.temperatureUnit == "Fahrenheit") { + temp.innerHTML = `${dev.newest_events.te.val * 1.8 + 32}°F`; + } else { + temp.innerHTML = `${dev.newest_events.te.val}°C`; + } + tempWrapper.appendChild(tempTitle); + tempWrapper.appendChild(temp); + wrapper.appendChild(tempWrapper); + } + if (this.config.showHumidity) { + const humiWrapper = document.createElement("div"); + const humiTitle = document.createElement("span"); + const humi = document.createElement("span"); + humiTitle.style.fontSize = "0.8em"; + humiTitle.innerHTML = this.config.humidityTitle; + humi.style.color = "#fff"; + humi.innerHTML = `${dev.newest_events.hu.val}%`; + humiWrapper.appendChild(humiTitle); + humiWrapper.appendChild(humi); + wrapper.appendChild(humiWrapper); + } + if (this.config.showIllumination) { + const illuWrapper = document.createElement("div"); + const illuTitle = document.createElement("span"); + const illu = document.createElement("span"); + illuTitle.style.fontSize = "0.8em"; + illuTitle.innerHTML = this.config.illuminationTitle; + illu.style.color = "#fff"; + illu.innerHTML = `${this.getIllumiString(dev.newest_events.il.val)}`; + illuWrapper.appendChild(illuTitle); + illuWrapper.appendChild(illu); + wrapper.appendChild(illuWrapper); + } + if (this.config.showMotion) { + const motiWrapper = document.createElement("div"); + const motiTitle = document.createElement("span"); + const moti = document.createElement("span"); + motiTitle.style.fontSize = "0.8em"; + motiTitle.innerHTML = this.config.motionTitle; + moti.style.color = "#fff"; + let unixTime = Date.parse(dev.newest_events.mo.created_at); + unixTime = unixTime + this.config.motionDateOffsetSeconds; + const date = new Date(unixTime); + const time = this.getHourString(date.getHours(), date.getMinutes()); + moti.innerHTML = `${date.getMonth() + 1}/${date.getDate()} ${time}`; + motiWrapper.appendChild(motiTitle); + motiWrapper.appendChild(moti); + wrapper.appendChild(motiWrapper); + } + } + + // Data from helper + if (this.dataNotification) { + var wrapperDataNotification = document.createElement("div"); + // translations + datanotification + wrapperDataNotification.innerHTML = "Updated at " + this.dataNotification.date; + + wrapper.appendChild(wrapperDataNotification); + } + return wrapper; + }, + + getScripts: function () { + return []; + }, + + getStyles: function () { + return []; + }, + + getTranslations: function () { + return false; + }, + + processData: function (data) { + var self = this; + this.sensorData = data; + if (this.loaded === false) { self.updateDom(self.config.animationSpeed); } + this.loaded = true; + + // the data if load + // send notification to helper + this.sendSocketNotification("MMM-NatureRemo-NOTIFICATION", data); + }, + + // socketNotificationReceived from helper + socketNotificationReceived: function (notification, payload) { + if (notification === "MMM-NatureRemo-NOTIFICATION") { + // set dataNotification + this.dataNotification = payload; + this.updateDom(); + } + }, +}); diff --git a/README.md b/README.md index cc9ac0f..875bd17 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # MMM-NatureRemo -Magic Mirror module for displaying sensor data from Nature Remo +Magic Mirror module for displaying sensor data from [Nature Remo](https://en.nature.global/). + +![image](https://user-images.githubusercontent.com/48573325/100518374-85a5de80-31d4-11eb-9604-a1e5dd07e329.png) + +[Nature Remo](https://en.nature.global/) has a set of sensors and these sensor data can be fetched from [Nature Remo cloud API](https://developer.nature.global/en/overview). +The cloud API is available for anyone who got Nature Remo smart controller by creating access token on the web page. +## Installation + +Clone this repository and place it on MagicMirror module directory. +``` +$ cd ~/MagicMirror/modules +$ git clone https://github.com/mtatsuma/MMM-NatureRemo.git +``` + +## Configuration example +``` + - module: MMM-NatureRemo + position: top_left + config: + apiToken: xxxx + deviceName: remo-1 +``` + +## Configuration options + +| Options | Required | Default | Description | +|:--------|:--------:|:--------|:------------| +| updateInterval | | `10 * 60 * 1000` | Weather data update interval (miliseconds). Note: the access rate for [Nature Remo cloud API](https://developer.nature.global/en/overview) is limited as 30 requests per 5 minutes. | +| retryDelay | | `5 * 1000` | Delay for retry to get weather data (miliseconds) | +| title | | `Nature Remo` | Title to display | +| apiBase | | `https://api.nature.global/` | Base URL of [Nature Remo cloud API](https://developer.nature.global/en/overview) | +| apiEndpoint | | `1/devices` | API endpoint to be used for fetching sensor data | +| apiToken | yes | | API access token to call [Nature Remo cloud API](https://developer.nature.global/en/overview). | +| deviceName | | | Device name of the target Nature Remo device. If you don't set both of `deviceName` and `deviceID`, a device on the top of the device list will be selected. | +| deviceID | | | Device ID of the target Nature Remo device. If you don't set both of `deviceName` and `deviceID`, a device on the top of the device list will be selected. | +| height | | `300px` | Height of the display box for this module. | +| width | | `500px` | Width of the display box for this module. | +| showTemperature | | `true` | Show temperature data. | +| showHumidity | | `true` | Show humidity data. | +| showIllumination | | `true` | Show illumination data (Dark, Dim, Medium, Light and sensor data value). | +| showMotion | | `true` | Show motion sensor data (motion detection time). | +| temperatureTitle | | `Temperature: ` | Title for temperature data | +| humidityTitle | | `Humidity: ` | Title for humidity data | +| illuminationTitle | | `Illumination: ` | Title for illumination data | +| motionTitle | | `Motion Detect: ` | Title for motion sensor data. | +| temperatureUnit | | `Celsius` | Unit of temperature. `Celsius` or `Fahrenheit` | +| motionDateOffsetSeconds | | `0` | Time offset (seconds) for motion detection time | +| motionDateHourFormat | | `24h` | If you set `12h`, the time format of motion detection time is changed to 12 hour format |