diff --git a/.gitignore b/.gitignore index 3bf582b..43d01e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# No dot-directories except github/vscode +# No dot-directories except github .*/ !.vscode/ !.github/ diff --git a/README.md b/README.md index 602e602..993e184 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,16 @@ Oura provides an [API](https://cloud.ouraring.com/v2/docs) to access to all the ## Changelog -### 0.0.1 + -- (Acgua) **WORK IN PROGRESS** +### **WORK IN PROGRESS** +- Some changes + +#### 0.0.1 +- (Acgua) Initial release ## License diff --git a/build/main.js b/build/main.js index 5346bb5..4a0488f 100644 --- a/build/main.js +++ b/build/main.js @@ -29,6 +29,15 @@ module.exports = __toCommonJS(main_exports); var utils = __toESM(require("@iobroker/adapter-core")); var import_axios = __toESM(require("axios")); var import_methods = require("./lib/methods"); +/** + * ------------------------------------------------------------------- + * ioBroker Oura Adapter + * @github https://github.com/Acgua/ioBroker.oura + * @author Acgua + * @created Adapter Creator v2.1.1 + * @license Apache License 2.0 + * ------------------------------------------------------------------- + */ class Oura extends utils.Adapter { constructor(options = {}) { super({ ...options, name: "oura" }); @@ -48,6 +57,12 @@ class Oura extends utils.Adapter { throw `Your Oura cloud token in your adapter configuration is not valid! [${this.config.token}]`; this.config.token = tkn; await this.asyncUpdateAll(); + timerId = setTimeout(function tick() { + counter++; + doStuff(); + if (counter <= 5) + timerId = setTimeout(tick, 2e3); + }, 2e3); this.intervalCloudupdate = setInterval(async () => { this.log.info("Scheduled update of cloud information."); await this.asyncUpdateAll(); @@ -73,34 +88,35 @@ class Oura extends utils.Adapter { await this.setObjectNotExistsAsync(what, { type: "device", common: { name: what }, native: {} }); for (const cloudDay of cloudAllDays) { if (!cloudDay.timestamp) { - this.log.warn(`'${what}' Cloud data retrieval: No timestamp in object`); + this.log.debug(`'${what}' Cloud data retrieval: No timestamp in object, so we disregard`); continue; } - const isoToday = this.getIsoDate(new Date(cloudDay.timestamp)); - if (!isoToday) { - this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp: [${cloudDay.timestamp}]`); + const dayPart = this.getWordForIsoDate(new Date(cloudDay.timestamp)); + if (!dayPart) { + this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp or other issue with timestamp: [${cloudDay.timestamp}]`); continue; } - await this.setObjectNotExistsAsync(`${what}.${isoToday}`, { type: "channel", common: { name: isoToday + " - " + what }, native: {} }); - await this.setObjectNotExistsAsync(`${what}.${isoToday}.json`, { type: "state", common: { name: "JSON", type: "string", role: "json", read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.json`, { val: JSON.stringify(cloudDay), ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}`, { type: "channel", common: { name: dayPart + " - " + what }, native: {} }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.json`, { type: "state", common: { name: "JSON", type: "string", role: "json", read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.json`, { val: JSON.stringify(cloudDay), ack: true }); for (const prop in cloudDay) { if (prop === "timestamp") { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.timestamp`, { type: "state", common: { name: "Timestamp", type: "number", role: "date", read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.timestamp`, { type: "state", common: { name: "Timestamp", type: "number", role: "date", read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true }); } else if (prop === "contributors") { for (const k in cloudDay.contributors) { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.contributors.${k}`, { type: "state", common: { name: k, type: "number", role: "info", read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.contributors.${k}`, { type: "state", common: { name: k, type: "number", role: "info", read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true }); } } else if (typeof cloudDay[prop] === "number") { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: "state", common: { name: prop, type: "number", role: "info", read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: "state", common: { name: prop, type: "number", role: "info", read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true }); } else if (typeof cloudDay[prop] === "string") { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: "state", common: { name: prop, type: "string", role: "info", read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: "state", common: { name: prop, type: "string", role: "info", read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true }); + } else if (typeof cloudDay[prop] === "object") { } else { - this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: [${typeof cloudDay[prop]}]`); + this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: ${typeof cloudDay[prop]}`); } } } @@ -108,7 +124,7 @@ class Oura extends utils.Adapter { if (gotData.length > 0) this.log.info(`Following data received from Oura cloud: ${gotData.join(", ")}`); if (noData.length > 0) - this.log.warn(`Could not get following data from Oura cloud: ${noData.join(", ")}`); + this.log.debug(`No Oura cloud data available for: ${noData.join(", ")}`); } catch (e) { this.log.error(this.err2Str(e)); } @@ -130,6 +146,8 @@ class Oura extends utils.Adapter { timeout }; const response = await import_axios.default.get(url, config); + this.log.debug(`Response Status: ${response.status} - ${response.statusText}`); + this.log.debug(`Response Config: ${JSON.stringify(response.config)}`); if (!response.data || !response.data.data || !response.data.data[0]) { return false; } @@ -176,6 +194,28 @@ class Oura extends utils.Adapter { return false; } } + getWordForIsoDate(date) { + try { + const dateTs = date.getTime(); + date.setHours(0, 0, 0, 0); + const now = new Date(); + now.setHours(0, 0, 0, 0); + const nowTs = now.getTime(); + const diffDays = Math.ceil((nowTs - dateTs) / 864e5); + if (diffDays < 0) { + throw `Negative date difference for given date, which is not supported.`; + } else if (diffDays === 0) { + return "00-today"; + } else if (diffDays === 1) { + return "01-yesterday"; + } else { + return String(diffDays).padStart(2, "0") + "-days-ago"; + } + } catch (e) { + this.log.error(this.err2Str(e)); + return false; + } + } onUnload(callback) { try { clearInterval(this.intervalCloudupdate); diff --git a/build/main.js.map b/build/main.js.map index 9904366..1176498 100644 --- a/build/main.js.map +++ b/build/main.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/main.ts"], - "sourcesContent": ["/*\n * Created with @iobroker/create-adapter v2.1.1\n */\n\n// Oura API (2.0): https://cloud.ouraring.com/v2/docs\n\n/**\n * For all imported NPM modules, open console, change dir e.g. to \"C:\\iobroker\\node_modules\\ioBroker.oura\\\",\n * and execute \"npm install \", ex: npm install got\n */\nimport * as utils from '@iobroker/adapter-core';\nimport axios from 'axios'; // https://github.com/axios/axios\nimport { err2Str, getIsoDate, isEmpty, wait } from './lib/methods';\n\n/**\n * Main Adapter Class\n */\nexport class Oura extends utils.Adapter {\n // Imported methods from ./lib/methods\n public err2Str = err2Str.bind(this);\n public isEmpty = isEmpty.bind(this);\n public wait = wait.bind(this);\n public getIsoDate = getIsoDate.bind(this);\n private intervalCloudupdate: any;\n\n /**\n * Constructor\n */\n public constructor(options: Partial = {}) {\n super({ ...options, name: 'oura' });\n this.on('ready', this.onReady.bind(this));\n this.on('unload', this.onUnload.bind(this));\n // this.on('stateChange', this.onStateChange.bind(this));\n this.intervalCloudupdate = null;\n }\n\n /**\n * Called once ioBroker databases are connected and adapter received configuration.\n */\n private async onReady(): Promise {\n try {\n // Basic verification of token\n let tkn = this.config.token;\n tkn = tkn.replace(/[^0-9A-Z]/g, ''); // remove all forbidden chars\n if (tkn.length !== 32) throw `Your Oura cloud token in your adapter configuration is not valid! [${this.config.token}]`;\n this.config.token = tkn;\n\n // Update now\n await this.asyncUpdateAll();\n\n // Update periodically\n this.intervalCloudupdate = setInterval(async () => {\n this.log.info('Scheduled update of cloud information.');\n await this.asyncUpdateAll();\n }, 1000 * 60 * 60); // every hour\n } catch (e) {\n this.log.error(this.err2Str(e));\n }\n }\n\n /**\n * TODO: TEST\n */\n private async asyncUpdateAll(): Promise {\n try {\n const ouraTypes = ['daily_activity', 'daily_readiness', 'daily_sleep', 'heartrate', 'session', 'sleep', 'tag', 'workout'];\n const startDate = new Date(Date.now() - 10 * 86400000); // 86400000 = 24 hours in ms\n const endDate = new Date(Date.now() + 86400000); // for yet some unknown reason, some data require a \"+1d\"...\n const gotData = [];\n const noData = [];\n for (const what of ouraTypes) {\n const cloudAllDays = await this.asyncGetCloudData(what, startDate, endDate);\n if (!cloudAllDays) {\n noData.push(what);\n continue;\n }\n gotData.push(what);\n await this.setObjectNotExistsAsync(what, { type: 'device', common: { name: what }, native: {} });\n\n for (const cloudDay of cloudAllDays) {\n if (!cloudDay.timestamp) {\n this.log.warn(`'${what}' Cloud data retrieval: No timestamp in object`);\n continue;\n }\n const isoToday = this.getIsoDate(new Date(cloudDay.timestamp));\n if (!isoToday) {\n this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp: [${cloudDay.timestamp}]`);\n continue;\n }\n\n await this.setObjectNotExistsAsync(`${what}.${isoToday}`, { type: 'channel', common: { name: isoToday + ' - ' + what }, native: {} });\n await this.setObjectNotExistsAsync(`${what}.${isoToday}.json`, { type: 'state', common: { name: 'JSON', type: 'string', role: 'json', read: true, write: false }, native: {} });\n await this.setStateAsync(`${what}.${isoToday}.json`, { val: JSON.stringify(cloudDay), ack: true });\n for (const prop in cloudDay) {\n if (prop === 'timestamp') {\n await this.setObjectNotExistsAsync(`${what}.${isoToday}.timestamp`, { type: 'state', common: { name: 'Timestamp', type: 'number', role: 'date', read: true, write: false }, native: {} });\n await this.setStateAsync(`${what}.${isoToday}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true });\n } else if (prop === 'contributors') {\n for (const k in cloudDay.contributors) {\n await this.setObjectNotExistsAsync(`${what}.${isoToday}.contributors.${k}`, { type: 'state', common: { name: k, type: 'number', role: 'info', read: true, write: false }, native: {} });\n await this.setStateAsync(`${what}.${isoToday}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true });\n }\n } else if (typeof cloudDay[prop] === 'number') {\n await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: 'state', common: { name: prop, type: 'number', role: 'info', read: true, write: false }, native: {} });\n await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true });\n } else if (typeof cloudDay[prop] === 'string') {\n await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: 'state', common: { name: prop, type: 'string', role: 'info', read: true, write: false }, native: {} });\n await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true });\n } else {\n this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: [${typeof cloudDay[prop]}]`);\n }\n }\n }\n }\n if (gotData.length > 0) this.log.info(`Following data received from Oura cloud: ${gotData.join(', ')}`);\n if (noData.length > 0) this.log.warn(`Could not get following data from Oura cloud: ${noData.join(', ')}`);\n } catch (e) {\n this.log.error(this.err2Str(e));\n }\n }\n\n /**\n * Get Oura Cloud Information\n * @param what - daily_activity, etc.\n * @param startDate as date object or timestamp\n * @param endDate as date object or timestamp\n * @returns Object\n */\n private async asyncGetCloudData(what: string, startDate: Date | number, endDate: Date | number): Promise<[{ [k: string]: any }] | false> {\n try {\n // Verify dates and convert to ISO format\n const sDate = this.getIsoDate(startDate);\n const eDate = this.getIsoDate(endDate);\n if (!sDate || !eDate) throw `Could not get cloud data, wrong date(s) provided`;\n const url = `https://api.ouraring.com/v2/usercollection/${what}?start_date=${sDate}&end_date=${eDate}`;\n this.log.debug('Final URL: ' + url);\n const timeout = 3000;\n\n /**\n * Axios\n * https://cloud.ouraring.com/v2/docs#section/Oura-HTTP-Response-Codes\n */\n try {\n const config = {\n method: 'get',\n headers: { Authorization: 'Bearer ' + this.config.token },\n timeout: timeout,\n };\n const response = await axios.get(url, config);\n // this.log.debug(`Response Status: ${response.status} - ${response.statusText}`);\n // this.log.debug(`Response Config: ${JSON.stringify(response.config)}`);\n if (!response.data || !response.data.data || !response.data.data[0]) {\n // this.log.info('::::: EMPTY RESPONSE ::::::');\n return false;\n }\n /*\n for (const elem of response.data.data) {\n delete response.data.data[elem].class_5_min;\n delete response.data.data[elem].data.met;\n }\n */\n return response.data.data;\n } catch (err) {\n if (axios.isAxiosError(err)) {\n if (!err?.response) {\n this.log.error(`[Oura Cloud] Login Failed - No Server Response. Timeout: ${timeout} ms`);\n } else if (err.response?.status === 400) {\n this.log.error('[Oura Cloud] Login Failed - Error 400 - ' + err.response?.statusText);\n } else if (err.response?.status === 401) {\n this.log.error(`[Oura Cloud] Error 401 - Invalid Access Token. Access token not provided or is invalid.`);\n this.log.error(`[Oura Cloud] Login Failed. Please check if your token \"${this.config.token}\" is correct.`);\n } else if (err.response?.status === 426) {\n this.log.error(`[Oura Cloud] Error 426 - Minimum App Version Error. The Oura user's mobile app does not meet the minimum app version requirement to support sharing the requested data type. The Oura user must update their mobile app to enable API access for the requested data type.`);\n this.log.error(`[Oura Cloud] Login Failed. Please ensure you use the latest Oura app`);\n } else if (err.response?.status === 429) {\n this.log.error(`[Oura Cloud] Error 429 - Request Rate Limit Exceeded. The API is rate limited to 5000 requests in a 5 minute period and you exceed this limit.`);\n this.log.error(`[Oura Cloud] Login Failed.`);\n } else if (err.response?.status) {\n console.log(`[Oura Cloud] Login Failed: Error ${err.response.status} - ${err.response.statusText}`);\n } else {\n console.log('[Oura Cloud] Login Failed - Error');\n }\n } else {\n if (err instanceof Error) {\n if (err.stack) {\n if (err.stack.startsWith('TypeError')) {\n this.log.error('[Oura Cloud] TYPE ERROR:' + err.stack);\n } else {\n this.log.error('[Oura Cloud] OTHER ERROR: ' + err.stack);\n }\n }\n if (err.message) this.log.error('msg: ' + err.message);\n } else {\n this.log.error('[Oura Cloud] Error: ' + this.err2Str(err));\n }\n }\n return false;\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return false;\n }\n }\n\n /**\n * Is called when adapter shuts down - callback has to be called under any circumstances!\n */\n private onUnload(callback: () => void): void {\n try {\n // Here you must clear all timeouts or intervals that may still be active\n // clearTimeout(timeout1);\n // clearTimeout(timeout2);\n // ...\n // clearInterval(interval1);\n clearInterval(this.intervalCloudupdate);\n\n callback();\n } catch (e) {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial | undefined) => new Oura(options);\n} else {\n // otherwise start the instance directly\n (() => new Oura())();\n}\n"], - "mappings": "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", + "sourcesContent": ["/**\n * -------------------------------------------------------------------\n * ioBroker Oura Adapter\n * @github https://github.com/Acgua/ioBroker.oura\n * @author Acgua \n * @created Adapter Creator v2.1.1\n * @license Apache License 2.0\n * -------------------------------------------------------------------\n */\n\n/**\n * For all imported NPM modules, open console, change dir e.g. to \"C:\\iobroker\\node_modules\\ioBroker.oura\\\",\n * and execute \"npm install \", ex: npm install axios\n */\nimport * as utils from '@iobroker/adapter-core';\nimport axios from 'axios';\nimport { err2Str, getIsoDate, isEmpty, wait } from './lib/methods';\n\n/**\n * Main Adapter Class\n */\nexport class Oura extends utils.Adapter {\n // Imported methods from ./lib/methods\n public err2Str = err2Str.bind(this);\n public isEmpty = isEmpty.bind(this);\n public wait = wait.bind(this);\n public getIsoDate = getIsoDate.bind(this);\n private intervalCloudupdate: any;\n\n /**\n * Constructor\n */\n public constructor(options: Partial = {}) {\n super({ ...options, name: 'oura' });\n this.on('ready', this.onReady.bind(this));\n this.on('unload', this.onUnload.bind(this));\n // this.on('stateChange', this.onStateChange.bind(this));\n this.intervalCloudupdate = null;\n }\n\n /**\n * Called once ioBroker databases are connected and adapter received configuration.\n */\n private async onReady(): Promise {\n try {\n // Basic verification of token\n let tkn = this.config.token;\n tkn = tkn.replace(/[^0-9A-Z]/g, ''); // remove all forbidden chars\n if (tkn.length !== 32) throw `Your Oura cloud token in your adapter configuration is not valid! [${this.config.token}]`;\n this.config.token = tkn;\n\n // Update now\n await this.asyncUpdateAll();\n\n // Update periodically\n\n timerId = setTimeout(function tick() {\n counter++;\n doStuff();\n if (counter <= 5) timerId = setTimeout(tick, 2000);\n }, 2000);\n \n\n\n this.intervalCloudupdate = setInterval(async () => {\n this.log.info('Scheduled update of cloud information.');\n await this.asyncUpdateAll();\n }, 1000 * 60 * 60); // every hour\n } catch (e) {\n this.log.error(this.err2Str(e));\n }\n }\n\n /**\n * TODO: TEST\n */\n private async asyncUpdateAll(): Promise {\n try {\n const ouraTypes = ['daily_activity', 'daily_readiness', 'daily_sleep', 'heartrate', 'session', 'sleep', 'tag', 'workout'];\n const startDate = new Date(Date.now() - 10 * 86400000); // 86400000 = 24 hours in ms\n const endDate = new Date(Date.now() + 86400000); // for yet some unknown reason, some data require a \"+1d\"...\n const gotData = [];\n const noData = [];\n for (const what of ouraTypes) {\n const cloudAllDays = await this.asyncGetCloudData(what, startDate, endDate);\n if (!cloudAllDays) {\n noData.push(what);\n continue;\n }\n gotData.push(what);\n await this.setObjectNotExistsAsync(what, { type: 'device', common: { name: what }, native: {} });\n\n for (const cloudDay of cloudAllDays) {\n if (!cloudDay.timestamp) {\n this.log.debug(`'${what}' Cloud data retrieval: No timestamp in object, so we disregard`);\n continue;\n }\n const dayPart = this.getWordForIsoDate(new Date(cloudDay.timestamp));\n if (!dayPart) {\n this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp or other issue with timestamp: [${cloudDay.timestamp}]`);\n continue;\n }\n\n await this.setObjectNotExistsAsync(`${what}.${dayPart}`, { type: 'channel', common: { name: dayPart + ' - ' + what }, native: {} });\n await this.setObjectNotExistsAsync(`${what}.${dayPart}.json`, { type: 'state', common: { name: 'JSON', type: 'string', role: 'json', read: true, write: false }, native: {} });\n await this.setStateChangedAsync(`${what}.${dayPart}.json`, { val: JSON.stringify(cloudDay), ack: true });\n for (const prop in cloudDay) {\n if (prop === 'timestamp') {\n await this.setObjectNotExistsAsync(`${what}.${dayPart}.timestamp`, { type: 'state', common: { name: 'Timestamp', type: 'number', role: 'date', read: true, write: false }, native: {} });\n await this.setStateChangedAsync(`${what}.${dayPart}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true });\n } else if (prop === 'contributors') {\n for (const k in cloudDay.contributors) {\n await this.setObjectNotExistsAsync(`${what}.${dayPart}.contributors.${k}`, { type: 'state', common: { name: k, type: 'number', role: 'info', read: true, write: false }, native: {} });\n await this.setStateChangedAsync(`${what}.${dayPart}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true });\n }\n } else if (typeof cloudDay[prop] === 'number') {\n await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: 'state', common: { name: prop, type: 'number', role: 'info', read: true, write: false }, native: {} });\n await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true });\n } else if (typeof cloudDay[prop] === 'string') {\n await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: 'state', common: { name: prop, type: 'string', role: 'info', read: true, write: false }, native: {} });\n await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true });\n } else if (typeof cloudDay[prop] === 'object') {\n // Nothing, we disregard objects, will be available in JSON anyway\n } else {\n this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: ${typeof cloudDay[prop]}`);\n }\n }\n }\n }\n if (gotData.length > 0) this.log.info(`Following data received from Oura cloud: ${gotData.join(', ')}`);\n if (noData.length > 0) this.log.debug(`No Oura cloud data available for: ${noData.join(', ')}`);\n } catch (e) {\n this.log.error(this.err2Str(e));\n }\n }\n\n /**\n * Get Oura Cloud Information\n * @param what - daily_activity, etc.\n * @param startDate as date object or timestamp\n * @param endDate as date object or timestamp\n * @returns Object\n */\n private async asyncGetCloudData(what: string, startDate: Date | number, endDate: Date | number): Promise<[{ [k: string]: any }] | false> {\n try {\n // Verify dates and convert to ISO format\n const sDate = this.getIsoDate(startDate);\n const eDate = this.getIsoDate(endDate);\n if (!sDate || !eDate) throw `Could not get cloud data, wrong date(s) provided`;\n const url = `https://api.ouraring.com/v2/usercollection/${what}?start_date=${sDate}&end_date=${eDate}`;\n this.log.debug('Final URL: ' + url);\n const timeout = 3000;\n\n /**\n * Axios\n * https://cloud.ouraring.com/v2/docs#section/Oura-HTTP-Response-Codes\n */\n try {\n const config = {\n method: 'get',\n headers: { Authorization: 'Bearer ' + this.config.token },\n timeout: timeout,\n };\n const response = await axios.get(url, config);\n this.log.debug(`Response Status: ${response.status} - ${response.statusText}`);\n this.log.debug(`Response Config: ${JSON.stringify(response.config)}`);\n if (!response.data || !response.data.data || !response.data.data[0]) {\n // this.log.info('::::: EMPTY RESPONSE ::::::');\n return false;\n }\n /*\n for (const elem of response.data.data) {\n delete response.data.data[elem].class_5_min;\n delete response.data.data[elem].data.met;\n }\n */\n return response.data.data;\n } catch (err) {\n if (axios.isAxiosError(err)) {\n if (!err?.response) {\n this.log.error(`[Oura Cloud] Login Failed - No Server Response. Timeout: ${timeout} ms`);\n } else if (err.response?.status === 400) {\n this.log.error('[Oura Cloud] Login Failed - Error 400 - ' + err.response?.statusText);\n } else if (err.response?.status === 401) {\n this.log.error(`[Oura Cloud] Error 401 - Invalid Access Token. Access token not provided or is invalid.`);\n this.log.error(`[Oura Cloud] Login Failed. Please check if your token \"${this.config.token}\" is correct.`);\n } else if (err.response?.status === 426) {\n this.log.error(`[Oura Cloud] Error 426 - Minimum App Version Error. The Oura user's mobile app does not meet the minimum app version requirement to support sharing the requested data type. The Oura user must update their mobile app to enable API access for the requested data type.`);\n this.log.error(`[Oura Cloud] Login Failed. Please ensure you use the latest Oura app`);\n } else if (err.response?.status === 429) {\n this.log.error(`[Oura Cloud] Error 429 - Request Rate Limit Exceeded. The API is rate limited to 5000 requests in a 5 minute period and you exceed this limit.`);\n this.log.error(`[Oura Cloud] Login Failed.`);\n } else if (err.response?.status) {\n console.log(`[Oura Cloud] Login Failed: Error ${err.response.status} - ${err.response.statusText}`);\n } else {\n console.log('[Oura Cloud] Login Failed - Error');\n }\n } else {\n if (err instanceof Error) {\n if (err.stack) {\n if (err.stack.startsWith('TypeError')) {\n this.log.error('[Oura Cloud] TYPE ERROR:' + err.stack);\n } else {\n this.log.error('[Oura Cloud] OTHER ERROR: ' + err.stack);\n }\n }\n if (err.message) this.log.error('msg: ' + err.message);\n } else {\n this.log.error('[Oura Cloud] Error: ' + this.err2Str(err));\n }\n }\n return false;\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return false;\n }\n }\n\n /**\n * Get '00-today', '01-yesterday', '02-days-ago' etc. for a given Date object\n */\n private getWordForIsoDate(date: Date): string | false {\n try {\n const dateTs = date.getTime();\n date.setHours(0, 0, 0, 0);\n const now = new Date();\n now.setHours(0, 0, 0, 0);\n const nowTs = now.getTime();\n const diffDays = Math.ceil((nowTs - dateTs) / 86400000);\n if (diffDays < 0) {\n throw `Negative date difference for given date, which is not supported.`;\n } else if (diffDays === 0) {\n return '00-today';\n } else if (diffDays === 1) {\n return '01-yesterday';\n } else {\n return String(diffDays).padStart(2, '0') + '-days-ago';\n }\n } catch (e) {\n this.log.error(this.err2Str(e));\n return false;\n }\n }\n\n /**\n * Is called when adapter shuts down - callback has to be called under any circumstances!\n */\n private onUnload(callback: () => void): void {\n try {\n // Here you must clear all timeouts or intervals that may still be active\n // clearTimeout(timeout1);\n // clearTimeout(timeout2);\n // ...\n // clearInterval(interval1);\n clearInterval(this.intervalCloudupdate);\n\n callback();\n } catch (e) {\n callback();\n }\n }\n}\n\nif (require.main !== module) {\n // Export the constructor in compact mode\n module.exports = (options: Partial | undefined) => new Oura(options);\n} else {\n // otherwise start the instance directly\n (() => new Oura())();\n}\n"], + "mappings": "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", "names": ["axios"] } diff --git a/io-package.json b/io-package.json index 7b1bb6c..308d444 100644 --- a/io-package.json +++ b/io-package.json @@ -40,8 +40,15 @@ "pl": "Integracja Oura pierścienia", "zh-cn": "我们的一体化" }, - "authors": ["Acgua "], - "keywords": ["oura", "ring", "health", "sleep"], + "authors": [ + "Acgua " + ], + "keywords": [ + "oura", + "ring", + "health", + "sleep" + ], "license": "Apache-2.0", "platform": "Javascript/Node.js", "main": "build/main.js", diff --git a/src/main.ts b/src/main.ts index 6618074..eabff82 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,19 @@ -/* - * Created with @iobroker/create-adapter v2.1.1 +/** + * ------------------------------------------------------------------- + * ioBroker Oura Adapter + * @github https://github.com/Acgua/ioBroker.oura + * @author Acgua + * @created Adapter Creator v2.1.1 + * @license Apache License 2.0 + * ------------------------------------------------------------------- */ -// Oura API (2.0): https://cloud.ouraring.com/v2/docs - /** * For all imported NPM modules, open console, change dir e.g. to "C:\iobroker\node_modules\ioBroker.oura\", - * and execute "npm install ", ex: npm install got + * and execute "npm install ", ex: npm install axios */ import * as utils from '@iobroker/adapter-core'; -import axios from 'axios'; // https://github.com/axios/axios +import axios from 'axios'; import { err2Str, getIsoDate, isEmpty, wait } from './lib/methods'; /** @@ -79,41 +83,43 @@ export class Oura extends utils.Adapter { for (const cloudDay of cloudAllDays) { if (!cloudDay.timestamp) { - this.log.warn(`'${what}' Cloud data retrieval: No timestamp in object`); + this.log.debug(`'${what}' Cloud data retrieval: No timestamp in object, so we disregard`); continue; } - const isoToday = this.getIsoDate(new Date(cloudDay.timestamp)); - if (!isoToday) { - this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp: [${cloudDay.timestamp}]`); + const dayPart = this.getWordForIsoDate(new Date(cloudDay.timestamp)); + if (!dayPart) { + this.log.warn(`'${what}' Cloud data retrieval: No valid timestamp or other issue with timestamp: [${cloudDay.timestamp}]`); continue; } - await this.setObjectNotExistsAsync(`${what}.${isoToday}`, { type: 'channel', common: { name: isoToday + ' - ' + what }, native: {} }); - await this.setObjectNotExistsAsync(`${what}.${isoToday}.json`, { type: 'state', common: { name: 'JSON', type: 'string', role: 'json', read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.json`, { val: JSON.stringify(cloudDay), ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}`, { type: 'channel', common: { name: dayPart + ' - ' + what }, native: {} }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.json`, { type: 'state', common: { name: 'JSON', type: 'string', role: 'json', read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.json`, { val: JSON.stringify(cloudDay), ack: true }); for (const prop in cloudDay) { if (prop === 'timestamp') { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.timestamp`, { type: 'state', common: { name: 'Timestamp', type: 'number', role: 'date', read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.timestamp`, { type: 'state', common: { name: 'Timestamp', type: 'number', role: 'date', read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.timestamp`, { val: new Date(cloudDay.timestamp).getTime(), ack: true }); } else if (prop === 'contributors') { for (const k in cloudDay.contributors) { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.contributors.${k}`, { type: 'state', common: { name: k, type: 'number', role: 'info', read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.contributors.${k}`, { type: 'state', common: { name: k, type: 'number', role: 'info', read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.contributors.${k}`, { val: cloudDay.contributors[k], ack: true }); } } else if (typeof cloudDay[prop] === 'number') { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: 'state', common: { name: prop, type: 'number', role: 'info', read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: 'state', common: { name: prop, type: 'number', role: 'info', read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true }); } else if (typeof cloudDay[prop] === 'string') { - await this.setObjectNotExistsAsync(`${what}.${isoToday}.${prop}`, { type: 'state', common: { name: prop, type: 'string', role: 'info', read: true, write: false }, native: {} }); - await this.setStateAsync(`${what}.${isoToday}.${prop}`, { val: cloudDay[prop], ack: true }); + await this.setObjectNotExistsAsync(`${what}.${dayPart}.${prop}`, { type: 'state', common: { name: prop, type: 'string', role: 'info', read: true, write: false }, native: {} }); + await this.setStateChangedAsync(`${what}.${dayPart}.${prop}`, { val: cloudDay[prop], ack: true }); + } else if (typeof cloudDay[prop] === 'object') { + // Nothing, we disregard objects, will be available in JSON anyway } else { - this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: [${typeof cloudDay[prop]}]`); + this.log.error(`${what}: property '${prop}' is unknown! - value: [${cloudDay[prop]}], type: ${typeof cloudDay[prop]}`); } } } } if (gotData.length > 0) this.log.info(`Following data received from Oura cloud: ${gotData.join(', ')}`); - if (noData.length > 0) this.log.warn(`Could not get following data from Oura cloud: ${noData.join(', ')}`); + if (noData.length > 0) this.log.debug(`No Oura cloud data available for: ${noData.join(', ')}`); } catch (e) { this.log.error(this.err2Str(e)); } @@ -147,8 +153,8 @@ export class Oura extends utils.Adapter { timeout: timeout, }; const response = await axios.get(url, config); - // this.log.debug(`Response Status: ${response.status} - ${response.statusText}`); - // this.log.debug(`Response Config: ${JSON.stringify(response.config)}`); + this.log.debug(`Response Status: ${response.status} - ${response.statusText}`); + this.log.debug(`Response Config: ${JSON.stringify(response.config)}`); if (!response.data || !response.data.data || !response.data.data[0]) { // this.log.info('::::: EMPTY RESPONSE ::::::'); return false; @@ -202,6 +208,32 @@ export class Oura extends utils.Adapter { } } + /** + * Get '00-today', '01-yesterday', '02-days-ago' etc. for a given Date object + */ + private getWordForIsoDate(date: Date): string | false { + try { + const dateTs = date.getTime(); + date.setHours(0, 0, 0, 0); + const now = new Date(); + now.setHours(0, 0, 0, 0); + const nowTs = now.getTime(); + const diffDays = Math.ceil((nowTs - dateTs) / 86400000); + if (diffDays < 0) { + throw `Negative date difference for given date, which is not supported.`; + } else if (diffDays === 0) { + return '00-today'; + } else if (diffDays === 1) { + return '01-yesterday'; + } else { + return String(diffDays).padStart(2, '0') + '-days-ago'; + } + } catch (e) { + this.log.error(this.err2Str(e)); + return false; + } + } + /** * Is called when adapter shuts down - callback has to be called under any circumstances! */