diff --git a/.eslintrc.json b/.eslintrc.json index 50409a9..2ce7aea 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,7 @@ "rules": { "indent": [ "error", - "tab", + 4, { "SwitchCase": 1 } diff --git a/README.md b/README.md index 2a76545..09ca5e1 100644 --- a/README.md +++ b/README.md @@ -10,83 +10,41 @@ **Tests:**: [![Travis-CI](http://img.shields.io/travis/MiGoller/ioBroker.life360/master.svg)](https://travis-ci.org/MiGoller/ioBroker.life360) -## life360 adapter for ioBroker - -An ioBroker adapter for Life360. - -## Developer manual -This section is intended for the developer. It can be deleted later - -### Getting started - -You are almost done, only a few steps left: -1. Create a new repository on GitHub with the name `ioBroker.life360` -1. Initialize the current folder as a new git repository: - ```bash - git init - git add . - git commit -m "Initial commit" - ``` -1. Link your local repository with the one on GitHub: - ```bash - git remote add origin https://github.com/MiGoller/ioBroker.life360 - ``` - -1. Push all files to the GitHub repo: - ```bash - git push origin master - ``` -1. Head over to [main.js](main.js) and start programming! - -### Scripts in `package.json` -Several npm scripts are predefined for your convenience. You can run them using `npm run ` -| Script name | Description | -|-------------|----------------------------------------------------------| -| `test:js` | Executes the tests you defined in `*.test.js` files. | -| `test:package` | Ensures your `package.json` and `io-package.json` are valid. | -| `test` | Performs a minimal test run on package files and your tests. | -| `coverage` | Generates code coverage using your test files. | - -### Writing tests -When done right, testing code is invaluable, because it gives you the -confidence to change your code while knowing exactly if and when -something breaks. A good read on the topic of test-driven development -is https://hackernoon.com/introduction-to-test-driven-development-tdd-61a13bc92d92. -Although writing tests before the code might seem strange at first, but it has very -clear upsides. - -The template provides you with basic tests for the adapter startup and package files. -It is recommended that you add your own tests into the mix. - -### Publishing the adapter -See the documentation of [ioBroker.repositories](https://github.com/ioBroker/ioBroker.repositories#requirements-for-adapter-to-get-added-to-the-latest-repository). - -### Test the adapter manually on a local ioBroker installation -In order to install the adapter locally without publishing, the following steps are recommended: -1. Create a tarball from your dev directory: - ```bash - npm pack - ``` -1. Upload the resulting file to your ioBroker host -1. Install it locally (The paths are different on Windows): - ```bash - cd /opt/iobroker - npm i /path/to/tarball.tgz - ``` - -For later updates, the above procedure is not necessary. Just do the following: -1. Overwrite the changed files in the adapter directory (`/opt/iobroker/node_modules/iobroker.life360`) -1. Execute `iobroker upload life360` on the ioBroker host +## Life360 adapter for ioBroker + +An ioBroker adapter for [Life360](https://www.life360.com). + +## Description + +This adapter connects to the [Life360](https://www.life360.com) cloud services to allow you to track people and to detect their presence at defined places. It retrieves information about the user's circles, the circles' members and the circles' places. These information persists the adapter in ioBroker states. Any states will get updated in a given interval. + +## Installation + +Right now you'll have to add the adapter to your ioBroker using a custom url pointing to the corresponding [GitHub](https://github.com/) repository at https://github.com/MiGoller/ioBroker.life360/tree/master . + +## Configuration + +You'll have to setup the adapter with your personal [Life360](https://www.life360.com) credentials to let the adapter poll the information from the cloud services. You can login with your mobile phone number or your email-address (recommended) for Life360, but in any case you'll have to set the password to your personal Life360 password. + +Feel free to modify the default timespan of 60 seconds for the polling interval. The adapter does not allow modifying the interval to less than 15 seconds to prevent gaining any rate limits and to prevent ioBroker Admin getting slower and slower. + +## Disclaimer +I did not find any official documentation for the [Life360](https://www.life360.com) REST APIs. Apparently [Life360](https://www.life360.com) does not support the use of the REST API for other applications than its own ones. + +My REST API integration is based on reverse engineering done by the open source community and an API token discovered on [Life360](https://www.life360.com) code which is public available. [Life360](https://www.life360.com) could disable or modify this API token or change its REST API in a way that this adapter will not work as expected anymore. ## Changelog +### 0.1.1 +* (migoller) First alpha release + ### 0.0.1 -* (MiGoller) initial release +* (migoller) initial release ## License MIT License -Copyright (c) 2019 MiGoller <1272642+MiGoller@users.noreply.github.com> +Copyright (c) 2019 Michael Goller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/admin/index_m.html b/admin/index_m.html index 2dd90cc..cdae91a 100644 --- a/admin/index_m.html +++ b/admin/index_m.html @@ -73,16 +73,26 @@ -
- - + + +
+
+ + +
+
+ + +
+
+ +
-
- - + +
diff --git a/admin/life360.png b/admin/life360.png index 094ebd9..22465a9 100644 Binary files a/admin/life360.png and b/admin/life360.png differ diff --git a/io-package.json b/io-package.json index 3ba4597..94172a8 100644 --- a/io-package.json +++ b/io-package.json @@ -1,8 +1,12 @@ { "common": { "name": "life360", - "version": "0.0.1", + "version": "0.1.1", "news": { + "0.1.1": { + "en": "First release", + "de": "Erstes Release" + }, "0.0.1": { "en": "initial release", "de": "Erstveröffentlichung", @@ -42,11 +46,11 @@ "zh-cn": "An ioBroker adapter for Life360." }, "authors": [ - "MiGoller <1272642+MiGoller@users.noreply.github.com>" + "migoller " ], "keywords": [ "ioBroker", - "template", + "Life360", "Smart Home", "home automation" ], @@ -69,9 +73,62 @@ ] }, "native": { - "option1": true, - "option2": "42" + "life360_username": "me@me.local", + "life360_password": "MySecretPassword", + "life360_phone": "", + "life360_countryCode": "", + "life360_polling_interval": "60" }, "objects": [], - "instanceObjects": [] + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + }, + { + "_id": "info.connection", + "type": "state", + "common": { + "name": "connected", + "desc": "Connected to Life360 cloud services?", + "type": "boolean", + "def": "false", + "read": "true", + "role": "value.info", + "write": "false" + }, + "native": {} + }, + { + "_id": "circles", + "type": "device", + "common": { + "name": "Circles", + "desc": "Life360 circles." + }, + "native": {} + }, + { + "_id": "places", + "type": "device", + "common": { + "name": "Places", + "desc": "Life360 places." + }, + "native": {} + }, + { + "_id": "people", + "type": "device", + "common": { + "name": "People", + "desc": "Life360 people." + }, + "native": {} + } + ] } \ No newline at end of file diff --git a/lib/iobHelpers.js b/lib/iobHelpers.js new file mode 100644 index 0000000..d05b02c --- /dev/null +++ b/lib/iobHelpers.js @@ -0,0 +1,104 @@ +"use strict"; + +/** + * IobLogger simplifies logging for adapter development and operations. + */ +exports.IobLogger = class { + /** + * Creates a new IobLogger instance + * @param {*} adapter_instance Set to your adapter instance to enable logging in ioBroker. + */ + constructor(adapter_instance) { + // this.myAdapter = adapter_instance; + this.setAdapter(adapter_instance); + } + + /** + * Sets the ioBroker adapter instance to log to. + * @param {*} adapter_instance Set to your adapter instance to enable logging in ioBroker. + */ + setAdapter(adapter_instance) { + this.myAdapter = adapter_instance; + } + + /** + * Create an ERROR log message. + * @param {*} message + */ + error(message) { + if (!this.myAdapter) console.error(`ERROR: ${message}`); else this.myAdapter.log.error(message); + } + + /** + * Create a WARN log message. + * @param {*} message + */ + warn(message) { + if (!this.myAdapter) console.warn(`WARN: ${message}`); else this.myAdapter.log.warn(message); + } + + /** + * Create an INFO log message. + * @param {*} message + */ + info(message) { + if (!this.myAdapter) console.log(`INFO: ${message}`); else this.myAdapter.log.info(message); + } + + /** + * Create a DEBUG log message. + * @param {*} message + */ + debug(message) { + if (!this.myAdapter) console.debug(`DEBUG: ${message}`); else this.myAdapter.log.debug(message); + } + + /** + * Create a SILLY log message. + * @param {*} message + */ + silly(message) { + if (!this.myAdapter) console.debug(`SILLY: ${message}`); else this.myAdapter.log.silly(message); + } + + /** + * Logger is a wrapper for logging. + * @param {string} level Set to "error", "warn", "info", "debug" + * @param {*} message The message to log + */ + logger(level, message) { + switch (level) { + case "error": + this.error(message); + break; + + case "warn": + this.warn(message); + break; + + case "info": + this.info(message); + break; + + case "debug": + this.debug(message); + break; + + case "silly": + this.silly(message); + break; + + default: + break; + } + } + + /** + * Log is an alias for Logger! + * @param {string} level Set to "error", "warn", "info", "debug", "silly" + * @param {*} message The message to log + */ + log(level, message) { + this.logger(level, message); + } +} diff --git a/lib/life360CloudConnector.js b/lib/life360CloudConnector.js new file mode 100644 index 0000000..7606688 --- /dev/null +++ b/lib/life360CloudConnector.js @@ -0,0 +1,705 @@ +"use strict"; + +let adapter; + +// Load core-modules ... +const Promise = require("bluebird"); +const request = Promise.promisify(require("request")); +Promise.promisifyAll(request); + +// ioBroker specific modules +const iobHelpers = require("./iobHelpers"); +const myLogger = new iobHelpers.IobLogger(adapter); + +/** + * Look up hard-coded "CLIENT_SECRET" in https://www.life360.com/circles/scripts/ccf35026.scripts.js ! + */ +const LIFE360_CLIENT_SECRET = "U3dlcUFOQWdFVkVoVWt1cGVjcmVrYXN0ZXFhVGVXckFTV2E1dXN3MzpXMnZBV3JlY2hhUHJlZGFoVVJhZ1VYYWZyQW5hbWVqdQ=="; + +/** + * The Life360 API URIs. + * - login URL + * - circles URL + */ +const LIFE360_URL = { + login: "https://www.life360.com/v3/oauth2/token.json", + circles: "https://www.life360.com/v3/circles" +}; + +const min_polling_interval = 15; // Min polling interval in seconds +const maxAgeToken = 300; // Max age of the Life360 token in seconds +let objTimeoutConnection = null; // Connection Timeout id +let objIntervalPoll = null; // Poll Interval id +let countOnlineOperations = 0; // How many online operations are running? +let adapterConnectionState = false; // Life360 connection status. + +/** + * Stores authentication information for the current session. + * - access token + * - type of token + */ +let auth = { + access_token: null, + token_type: null +}; + +/** + * Stores the data retrieved from Life360 cloud services. + */ +let cloud_data = { + circles: [] +}; + +/** + * Returns the number of pending online operations against Life360 cloud services. + */ +function getCurrentOnlineOperations() { + return countOnlineOperations; +} + +/** + * Notify the Life360 cloud connector about starting a new online operation. + */ +function startOnlineOperation() { + countOnlineOperations += 1; + logger("silly", `Current online operations: ${countOnlineOperations}.`); + return countOnlineOperations; +} + +/** + * Notify the Life360 cloud connector about finished an online operation. + */ +function stopOnlineOperation() { + countOnlineOperations -= 1; + if (countOnlineOperations < 0) countOnlineOperations = 0; + logger("silly", `Current online operations: ${countOnlineOperations}.`); + return countOnlineOperations; +} + +/** + * Simple sleep function. + * @param {number} milliseconds Time to sleep (ms.). + */ +function Sleep(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +/** + * Logger is a wrapper for logging. + * @param {*} level Set to "error", "warn", "info", "debug" + * @param {*} message The message to log + */ +function logger(level, message) { + myLogger.logger(level, message); +} + +/** + * Updates the Life360 connector's state for the ioBroker instance. + * @param {boolean} isConnected Set to true if connected. + */ +function setAdapterConnectionState(isConnected) { + if (!adapter) { + // No adapter instance set. + } + else { + adapter.setState("info.connection", isConnected); + if (isConnected != adapterConnectionState) { + if (isConnected) + myLogger.info("Connected to Life360 cloud services."); + else + myLogger.info("Disconnected from Life360 cloud services."); + } + adapterConnectionState = isConnected; + } +} + +/** + * Set ioBroker adapter instance for the connector + * @param {*} adapter_in The adapter instance for this connector. +*/ +exports.setAdapter = function(adapter_in) { + adapter = adapter_in; + myLogger.setAdapter(adapter); +}; + +/** + * Connect to the Life360 service. + * Specify a username OR both a phone number and country code. + * @param {*} username Life360 username, or undefined if phone specified. + * @param {*} password Life360 password. + * @param {*} phone Life360 phone, or undefined if username specified. + * @param {*} countryCode Optional phone country code, defaults to 1 if not specified. + */ +exports.connectLife360 = function(username, password, phone, countryCode) { + return new Promise((resolve, reject) => { + if(!username || typeof username === "function") { + if (!adapter) username = process.env.LIFE360_USERNAME; else username = adapter.config.life360_username; + } + + if(!password || typeof password === "function") { + if (!adapter) password = process.env.LIFE360_PASSWORD; else password = adapter.config.life360_password; + } + + if(!phone || typeof phone === "function") { + if (!adapter) phone = process.env.LIFE360_PHONE; else phone = adapter.config.life360_phone; + } + + if(!countryCode || typeof countryCode === "function") { + if (!adapter) countryCode = process.env.LIFE360_COUNTRYCODE; else countryCode = adapter.config.life360_countryCode; + } + + logger("debug", "Connecting to Life360 service ..."); + + auth = { + access_token: null, + token_type: null + }; + + countryCode = typeof countryCode !== "undefined" ? countryCode : 1; + username = typeof username !== "undefined" ? username : ""; + phone = typeof phone !== "undefined" ? phone : ""; + // if(!password) throw new Error("Life360: No password specified."); + + const options = { + url: LIFE360_URL.login, + method: "POST", + body: `countryCode=${countryCode}&username=${username}&phone=${phone}&password=${password}&grant_type=password`, + headers: { + "Authorization": `Authorization: Basic ${LIFE360_CLIENT_SECRET}`, + "Content-Type" : "application/x-www-form-urlencoded" + }, + json: true + }; + + request(options) + .then(response => { + if ((response.statusMessage === "Forbidden") || !response.body["access_token"]) { + auth = { + access_token: null, + token_type: null + }; + + logger("error", "Connection established but failed to authenticate. Check your credentials!"); + logger("debug", "Auth tokens deleted."); + + reject(new Error("Connection established but failed to authenticate. Check your credentials!")); + } + else { + auth = { + access_token: response.body["access_token"], + token_type: response.body["token_type"] + }; + + logger("debug", `Logged in as user: ${username}, phone: ${phone} .`); + logger("debug", "Saved auth tokens."); + + resolve(auth); + } + }) + .catch(err => { + setAdapterConnectionState(false); + reject(new Error("Unable to connect: " + err)); + }); + }); +}; + +/** + * Ensures connection to the Life360 service. + */ +exports.connect = function() { + return new Promise((resolve, reject) => { + if (!exports.is_connected()) { + logger("debug", "Not authenticated against Life360. Will try to connect ..."); + + // exports.connectLife360(process.env.LIFE360_USERNAME, process.env.LIFE360_PASSWORD, process.env.LIFE360_PHONE, process.env.LIFE360_COUNTRYCODE) + exports.connectLife360(null, null, null, null) + .then(auth_new => { + auth = auth_new; + + // Set timeout to remove auth tokens + objTimeoutConnection = setTimeout(() => { + exports.disconnect(); + }, (maxAgeToken / 3 * 2) * 1000); + + setAdapterConnectionState(true); + + resolve(auth); + }) + .catch(err => { + exports.disconnect(); + setAdapterConnectionState(false); + reject(new Error(err)); + }); + } + else { + resolve(auth); + } + }); +}; + + +/** + * Disconnect from Life360 (i.e. clear all tokens) + */ +exports.disconnect = async function() { + clearTimeout(objTimeoutConnection); + + if (getCurrentOnlineOperations() > 0) { + logger("info", "Waiting for online operations to finish ..."); + while (getCurrentOnlineOperations() != 0) { + await Sleep(1000); + logger("silly", ` - Pending operations: ${getCurrentOnlineOperations()}.`); + } + } + + auth = { + access_token: null, + token_type: null + }; + + logger("debug", "Auth tokens deleted."); + // logger("info", "Disconnected from Life360."); +}; + +/** + * Returns true if connected to Life360 cloud services. + */ +exports.is_connected = function() { + return auth.access_token; +}; + +/** + * Returns the authentication information for Life360. + */ +exports.get_auth = function () { + return auth; +}; + +/** + * Returns a list of the user's Life360 circles. + */ +exports.getCircles = function(auth_in) { + return new Promise((resolve, reject) => { + if (!auth_in) auth_in = auth; + + const options = { + url: LIFE360_URL.circles, + headers: { + "Authorization": `${auth_in.token_type} ${auth_in.access_token}` + }, + json: true + }; + + logger("silly", `Retrieving circles at ${LIFE360_URL.circles}`); + + request(options) + .then(response => { + if (!response.body.circles) { + logger("error", "No circles found!"); + reject(new Error("No circles found!")); + } + else { + if (response.body.circles.length == 0) { + logger("error", "No circles in your Life360."); + reject(new Error("No circles in your Life360.")); + } + else { + logger("debug", "Retrieved circles."); + resolve(response.body.circles); + } + } + }) + .catch(err => { + reject(new Error("Unable to poll circles: " + err)); + }); + }); +}; + +/** + * Returns details for a Life360 circle identified by the the circle's id. + */ +exports.getCircleById = function(auth_in, circleId) { + return new Promise((resolve, reject) => { + if (!auth_in) auth_in = auth; + + const LIFE360_CIRCLE_URL = `${LIFE360_URL.circles}/${circleId}`; + const options = { + url: LIFE360_CIRCLE_URL, + headers: { + "Authorization": `${auth_in.token_type} ${auth_in.access_token}` + }, + json: true + }; + + logger("silly", `Retrieving circle at ${LIFE360_CIRCLE_URL}`); + + request(options) + .then(response => { + logger("silly", `Retrieved circle with id ${circleId} !`); + resolve(response.body); + }) + .catch(err => { + reject(new Error(`Unable to poll circle with ID ${circleId}: ${err}`)); + }); + }); +}; + +/** + * Deprecated. + */ +exports.getCircleMembersPromise = function(circle_in) { + return new Promise((resolve, reject) => { + if (!circle_in) { + reject(new Error("Provide a circle object, please.")); + } + else { + const members = []; + + if (circle_in.members.length == 0) { + console.log("Circle has no members."); + } + else { + for (let oMember in circle_in.members) { + let member = circle_in.members[oMember]; + members.push( {id: member.id, json: member} ); + } + } + + resolve(members); + } + }); +}; + +/** + * Returns an array conaining a circle's members. + */ +exports.getCircleMembers = function(circle_in) { + const members = []; + + if (!circle_in) { + logger("error", "Provide a circle object, please."); + } + else { + if (circle_in.members.length == 0) { + logger("debug", "Circle has no members."); + } + else { + for (let oMember in circle_in.members) { + let member = circle_in.members[oMember]; + members.push( {id: member.id, json: member} ); + } + } + } + + return members; +}; + +/** + * Disables automatic polling. + */ +exports.disablePolling = function() { + if (objIntervalPoll) { + clearTimeout(objIntervalPoll); + logger("info", "Disabled polling."); + + } +}; + +/** + * Enables automatic polling. + */ +exports.setupPolling = function(callback) { + let polling_interval = min_polling_interval; + + if (!adapter) + polling_interval = Number(process.env.LIFE360_POLLING_INTERVAL); + else + polling_interval = Number(adapter.config.life360_polling_interval); + + if (polling_interval < min_polling_interval) { + logger("error", "Polling interval should be greater than " + min_polling_interval); + + return false; + } else { + exports.disablePolling(); + + // exports.poll(callback); + exports.pollAsync(callback); + + // Enable polling + objIntervalPoll = setInterval(() => { + // exports.poll(callback); + exports.pollAsync(callback); + }, polling_interval * 1000); + + logger("info", `Polling enabled every ${polling_interval} seconds.`); + return true; + } +}; + +/** + * DEPRECATED: Initiates a Life360 cloud data poll and passes the data to a callback function. +*/ +exports.poll = function(callback) { + pollLife360Data() + .then(cloud_data => { + if (callback) { + logger("debug", "Pushing cloud_data to callback function"); + callback(false, cloud_data); + } + return true; + }) + .catch(err => { + if (callback) { + callback(err, null); + } + else { + logger("error", `Error polling Life360 data: ${err}`); + } + return false; + }); +}; + +/** + * DEPRECATED: Polls the Life360 cloud data + */ +function pollLife360Data() { + return new Promise((resolve, reject) => { + cloud_data.circles = []; + + exports.connect() + .then(auth => { + // Connected and authenticated + exports.getCircles(auth) + .then(circles => { + // Circles polled + let circlePromises = []; + for (let oCircle in circles) { + circlePromises.push(exports.getCircleById(auth, circles[oCircle].id)); + } + + Promise.all(circlePromises) + .then((result) => { + // Called when all promises are resolved! + cloud_data.circles = result; + logger("silly", `Retrieved data for ${cloud_data.circles.length} circle(s).`); + resolve(cloud_data); + }) + .catch(err => { + logger("error", `Error while polling the Life360 circle data: ${err}`); + + reject(new Error(`Error while polling the Life360 circle data: ${err}`)); + }); + }) + .catch(err => { + logger("error", `Error while polling the Life360 circle data: ${err}`); + + reject(new Error(`Error while polling the Life360 circle data: ${err}`)); + }); + + }) + .catch(err => { + logger("error", `Error while polling the Life360 circle data: ${err}`); + + reject(new Error(`Error while polling the Life360 data: ${err}`)); + }); + }); +} +/** + * Initiates an async Life360 cloud data poll and passes the data to a callback function. + * @param {Function} callback The callback function. + */ +exports.pollAsync = function(callback) { + myLogger.debug("Fetching Life360 cloud data ..."); + pollLife360DataAsync() + .then(cloud_data => { + if (callback) { + logger("debug", "Pushing cloud_data to callback function"); + callback(false, cloud_data); + } + return true; + }) + .catch(err => { + if (callback) { + callback(err, null); + } + else { + logger("error", `Error polling Life360 data: ${err}`); + } + return false; + }); +}; + +/** + * Polls (async) the Life360 cloud data. + */ +async function pollLife360DataAsync() { + cloud_data.circles = []; + + try { + // Ensure we are connected and authorized + startOnlineOperation(); + const auth_in = await exports.connect(); + + // Connected. Start polling Life360 data. + + // First poll the user's circles. + const circles = await exports.getCirclesAsync(auth_in); + + for (let c in circles) { + const circle = circles[c]; + logger("silly", `circle ${circle.id} --> ${circle.name}`); + + // Get circle's members + const circleMembers = await exports.getCircleMembersAsync(auth_in, circle.id); + circle.members = circleMembers; + logger("silly", ` - ${circle.members.length} member(s) found.`); + + // for (let m in circleMembers) { + // const member = circleMembers[m]; + // logger("silly", ` - ${member.id} --> ${member.firstName} ${member.lastName}`); + // } + + // Get circle's places + const circlePlaces = await exports.getCirclePlacesAsync(auth_in, circle.id); + circle.places = circlePlaces; + logger("silly", ` - ${circle.places.length} place(s) found.`); + + // for (let m in circlePlaces) { + // const place = circlePlaces[m]; + // console.log(` - ${place.id} --> ${place.name} (lon: ${place.longitude}, lat: ${place.latitude})`); + // } + } + + // Return the retrieved Life360 cloud data + cloud_data.circles = circles; + stopOnlineOperation(); + return cloud_data; + } catch (error) { + stopOnlineOperation(); + logger("error", error); + } +}; + +/** + * Returns the Life360 circles. + * @param {*} auth_in The auth object. + */ +exports.getCirclesAsync = async function(auth_in) { + if (!auth_in) auth_in = auth; + + const options = { + url: LIFE360_URL.circles, + headers: { + "Authorization": `${auth_in.token_type} ${auth_in.access_token}` + }, + json: true + }; + + logger("silly", `Async - Retrieving circles at ${LIFE360_URL.circles}`); + + try { + startOnlineOperation(); + const response = await request(options); + + let obj = []; + + if (!response.body.circles) { + logger("warn", "No circles found!"); + } + else { + logger("silly", `Retrieved ${response.body.circles.length} circle(s).`); + obj = response.body.circles; + } + + stopOnlineOperation(); + return obj; + } catch (error) { + logger("error", `Failed to retrieve members: ${error}`); + stopOnlineOperation(); + } + +}; + +/** + * Returns the Life360 circle's members. + * @param {*} auth_in The auth object. + * @param {*} circleId The id of a Life360 circle. + */ +exports.getCircleMembersAsync = async function(auth_in, circleId) { + if (!auth_in) auth_in = auth; + + const URL = `${LIFE360_URL.circles}/${circleId}/members`; + const options = { + url: URL, + headers: { + "Authorization": `${auth_in.token_type} ${auth_in.access_token}` + }, + json: true + }; + + logger("silly", `Retrieving members at ${URL}`); + + try { + startOnlineOperation(); + const response = await request(options); + + let obj = []; + + if (!response.body.members) { + logger("warn", "No members found!"); + } + else { + logger("silly", `Retrieved ${response.body.members.length} member(s).`); + obj = response.body.members; + } + + stopOnlineOperation(); + return obj; + } catch (error) { + logger("error", `Failed to retrieve members: ${error}`); + stopOnlineOperation(); + } + +}; + +/** + * Returns the Life360 circle's places. + * @param {*} auth_in The auth object. + * @param {*} circleId The id of a Life360 circle. + */ +exports.getCirclePlacesAsync = async function(auth_in, circleId) { + if (!auth_in) auth_in = auth; + + const URL = `${LIFE360_URL.circles}/${circleId}/places`; + const options = { + url: URL, + headers: { + "Authorization": `${auth_in.token_type} ${auth_in.access_token}` + }, + json: true + }; + + logger("silly", `Retrieving places at ${URL}`); + + try { + startOnlineOperation(); + const response = await request(options); + + let obj = []; + + if (!response.body.places) { + logger("warn", "No places found!"); + } + else { + logger("silly", `Retrieved ${response.body.places.length} place(s).`); + obj = response.body.places; + } + + stopOnlineOperation(); + return obj; + } catch (error) { + logger("error", `Failed to retrieve places: ${error}`); + stopOnlineOperation(); + } +}; diff --git a/lib/life360DbConnector.js b/lib/life360DbConnector.js new file mode 100644 index 0000000..a2a35f6 --- /dev/null +++ b/lib/life360DbConnector.js @@ -0,0 +1,712 @@ +"use strict"; + +let adapter; + +// Core-modules ... +const Promise = require("bluebird"); + +// ioBroker specific modules +const iobHelpers = require("./iobHelpers"); +const myLogger = new iobHelpers.IobLogger(adapter); + +const dpPrefix = { + "adapter": null, + "circles": "circles", + "people": "people", + "places": "places" +}; + +const dpLife360Type = { + "circle": "device", + "place": "device", + "person": "device", + "members": "channel", + "places": "channel" +}; + +/** + * Logger is a wrapper for logging. + * @param {*} level Set to "error", "warn", "info", "debug" + * @param {*} message The message to log + */ +function logger(level, message) { + myLogger.logger(level, message); +} + +/** + * Set ioBroker adapter instance for the connector + * @param {*} adapter_in The adapter instance for this connector. +*/ +exports.setAdapter = function(adapter_in) { + adapter = adapter_in; + myLogger.setAdapter(adapter); +}; + +exports.getPrefix_Circles = function() { + return dpPrefix.circles; +}; + +exports.createCircleState = function(circle) { + return new Promise((resolve, reject) => { + createCircleDP(circle) + .then((result) => { + resolve(result); + }).catch((err) => { + reject(err); + }); + }); +}; + +/** + * Creates an OpenStreetMap URL to show the given position. + * @param {number} lat Set to GPS latitude. + * @param {number} lon Set to GPS longitude. + * @param {number} zoom Set to zoom factor 1 up to 19. + */ +function getOpenStreetMapUrl(lat, lon, zoom) { + if (!zoom) zoom = 15; + return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=${zoom}/${lat}/${lon}`; +} + +/** + * ============================================================================ + * ---------------------------------------------------------------------------- + * ioBroker Datapoint helper functions + * ---------------------------------------------------------------------------- + * ============================================================================ + */ + +/** + * Creates an ioBroker Datapoint Object + * @param {*} dpId The datapoint's id. + * @param {*} obj ioBroker datapoint object. + */ +async function createDataPointRawAsync(dpId, obj) { + // Create ioBroker object if it does not exists + await adapter.setObjectNotExistsAsync(dpId, obj); + return obj; +} + +/** + * Wrapper to easily create an ioBroker Datapoint Object + * @param {*} dpId The datapoint's id + * @param {*} dpType Type of the datapoint + * @param {*} dpName Name of the datapoint + * @param {*} dpDesc Description of the datapoint + */ +async function createObjectDP(dpId, dpType, dpName, dpDesc) { + const obj = { + "_id": dpId, + "type": dpType, + "common": { + "name": dpName, + "desc": dpDesc + }, + "native": {} + }; + + // Create ioBroker object if it does not exists + return await createDataPointRawAsync(dpId, obj); +} + +/** + * Wrapper to easily create an ioBroker State datapoint + * @param {*} dpId The datapoint's id + * @param {*} dpName Name of the datapoint + * @param {*} dpRead Set to true to grant read access to the datapoint. + * @param {*} dpWrite Set to true to grad write access to the datapoint. + * @param {*} dpType Type of the datapoint + * @param {*} dpRole Role of the datapoint + */ +async function createStateDP(dpId, dpName, dpRead, dpWrite, dpType, dpRole) { + const obj = { + "_id": dpId, + "type": "state", + "common": { + "name": dpName, + "read": dpRead, + "write": dpWrite, + "type": dpType, + "role": dpRole + }, + "native": {} + }; + + // Create ioBroker object if it does not exists + return await createDataPointRawAsync(dpId, obj); +} + +// function createStateReadOnlyDP(dpId, dpName, dpType, dpRole) { +// return createStateDP(dpId, dpName, true, false, dpType, dpRole); +// } + +/** + * Creates a state datapoint and to set it's value. + * @param {*} dpId The datapoint's id + * @param {*} dpName Name of the datapoint + * @param {*} dpRead Set to true to grant read access to the datapoint. + * @param {*} dpWrite Set to true to grad write access to the datapoint. + * @param {*} dpType Type of the datapoint + * @param {*} dpRole Role of the datapoint + * @param {*} val The state's value + * @param {*} ack Ack? + */ +async function setStateValue(dpId, dpName, dpRead, dpWrite, dpType, dpRole, val, ack) { + myLogger.silly(`setStateValue --> ${dpId} = ${val}`); + + // Create ioBroker state object + const obj = createStateDP(dpId, dpName, dpRead || true, dpWrite || true, dpType, dpRole); + + // Update state + await adapter.setStateAsync(dpId, val, ack); + + return obj; +} + +/** + * Wrapper to create a read only state datapoint and to set it's value. + * @param {*} dpId The datapoint's id + * @param {*} dpName Name of the datapoint + * @param {*} dpType Type of the datapoint + * @param {*} dpRole Role of the datapoint + * @param {*} val The state's value + * @param {*} ack Ack? + */ +async function setStateReadOnlyValue(dpId, dpName, dpType, dpRole, val, ack) { + // myLogger.silly(`setStateReadOnlyValue --> ${dpId} = ${val}`); + + return setStateValue(dpId, dpName, true, false, dpType, dpRole, val, ack); +} + +/** + * Returns an array of persons for the members of the given circles. + * @param {array} circles The Life360 circles to process. + */ +function getPersons(circles) { + let persons = []; + + for (let c in circles) { + const circle = circles[c]; + + for (let m in circle.members) { + const member = circle.members[m]; + + if (!(persons.some(person => person.id === member.id))) { + // Add new person to the array + persons.push(member); + } + } + } + + return persons; +} + +/** + * Returns an array of places for the given circles. + * @param {array} circles The Life360 circles to process. + */ +function getPlaces(circles) { + let places = []; + + for (let c in circles) { + const circle = circles[c]; + + for (let p in circle.places) { + const place = circle.places[p]; + + if (!(places.some(aPlace => aPlace.id === place.id))) { + // Add new place to the array + places.push(place); + } + } + } + + return places; +} + +/** + * ============================================================================ + * ---------------------------------------------------------------------------- + * This is the primary receiver for the Life360 cloud data (circles.) + * ---------------------------------------------------------------------------- + * ============================================================================ + */ + +/** + * This is the primary receiver for the Life360 cloud data (circles.) + * @param {*} err Set to false, if no error occured, otherwise Error object. + * @param {object} cloud_data The cloud data object to publish to ioBroker. + */ +exports.publishCloudData = function(err, cloud_data) { + + if (!err) { + // Get all Life360 places from the circles. + cloud_data.places = getPlaces(cloud_data.circles); + + // Get all Life360 circles' members. + cloud_data.persons = getPersons(cloud_data.circles); + + // Publish all known Life360 places to ioBroker + publishPlaces(cloud_data.places); + + // Publish all known Life360 circles' members + publishPeople(cloud_data.persons); + + // Publish the circles + publishCircles(cloud_data.circles); + + // That's it. + myLogger.debug("Life360 cloud data processed."); + } + else { + logger("error", err); + } +}; + +/** + * ============================================================================ + * ---------------------------------------------------------------------------- + * Functions to publish Life360 places to ioBroker + * ---------------------------------------------------------------------------- + * ============================================================================ + */ + +/** + * + * @param {*} places + */ +function publishPlaces(places) { + for (let p in places) { + try { + publishPlace(places[p]); + myLogger.silly(`Created / updated place ${places[p].id} --> ${places[p].name}`); + } catch (error) { + myLogger.error(error); + } + } + + myLogger.debug(`Published ${places.length} place(s) to ioBroker.`); +} + +/** + * + * @param {*} place + */ +async function publishPlace(place) { + // { + // "id": "", + // "ownerId": "owner ", + // "circleId": "circle ", + // "name": "", + // "latitude": "", + // "longitude": "", + // "radius": "", + // "type": null, + // "typeLabel": null + // }, + + // Create an object for the place + const dpPlace = await createPlaceDP(place); + + // Now set the place's states. + await createPlaceStateDP(dpPlace._id, "id", place.id); + await createPlaceStateDP(dpPlace._id, "ownerId", place.ownerId); + await createPlaceStateDP(dpPlace._id, "circleId", place.circleId); + await createPlaceStateDP(dpPlace._id, "name", place.name); + await createPlaceStateDP(dpPlace._id, "latitude", Number(place.latitude)); + await createPlaceStateDP(dpPlace._id, "longitude", Number(place.longitude)); + await createPlaceStateDP(dpPlace._id, "radius", Number(place.radius)); + + // Finally create an OpenStreetMap URL + await createPlaceStateDP(dpPlace._id, "urlMap", getOpenStreetMapUrl(Number(place.latitude), Number(place.longitude), 15)); +} + +/** + * Creates an object datapoint for a place + * @param {*} place The place object. + * @param {*} id Optional. Will be set to ${dpPrefix.places}.${place.id} if missing. + */ +function createPlaceDP(place, id) { + let dpId; + if (typeof id === "undefined") + dpId = `${dpPrefix.places}.${place.id}`; + else + dpId = id; + + return createObjectDP(dpId, dpLife360Type.place, place.name, place.name); +} + +async function createPlaceStateDP(idDP, state, val) { + let cRole = "state"; + const cType = typeof(val); + // const dpID_state = `${idDP}.${state}`; + + switch (state) { + case "createdAt": + cRole = "date"; + break; + + case "latitude": + cRole = "value.gps.latitude"; + break; + + case "longitude": + cRole = "value.gps.longitude"; + break; + + default: + switch(cType) { + case "string": + cRole = "text"; + break; + case "number": + cRole = "value"; + break; + } + break; + } + + const obj = await setStateReadOnlyValue(`${idDP}.${state}`, state, cType, cRole, val, true); + + return obj; +} + +/** + * ============================================================================ + * ---------------------------------------------------------------------------- + * Functions to publish Life360 people to ioBroker + * ---------------------------------------------------------------------------- + * ============================================================================ + */ + +/** + * + * @param {*} persons + */ +function publishPeople(persons) { + for (let p in persons) { + try { + publishPerson(persons[p]); + myLogger.silly(`Created / updated person ${persons[p].id} --> ${persons[p].name}`); + } catch (error) { + myLogger.error(error); + } + } + + myLogger.debug(`Published ${persons.length} people to ioBroker.`); +} + +/** + * + * @param {*} person + * @param {*} idParentDp + */ +async function publishPerson(person, idParentDp) { + /* + { + "features": { + "device": "1", + "smartphone": "1", + "nonSmartphoneLocating": "0", + "geofencing": "1", + "shareLocation": "1", + "shareOffTimestamp": null, + "disconnected": "0", + "pendingInvite": "0", + "mapDisplay": "1" + }, + "issues": { + "disconnected": "0", + "type": null, + "status": null, + "title": null, + "dialog": null, + "action": null, + "troubleshooting": "0" + }, + "location": { + "latitude": "", + "longitude": "", + "accuracy": "", + "startTimestamp": , + "endTimestamp": "", + "since": , + "timestamp": "", + "name": null, + "placeType": null, + "source": null, + "sourceId": null, + "address1": null, + "address2": "", + "shortAddress": "", + "inTransit": "0", + "tripId": null, + "driveSDKStatus": null, + "battery": "", + "charge": "0", + "wifiState": "1", + "speed": 0, + "isDriving": "0", + "userActivity": null + }, + "communications": [ + { + "channel": "Voice", + "value": "+1....", + "type": "Home" + }, + { + "channel": "Email", + "value": "me@my.local", + "type": null + } + ], + "medical": null, + "relation": null, + "createdAt": "", + "activity": null, + "id": "", + "firstName": "...", + "lastName": "...", + "isAdmin": "0", + "avatar": null, + "pinNumber": null, + "loginEmail": "me@my.local", + "loginPhone": "+1...." + } + */ + + // Create an object for the person + let dpId = `${dpPrefix.people}.${person.id}`; + if (typeof idParentDp !== "undefined") dpId = `${idParentDp}.${person.id}`; + + const dpPerson = await createPersonDP(person, dpId); + + // Now set the person's states. + await createPersonStateDP(dpPerson._id, "id", person.id); + await createPersonStateDP(dpPerson._id, "createdAt", Number(person.createdAt) * 1000); + await createPersonStateDP(dpPerson._id, "firstName", person.firstName); + await createPersonStateDP(dpPerson._id, "lastName", person.lastName); + await createPersonStateDP(dpPerson._id, "avatar", person.avatar); + // await createPersonStateDP(dpPerson._id, "loginEmail", person.loginEmail); + // await createPersonStateDP(dpPerson._id, "loginPhone", person.loginPhone); + await createPersonStateDP(dpPerson._id, "latitude", Number(person.location.latitude)); + await createPersonStateDP(dpPerson._id, "longitude", Number(person.location.longitude)); + await createPersonStateDP(dpPerson._id, "disconnected", Boolean(person.issues.disconnected)); + await createPersonStateDP(dpPerson._id, "status", person.issues.status || "Ok"); + await createPersonStateDP(dpPerson._id, "lastPositionAt", Number(person.location.timestamp) * 1000); + + // Finally create an OpenStreetMap URL + await createPersonStateDP(dpPerson._id, "urlMap", getOpenStreetMapUrl(Number(person.location.latitude), Number(person.location.longitude), 15)); +} + +/** + * Creates an object datapoint for a person + * @param {*} person The person object. + * @param {*} id Optional. Will be set to ${dpPrefix.people}.${person.id} if missing. + */ +function createPersonDP(person, id) { + let dpId; + if (typeof id === "undefined") + dpId = `${dpPrefix.people}.${person.id}`; + else + dpId = id; + + // myLogger.info(`PERSON ${dpId} --> ${person.firstName} ${person.lastName}`); + return createObjectDP(dpId, dpLife360Type.person, `${person.firstName} ${person.lastName}`, `${person.firstName} ${person.lastName}`); +} + +/** + * + * @param {*} idDP + * @param {*} state + * @param {*} val + */ +async function createPersonStateDP(idDP, state, val) { + let cRole = "state"; + const cType = typeof(val); + + switch (state) { + case "createdAt": + case "lastPositionAt": + cRole = "date"; + break; + + case "latitude": + cRole = "value.gps.latitude"; + break; + + case "longitude": + cRole = "value.gps.longitude"; + break; + + default: + switch(cType) { + case "string": + cRole = "text"; + break; + case "number": + cRole = "value"; + break; + } + break; + } + + const obj = await setStateReadOnlyValue(`${idDP}.${state}`, state, cType, cRole, val, true); + + return obj; +} + +/** + * ============================================================================ + * ---------------------------------------------------------------------------- + * Functions to publish Life360 circles to ioBroker + * ---------------------------------------------------------------------------- + * ============================================================================ + */ + +/** + * + * @param {*} circles + */ +function publishCircles(circles) { + for (let c in circles) { + try { + publishCircle(circles[c]); + } catch (error) { + myLogger.error(error); + } + } + + myLogger.debug(`Published ${circles.length} circle(s) to ioBroker.`); +} + +/** + * + * @param {*} circle + * @param {*} idParentDp + */ +async function publishCircle(circle, idParentDp) { + // Create an object for the circle + let dpId; + if (typeof idParentDp === "undefined") dpId = `${dpPrefix.circles}.${circle.id}`; else dpId = `${idParentDp}.${circle.id}`; + + // let dpId = `${dpPrefix.circles}.${circle.id}`; + // if (typeof idParentDp !== "undefined") dpId = `${idParentDp}.${circle.id}`; + + const dpCircle = await createCircleDP(circle, dpId); + + // Now set the circle's states. + createCircleStateDP(dpCircle._id, "id", circle.id); + createCircleStateDP(dpCircle._id, "name", circle.name); + createCircleStateDP(dpCircle._id, "memberCount", Number(circle.memberCount)); + createCircleStateDP(dpCircle._id, "createdAt", Number(circle.createdAt) * 1000); + + // Publish the circle's places including members' status. + publishCirclePlaces(dpCircle._id, circle); +} + +/** + * Creates an object datapoint for a circle + * @param {*} circle The circle object. + * @param {*} id Optional. Will be set to ${dpPrefix.circles}.${circle.id} if missing. + */ +function createCircleDP(circle, id) { + let dpId; + if (typeof id === "undefined") dpId = `${dpPrefix.circles}.${circle.id}`; else dpId = id; + + return createObjectDP(dpId, dpLife360Type.circle, `${circle.name}`, `Life360 circle for ${circle.name}`); +} + +/** + * + * @param {*} idDP + * @param {*} state + * @param {*} val + */ +async function createCircleStateDP(idDP, state, val) { + let cRole = "state"; + const cType = typeof(val); + + switch (state) { + case "createdAt": + cRole = "date"; + break; + + case "latitude": + cRole = "value.gps.latitude"; + break; + + case "longitude": + cRole = "value.gps.longitude"; + break; + + default: + switch(cType) { + case "string": + cRole = "text"; + break; + case "number": + cRole = "value"; + break; + } + break; + } + + const obj = await setStateReadOnlyValue(`${idDP}.${state}`, state, cType, cRole, val, true); + + return obj; +} + +/** + * + * @param {*} idDP + * @param {*} circle + */ +async function publishCirclePlaces(idDP, circle) { + const members = getPersons([ circle ]); + const places = getPlaces([ circle ]); + + // Are there any places? + if (places.length > 0) { + + // Create an object datapoint for the circle's places. + const idCirclePlaces = `${idDP}.places`; + if (createObjectDP(idCirclePlaces, "device", "Places", `${circle.name}'s places`)) { + // Places DP has been created. + for (let p in places) { + const place = places[p]; + // Create an object datapoint for the place. + const idPlace =`${idCirclePlaces}.${place.id}`; + + if (createObjectDP(idPlace, "device", place.name, `${place.name} (${circle.name})`)) { + // Place DP created. + let memberCount = 0; + + for (let m in members) { + const member = members[m]; + + // Has member entered the place? + let memberEntered = false; + if (Number(member.issues.disconnected) == 0) memberEntered = (member.location.sourceId === place.id); + + if (memberEntered) memberCount += 1; + + // Create an object datapoint for the member. + const idMember = `${idPlace}.${member.id}`; + if (createObjectDP(idMember, "channel", `${member.firstName} @ ${place.name}`, `${member.firstName} ${member.lastName} @ ${place.name} (${circle.name})`)) { + // Member DP created. + + // Indicate if member is present at the place. + await setStateReadOnlyValue(`${idMember}.isPresent`, "Present", "indicator", "boolean", memberEntered, true); + } + + } + + // Indicate the count of members present at the place. + await setStateReadOnlyValue(`${idPlace}.membersPresent`, `People @ ${place.name}`, "state", "number", memberCount, true); + } + } + } + } +} diff --git a/main.js b/main.js index bd8ee61..fbb729c 100644 --- a/main.js +++ b/main.js @@ -11,146 +11,164 @@ const utils = require("@iobroker/adapter-core"); // Load your modules here, e.g.: // const fs = require("fs"); +const life360Connector = require("./lib/life360CloudConnector"); +const life360DbConnector = require("./lib/life360DbConnector"); + class Life360 extends utils.Adapter { - /** + /** * @param {Partial} [options={}] */ - constructor(options) { - super({ - ...options, - name: "life360", - }); - this.on("ready", this.onReady.bind(this)); - this.on("objectChange", this.onObjectChange.bind(this)); - this.on("stateChange", this.onStateChange.bind(this)); - // this.on("message", this.onMessage.bind(this)); - this.on("unload", this.onUnload.bind(this)); - } - - /** + constructor(options) { + super({ + ...options, + name: "life360", + }); + this.on("ready", this.onReady.bind(this)); + this.on("objectChange", this.onObjectChange.bind(this)); + this.on("stateChange", this.onStateChange.bind(this)); + // this.on("message", this.onMessage.bind(this)); + this.on("unload", this.onUnload.bind(this)); + } + + /** * Is called when databases are connected and adapter received configuration. */ - async onReady() { - // Initialize your adapter here - - // The adapters config (in the instance object everything under the attribute "native") is accessible via - // this.config: - this.log.info("config option1: " + this.config.option1); - this.log.info("config option2: " + this.config.option2); - - /* + async onReady() { + // Initialize your adapter here + life360Connector.setAdapter(this); // Sets the adapter instance for the Life360 connector + life360DbConnector.setAdapter(this); + + // Setup polling Life360 cloud data + life360Connector.setupPolling(function(err, cloud_data) { + if (!err) { + // Pass the retrieved Life360 cloud data to the DB connector. + life360DbConnector.publishCloudData(err, cloud_data); + } + else { + // Error setting up polling. + } + }); + + // The adapters config (in the instance object everything under the attribute "native") is accessible via + // this.config: + // this.log.info("config option1: " + this.config.option1); + // this.log.info("config option2: " + this.config.option2); + + /* For every state in the system there has to be also an object of type state Here a simple template for a boolean variable named "testVariable" Because every adapter instance uses its own unique namespace variable names can't collide with other adapters variables */ - await this.setObjectAsync("testVariable", { - type: "state", - common: { - name: "testVariable", - type: "boolean", - role: "indicator", - read: true, - write: true, - }, - native: {}, - }); - - // in this template all states changes inside the adapters namespace are subscribed - this.subscribeStates("*"); - - /* - setState examples - you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd) - */ - // the variable testVariable is set to true as command (ack=false) - await this.setStateAsync("testVariable", true); - - // same thing, but the value is flagged "ack" - // ack should be always set to true if the value is received from or acknowledged from the target system - await this.setStateAsync("testVariable", { val: true, ack: true }); - - // same thing, but the state is deleted after 30s (getState will return null afterwards) - await this.setStateAsync("testVariable", { val: true, ack: true, expire: 30 }); - - // examples for the checkPassword/checkGroup functions - let result = await this.checkPasswordAsync("admin", "iobroker"); - this.log.info("check user admin pw ioboker: " + result); - - result = await this.checkGroupAsync("admin", "admin"); - this.log.info("check group user admin group admin: " + result); - } - - /** + // await this.setObjectAsync("testVariable", { + // type: "state", + // common: { + // name: "testVariable", + // type: "boolean", + // role: "indicator", + // read: true, + // write: true, + // }, + // native: {}, + // }); + + // // in this template all states changes inside the adapters namespace are subscribed + // this.subscribeStates("*"); + + // /* + // setState examples + // you will notice that each setState will cause the stateChange event to fire (because of above subscribeStates cmd) + // */ + // // the variable testVariable is set to true as command (ack=false) + // await this.setStateAsync("testVariable", true); + + // // same thing, but the value is flagged "ack" + // // ack should be always set to true if the value is received from or acknowledged from the target system + // await this.setStateAsync("testVariable", { val: true, ack: true }); + + // // same thing, but the state is deleted after 30s (getState will return null afterwards) + // await this.setStateAsync("testVariable", { val: true, ack: true, expire: 30 }); + + // // examples for the checkPassword/checkGroup functions + // let result = await this.checkPasswordAsync("admin", "iobroker"); + // this.log.info("check user admin pw ioboker: " + result); + + // result = await this.checkGroupAsync("admin", "admin"); + // this.log.info("check group user admin group admin: " + result); + } + + /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ - onUnload(callback) { - try { - this.log.info("cleaned everything up..."); - callback(); - } catch (e) { - callback(); - } - } - - /** + onUnload(callback) { + try { + life360Connector.disablePolling(); + life360Connector.disconnect(); + this.setState("info.connection", false); + this.log.info("cleaned everything up..."); + callback(); + } catch (e) { + callback(); + } + } + + /** * Is called if a subscribed object changes * @param {string} id * @param {ioBroker.Object | null | undefined} obj */ - onObjectChange(id, obj) { - if (obj) { - // The object was changed - this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`); - } else { - // The object was deleted - this.log.info(`object ${id} deleted`); - } - } - - /** + onObjectChange(id, obj) { + if (obj) { + // The object was changed + this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`); + } else { + // The object was deleted + this.log.info(`object ${id} deleted`); + } + } + + /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ - onStateChange(id, state) { - if (state) { - // The state was changed - this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); - } else { - // The state was deleted - this.log.info(`state ${id} deleted`); - } - } - - // /** - // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... - // * Using this method requires "common.message" property to be set to true in io-package.json - // * @param {ioBroker.Message} obj - // */ - // onMessage(obj) { - // if (typeof obj === "object" && obj.message) { - // if (obj.command === "send") { - // // e.g. send email or pushover or whatever - // this.log.info("send command"); - - // // Send response in callback if required - // if (obj.callback) this.sendTo(obj.from, obj.command, "Message received", obj.callback); - // } - // } - // } - + onStateChange(id, state) { + if (state) { + // The state was changed + this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); + } else { + // The state was deleted + this.log.info(`state ${id} deleted`); + } + } + + // /** + // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... + // * Using this method requires "common.message" property to be set to true in io-package.json + // * @param {ioBroker.Message} obj + // */ + // onMessage(obj) { + // if (typeof obj === "object" && obj.message) { + // if (obj.command === "send") { + // // e.g. send email or pushover or whatever + // this.log.info("send command"); + + // // Send response in callback if required + // if (obj.callback) this.sendTo(obj.from, obj.command, "Message received", obj.callback); + // } + // } + // } } // @ts-ignore parent is a valid property on module if (module.parent) { - // Export the constructor in compact mode - /** + // Export the constructor in compact mode + /** * @param {Partial} [options={}] */ - module.exports = (options) => new Life360(options); + module.exports = (options) => new Life360(options); } else { - // otherwise start the instance directly - new Life360(); -} \ No newline at end of file + // otherwise start the instance directly + new Life360(); +} diff --git a/package-lock.json b/package-lock.json index 9a66fa0..5412f16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -243,7 +243,6 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -461,6 +460,19 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -506,12 +518,27 @@ "async-done": "^1.2.2" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, "axios": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", @@ -600,12 +627,25 @@ } } }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -692,6 +732,11 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -923,6 +968,14 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -975,8 +1028,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cross-spawn": { "version": "6.0.5", @@ -1001,6 +1053,14 @@ "type": "^1.0.1" } }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1110,6 +1170,11 @@ } } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -1153,6 +1218,15 @@ "object.defaults": "^1.1.0" } }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -1473,8 +1547,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "extend-shallow": { "version": "3.0.2", @@ -1573,6 +1646,11 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -1588,14 +1666,12 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -1771,6 +1847,21 @@ "for-in": "^1.0.1" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -2366,6 +2457,14 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", @@ -2658,6 +2757,20 @@ "glogg": "^1.0.0" } }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2738,6 +2851,16 @@ "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", "dev": true }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3062,6 +3185,11 @@ "has-symbols": "^1.0.0" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -3107,6 +3235,11 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3123,11 +3256,20 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3135,6 +3277,11 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -3144,6 +3291,17 @@ "graceful-fs": "^4.1.6" } }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "just-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", @@ -3355,6 +3513,19 @@ "to-regex": "^3.0.2" } }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -3578,6 +3749,11 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -3897,6 +4073,11 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -3959,6 +4140,11 @@ "resolve": "^1.11.1" } }, + "psl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" + }, "pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", @@ -3983,8 +4169,12 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "read-pkg": { "version": "1.1.0", @@ -4151,6 +4341,33 @@ "remove-trailing-separator": "^1.1.0" } }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4249,8 +4466,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -4264,8 +4480,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { "version": "5.7.1", @@ -4581,6 +4796,22 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -4856,12 +5087,41 @@ "through2": "^2.0.3" } }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -5002,7 +5262,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } @@ -5025,6 +5284,11 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, "v8-compile-cache": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", @@ -5056,6 +5320,16 @@ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "vinyl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", diff --git a/package.json b/package.json index 072d47f..0d1a252 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "iobroker.life360", - "version": "0.0.1", + "version": "0.1.1", "description": "An ioBroker adapter for Life360.", "author": { - "name": "MiGoller", - "email": "1272642+MiGoller@users.noreply.github.com" + "name": "migoller", + "email": "goller.michael@gmail.com" }, "homepage": "https://github.com/MiGoller/ioBroker.life360", "license": "MIT", @@ -19,7 +19,9 @@ "url": "https://github.com/MiGoller/ioBroker.life360" }, "dependencies": { - "@iobroker/adapter-core": "^1.0.3" + "@iobroker/adapter-core": "^1.0.3", + "bluebird": "^3.5.5", + "request": "^2.88.0" }, "devDependencies": { "@iobroker/testing": "^1.2.5",