iobroker.google-sharedlocations2 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,11 @@ Copyright and trademark of Google are property of Google.
27
27
  Placeholder for the next version (at the beginning of the line):
28
28
  ### **WORK IN PROGRESS**
29
29
  -->
30
+ ### 0.1.0 (2026-02-02)
31
+ * (Garfonso) added: support for places
32
+ * (Garfonso) added: support for fences
33
+ * (Garfonso) try to prevent login as much as possible.
34
+
30
35
  ### 0.0.3 (2026-01-28)
31
36
  * (Garfonso) prevent login if no username and password is set
32
37
  * (Garfonso) fix tests
@@ -1,51 +1,135 @@
1
1
  {
2
2
  "i18n": true,
3
- "type": "panel",
4
- "items": {
5
- "_explanation": {
6
- "type": "staticText",
7
- "label": "Please provide the credentials of an ioBroker Google Account which you share your location with using the Google Maps App. It is strongly recommended to create a dedicated Google Account for this purpose.",
8
- "newLine": true,
9
- "xs": 12,
10
- "sm": 12,
11
- "md": 12,
12
- "lg": 12,
13
- "xl": 12
3
+ "type": "tabs",
4
+ "items": [
5
+ {
6
+ "type": "panel",
7
+ "label": "Google Settings",
8
+ "items": {
9
+ "_explanation": {
10
+ "type": "staticText",
11
+ "label": "Please provide the credentials of an ioBroker Google Account which you share your location with using the Google Maps App. It is strongly recommended to create a dedicated Google Account for this purpose.",
12
+ "newLine": true,
13
+ "xs": 12,
14
+ "sm": 12,
15
+ "md": 12,
16
+ "lg": 12,
17
+ "xl": 12
18
+ },
19
+ "googleUsername": {
20
+ "type": "text",
21
+ "label": "Username of ioBroker Google Account",
22
+ "newLine": true,
23
+ "xs": 12,
24
+ "sm": 12,
25
+ "md": 6,
26
+ "lg": 6,
27
+ "xl": 6
28
+ },
29
+ "googlePassword": {
30
+ "type": "password",
31
+ "label": "Password of ioBroker Google Account",
32
+ "visible": true,
33
+ "xs": 12,
34
+ "sm": 12,
35
+ "md": 6,
36
+ "lg": 6,
37
+ "xl": 6
38
+ },
39
+ "pollInterval": {
40
+ "type": "number",
41
+ "label": "Polling Interval (in seconds, at least 60 seconds)",
42
+ "default": 300,
43
+ "unit": "s",
44
+ "min": 60,
45
+ "max": 86400,
46
+ "step": 10,
47
+ "newLine": true,
48
+ "xs": 12,
49
+ "sm": 12,
50
+ "md": 6,
51
+ "lg": 6,
52
+ "xl": 6
53
+ }
54
+ }
14
55
  },
15
- "googleUsername": {
16
- "type": "text",
17
- "label": "Username of ioBroker Google Account",
18
- "newLine": true,
19
- "xs": 12,
20
- "sm": 12,
21
- "md": 6,
22
- "lg": 6,
23
- "xl": 6
24
- },
25
- "googlePassword": {
26
- "type": "password",
27
- "label": "Password of ioBroker Google Account",
28
- "visible": true,
29
- "xs": 12,
30
- "sm": 12,
31
- "md": 6,
32
- "lg": 6,
33
- "xl": 6
34
- },
35
- "pollInterval": {
36
- "type": "number",
37
- "label": "Polling Interval (in seconds, at least 60 seconds)",
38
- "default": 300,
39
- "unit": "s",
40
- "min": 60,
41
- "max": 86400,
42
- "step": 10,
43
- "newLine": true,
44
- "xs": 12,
45
- "sm": 12,
46
- "md": 6,
47
- "lg": 6,
48
- "xl": 6
56
+ {
57
+ "type": "panel",
58
+ "label": "Geofencing Settings",
59
+ "items": {
60
+ "placesInstance": {
61
+ "type": "instance",
62
+ "label": "Instance of iobroker.places to use (set to -1 to auto-detect)",
63
+ "adapter": "places",
64
+ "allowDeactivate": true,
65
+ "newLine": true,
66
+ "xs": 12,
67
+ "sm": 12,
68
+ "md": 6,
69
+ "lg": 6,
70
+ "xl": 6
71
+ },
72
+ "fences": {
73
+ "type": "table",
74
+ "clone": false,
75
+ "newLine": true,
76
+ "xs": 12,
77
+ "sm": 12,
78
+ "md": 12,
79
+ "lg": 12,
80
+ "xl": 12,
81
+ "label": "Fences, will create boolean states for each fence to indicate if user is within the fence",
82
+ "items": [
83
+ {
84
+ "type": "text",
85
+ "title": "Name of fence",
86
+ "width": "20%",
87
+ "attr": "name",
88
+ "sort": true,
89
+ "default": ""
90
+ },
91
+ {
92
+ "type": "number",
93
+ "title": "Latitude",
94
+ "width": "20%",
95
+ "attr": "latitude"
96
+ },
97
+ {
98
+ "type": "number",
99
+ "title": "Longitude",
100
+ "width": "20%",
101
+ "attr": "longitude"
102
+ },
103
+ {
104
+ "type": "number",
105
+ "title": "Radius (m)",
106
+ "width": "5%",
107
+ "attr": "radius",
108
+ "default": 100
109
+ },
110
+ {
111
+ "type": "selectSendTo",
112
+ "command": "getUsers",
113
+ "attr": "user",
114
+ "multiple": false,
115
+ "title": "Users",
116
+ "width": "20%",
117
+ "default": ""
118
+ },
119
+ {
120
+ "type": "text",
121
+ "title": "Fence ID",
122
+ "width": "15%",
123
+ "attr": "fenceId",
124
+ "defaultFunc": "data.name + data.user",
125
+ "alsoDependsOn": [
126
+ "name",
127
+ "user"
128
+ ]
129
+ }
130
+ ]
131
+ }
132
+ }
49
133
  }
50
- }
51
- }
134
+ ]
135
+ }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "google-sharedlocations2",
4
- "version": "0.0.3",
4
+ "version": "0.1.0",
5
5
  "news": {
6
+ "0.1.0": {
7
+ "en": "added: support for places\nadded: support for fences\ntry to prevent login as much as possible.",
8
+ "de": "hinzugefügt: unterstützung für places\nhinzugefügt: unterstützung für fences\nversuche, die anmeldung so weit wie möglich zu verhindern.",
9
+ "ru": "добавлено: поддержка мест\nдобавлено: поддержка заборов\nстарайтесь максимально предотвратить вход.",
10
+ "pt": "adicionado: suporte para locais\nadicionado: suporte para cercas\ntentar evitar o login o máximo possível.",
11
+ "nl": "toegevoegd: ondersteuning voor plaatsen\ntoegevoegd: steun voor hekken\nproberen zo veel mogelijk inloggen te voorkomen.",
12
+ "fr": "ajouté: soutien aux places\najouté: soutien aux clôtures\nessayer d'éviter la connexion autant que possible.",
13
+ "it": "aggiunto: supporto per i luoghi\naggiunto: supporto per recinzioni\ncercare di impedire il login il più possibile.",
14
+ "es": "añadido: apoyo a los lugares\nañadido: soporte para vallas\ntratar de prevenir la entrada tanto como sea posible.",
15
+ "pl": "dodane: wsparcie dla miejsc\ndodane: wsparcie dla ogrodzeń\nspróbuj zapobiec logowaniu jak najwięcej.",
16
+ "uk": "додано: підтримка місць\nдоданий: підтримка парканів\nнамагатися попередити логін якомога простіше.",
17
+ "zh-cn": "添加:对位置的支持\n添加:支持围栏\n尽量防止登录."
18
+ },
6
19
  "0.0.3": {
7
20
  "en": "prevent login if no username and password is set\nfix tests",
8
21
  "de": "verhindern login, wenn kein benutzername und passwort eingestellt ist\ntests repariert",
@@ -95,6 +108,7 @@
95
108
  "compact": true,
96
109
  "connectionType": "cloud",
97
110
  "dataSource": "poll",
111
+ "messagebox": true,
98
112
  "adminUI": {
99
113
  "config": "json"
100
114
  },
@@ -153,7 +167,9 @@
153
167
  "native": {
154
168
  "googleUsername": "",
155
169
  "googlePassword": "",
156
- "pollInterval": 60
170
+ "pollInterval": 60,
171
+ "placesInstance": "",
172
+ "fences": []
157
173
  },
158
174
  "protectedNative": [
159
175
  "googlePassword"
@@ -197,6 +213,15 @@
197
213
  "def": ""
198
214
  }
199
215
  },
216
+ {
217
+ "_id": "fences",
218
+ "type": "folder",
219
+ "common": {
220
+ "name": "Geofences",
221
+ "desc": "States for fences defined in the config."
222
+ },
223
+ "native": {}
224
+ },
200
225
  {
201
226
  "_id": "users",
202
227
  "type": "folder",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.google-sharedlocations2",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "description": "Share your location with iobroker via google maps.",
5
5
  "author": {
6
6
  "name": "Garfonso",
@@ -0,0 +1,220 @@
1
+ import axios from 'axios';
2
+ import type { GoogleSharedlocations2 } from '../main';
3
+ import puppeteer from 'puppeteer';
4
+ import type { Browser } from 'puppeteer';
5
+
6
+ /**
7
+ * Helper class to manage Google cookies.
8
+ */
9
+ export class Cookie {
10
+ currentCookie: string;
11
+ username?: string;
12
+ password?: string;
13
+ adapter: GoogleSharedlocations2;
14
+ log;
15
+ private browser: Browser | null = null;
16
+
17
+ /**
18
+ * Construct cookie helper
19
+ *
20
+ * @param adapter - adapter instance
21
+ */
22
+ constructor(adapter: GoogleSharedlocations2) {
23
+ this.currentCookie = '';
24
+ this.username = '';
25
+ this.password = '';
26
+ this.adapter = adapter;
27
+ this.log = adapter.log;
28
+ }
29
+
30
+ /**
31
+ * Initialize the cookie helper by loading the cookie from state.
32
+ */
33
+ async init(): Promise<void> {
34
+ this.username = this.adapter.config.googleUsername;
35
+ this.password = this.adapter.config.googlePassword;
36
+ try {
37
+ const state = await this.adapter.getStateAsync('info.currentCookies');
38
+ if (state && state.val && typeof state.val === 'string') {
39
+ this.currentCookie = state.val;
40
+ this.log?.debug('Loaded cookie from state.');
41
+ } else {
42
+ this.currentCookie = '';
43
+ this.log?.debug('No cookie found in state, trying to log in to get new one.');
44
+ await this.loginToGetNewCookies();
45
+ }
46
+ } catch (err: any) {
47
+ this.log?.error(`Error loading cookie from state: ${err}`);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Store the current cookie in an iobroker state.
53
+ */
54
+ async storeCookie(): Promise<void> {
55
+ try {
56
+ await this.adapter.setStateAsync('info.currentCookies', this.currentCookie, true);
57
+ } catch (err: any) {
58
+ this.log?.error(`Error storing cookie: ${err}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Augment the current cookie with data from the 'set-cookie' header.
64
+ *
65
+ * @param headers - HTTP headers of axios response
66
+ */
67
+ async augmentCookieFromHeader(headers: Record<string, any>): Promise<void> {
68
+ if (headers['set-cookie'] && headers['set-cookie'].length) {
69
+ this.log?.debug('New header received.');
70
+ const oldLength = this.currentCookie.length;
71
+ const cookies = this.currentCookie.split('; ').map(c => c.split('='));
72
+
73
+ //split old cookie and new cookie. Update single values.
74
+ for (const header of headers['set-cookie']) {
75
+ const incomingCookies = header.split('; ');
76
+ for (const cookie of incomingCookies) {
77
+ const [name, value] = cookie.split('=');
78
+ const cIndex = cookies.findIndex(c => c[0] === name);
79
+ if (cIndex < 0) {
80
+ cookies.push([name, value]); //add
81
+ } else {
82
+ cookies[cIndex][1] = value; //update
83
+ }
84
+ }
85
+ }
86
+
87
+ this.currentCookie = cookies.map(cv => cv.join('=')).join('; ');
88
+ this.log?.debug(`Cookie updated. Length: ${oldLength} -> ${this.currentCookie.length}`);
89
+ return this.storeCookie();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Improve the current cookie by making a request to Google My Account page.
95
+ */
96
+ async improveCookie(): Promise<void> {
97
+ //see https://github.com/costastf/locationsharinglib/blob/master/locationsharinglib/locationsharinglib.py#L105
98
+ const options = {
99
+ url: 'https://myaccount.google.com/?hl=en',
100
+ headers: {
101
+ Cookie: this.currentCookie,
102
+ },
103
+ method: 'get',
104
+ };
105
+
106
+ try {
107
+ const response = await axios(options);
108
+
109
+ if (response.status !== 200) {
110
+ this.log?.error(`Failed improving cookie: ${response.status}`);
111
+ } else {
112
+ await this.augmentCookieFromHeader(response.headers);
113
+ }
114
+ } catch (err: any) {
115
+ this.log?.error(err);
116
+ this.log?.info('Connection to google maps failure.');
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Login to Google using puppeteer to get new cookies.
122
+ */
123
+ async loginToGetNewCookies(): Promise<boolean> {
124
+ try {
125
+ if (this.browser) {
126
+ this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
127
+ return false;
128
+ }
129
+ if (!this.username || !this.password) {
130
+ this.log.warn('Google username or password not set in adapter configuration. Can not login.');
131
+ return false;
132
+ }
133
+
134
+ this.log.info('Trying to login to Google to get new cookies.');
135
+ //testing puppeteer:
136
+ this.log.debug('Starting browser.');
137
+ this.browser = await puppeteer.launch({
138
+ headless: true,
139
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'],
140
+ ignoreDefaultArgs: ['--enable-automation'], //hide automation flag, did not help.
141
+ });
142
+ this.log.debug('browser started, opening new page.');
143
+ const page = await this.browser.newPage();
144
+
145
+ //hide puppeteer automation flag
146
+ await page.evaluateOnNewDocument(() => {
147
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
148
+ });
149
+ await page.setUserAgent({
150
+ userAgent:
151
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
152
+ });
153
+
154
+ this.log.debug('going to google login page.');
155
+ await page.goto(
156
+ 'https://accounts.google.com/ServiceLogin?hl=de&continue=https://www.google.com/maps&gae=cb-eomtm',
157
+ {
158
+ waitUntil: 'networkidle2',
159
+ timeout: 60000,
160
+ },
161
+ );
162
+
163
+ this.log.debug('filling in username and clicking next.');
164
+ await page.locator('#identifierId').fill(this.username);
165
+ //is this enough, or do we need to search button in this div?
166
+ await page.locator('#identifierNext').click();
167
+ //waiting for #password fails in headles.. :-(
168
+ await page.waitForNetworkIdle({ idleTime: 2000 });
169
+
170
+ this.log.debug('filling in password and clicking next.');
171
+ //do we need to wait until page is loaded / rendered here?
172
+ await page.locator('input[type="password"]').fill(this.password);
173
+ this.log.debug('clicking password next button.');
174
+ await page.locator('#passwordNext').click();
175
+ //await page.waitForNetworkIdle({ idleTime: 2000 }); -> does never happen in headless.. :-/
176
+ await new Promise(resolve => setTimeout(resolve, 3000));
177
+
178
+ await page.goto('https://www.google.com/maps');
179
+ this.log.debug('getting cookies.');
180
+ //using deprecated function, but browser.cookies just does not work...???
181
+ const cookies = await page.cookies();
182
+
183
+ this.currentCookie = cookies
184
+ .filter(c => c.domain.includes('google'))
185
+ .map(c => `${c.name}=${c.value}`)
186
+ .join('; ');
187
+ //this.log.debug(this._cookies);
188
+ //console.log(this._cookies);
189
+ await this.browser.close();
190
+ if (this.currentCookie.length < 50) {
191
+ this.log.warn('Cookie string seems too short, login probably failed!');
192
+ } else {
193
+ this.log.info('Obtained new cookies from Google login.');
194
+ await this.storeCookie();
195
+ }
196
+ this.browser = null;
197
+ return true;
198
+ } catch (e) {
199
+ this.log.error(`Error in puppeteer: ${(e as Error).message}`);
200
+ }
201
+ return false;
202
+ }
203
+
204
+ /**
205
+ * Clean up on unload.
206
+ */
207
+ async cleanUp(): Promise<void> {
208
+ if (this.browser) {
209
+ await this.browser.close();
210
+ this.browser = null;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Check if the current cookie is valid.
216
+ */
217
+ isValid(): boolean {
218
+ return this.currentCookie.length > 50;
219
+ }
220
+ }
@@ -0,0 +1,62 @@
1
+ import type { User } from './User';
2
+
3
+ /**
4
+ * Geofence class
5
+ */
6
+ export class Fence {
7
+ name: string;
8
+ lat: number;
9
+ long: number;
10
+ radius: number;
11
+ user: string;
12
+ fenceId: string;
13
+ valid: boolean = true;
14
+
15
+ /**
16
+ * Constructor
17
+ *
18
+ * @param name of the fence
19
+ * @param lat of the fence center
20
+ * @param long of the fence center
21
+ * @param radius in meters
22
+ * @param user user id to check
23
+ * @param fenceId iobroker state id to set
24
+ */
25
+ constructor(name: string, lat: number, long: number, radius: number, user: string, fenceId: string) {
26
+ this.name = name;
27
+ this.lat = lat;
28
+ this.long = long;
29
+ this.radius = radius;
30
+ this.user = user;
31
+ this.fenceId = fenceId;
32
+ this.valid = !!(lat && long && radius > 0 && user && fenceId);
33
+ }
34
+
35
+ private toRadians(degrees: number): number {
36
+ return degrees * (Math.PI / 180);
37
+ }
38
+
39
+ private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
40
+ const R = 6371000; // Earth radius in meters
41
+ const dLat = this.toRadians(lat2 - lat1);
42
+ const dLon = this.toRadians(lon2 - lon1);
43
+ const a =
44
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
45
+ Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
46
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
47
+ return R * c;
48
+ }
49
+
50
+ /**
51
+ * Check if a point is inside the fence.
52
+ *
53
+ * @param user to check
54
+ */
55
+ isInsideFence(user: User): boolean {
56
+ if (this.valid && user.id && user.lat && user.long) {
57
+ const distance = this.haversineDistance(this.lat, this.long, user.lat, user.long);
58
+ return distance <= this.radius;
59
+ }
60
+ return false;
61
+ }
62
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * User class representing a user with location and info data.
3
+ */
4
+ export class User {
5
+ id: string | null;
6
+ name?: string;
7
+ photoURL?: string;
8
+ lat?: number;
9
+ long?: number;
10
+ address?: string;
11
+ battery?: number;
12
+ timestamp?: number;
13
+ accuracy?: number;
14
+
15
+ /**
16
+ * Creates a User instance from location data.
17
+ *
18
+ * @param locationData - Array containing user location and info data
19
+ */
20
+ constructor(locationData: Array<any>) {
21
+ this.id = null;
22
+ if (locationData && Array.isArray(locationData)) {
23
+ // locationData present
24
+ if (locationData[0] && locationData[0][0]) {
25
+ this.id = locationData[0][0];
26
+ }
27
+ if (locationData[0] && locationData[0][1]) {
28
+ this.photoURL = locationData[0][1];
29
+ }
30
+ if (locationData[0] && locationData[0][3]) {
31
+ this.name = locationData[0][3];
32
+ }
33
+ if (locationData[1] && locationData[1][1] && locationData[1][1][2]) {
34
+ this.lat = locationData[1][1][2];
35
+ }
36
+ if (locationData[1] && locationData[1][1] && locationData[1][1][1]) {
37
+ this.long = locationData[1][1][1];
38
+ }
39
+ if (locationData[1] && locationData[1][4]) {
40
+ this.address = locationData[1][4];
41
+ }
42
+ if (locationData[13] && locationData[13][1]) {
43
+ this.battery = locationData[13][1];
44
+ }
45
+ if (locationData[1] && locationData[1][2]) {
46
+ this.timestamp = locationData[1][2];
47
+ }
48
+ if (locationData[1] && locationData[1][3]) {
49
+ this.accuracy = locationData[1][3];
50
+ }
51
+ }
52
+ }
53
+ }
@@ -7,6 +7,15 @@ declare global {
7
7
  googleUsername: string;
8
8
  googlePassword: string;
9
9
  pollInterval: number;
10
+ placesInstance: string;
11
+ fences: Array<{
12
+ name: string;
13
+ latitude: number;
14
+ longitude: number;
15
+ radius: number;
16
+ user: string;
17
+ fenceId: string;
18
+ }>;
10
19
  }
11
20
  }
12
21
  }
package/src/main.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  // you need to create an adapter
7
7
  import * as utils from '@iobroker/adapter-core';
8
8
 
9
- import puppeteer from 'puppeteer';
10
- import type { Browser } from 'puppeteer';
9
+ import { User } from './lib/User';
10
+ import { Fence } from './lib/Fence';
11
+ import { Cookie } from './lib/Cookie';
11
12
  import axios from 'axios';
12
13
 
13
14
  //used to test timeout against
@@ -16,13 +17,22 @@ const MAX_INT32 = 2 ** 31 - 1; // 2147483647 (hex 0x7FFFFFFF)
16
17
  // Load your modules here, e.g.:
17
18
  // import * as fs from 'fs';
18
19
 
19
- class GoogleSharedlocations2 extends utils.Adapter {
20
- _cookies: string | null = null;
20
+ /**
21
+ * The adapter class
22
+ */
23
+ export class GoogleSharedlocations2 extends utils.Adapter {
21
24
  _pollTimeout: ioBroker.Timeout | undefined;
22
25
  _pollInterval: number = 300;
23
26
  _successFullPolls: number = 1; // let us try a relogin at start, if cookie does not work.
24
- _browser: Browser | null = null;
27
+ _users: Record<string, User> = {};
28
+ fences: Fence[] = [];
29
+ cookie: Cookie;
25
30
 
31
+ /**
32
+ * Creates an instance of the adapter.
33
+ *
34
+ * @param options - adapter options
35
+ */
26
36
  public constructor(options: Partial<utils.AdapterOptions> = {}) {
27
37
  super({
28
38
  ...options,
@@ -31,8 +41,9 @@ class GoogleSharedlocations2 extends utils.Adapter {
31
41
  this.on('ready', this.onReady.bind(this));
32
42
  this.on('stateChange', this.onStateChange.bind(this));
33
43
  // this.on('objectChange', this.onObjectChange.bind(this));
34
- // this.on('message', this.onMessage.bind(this));
44
+ this.on('message', this.onMessage.bind(this));
35
45
  this.on('unload', this.onUnload.bind(this));
46
+ this.cookie = new Cookie(this);
36
47
  }
37
48
 
38
49
  /**
@@ -43,15 +54,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
43
54
 
44
55
  // Reset the connection indicator during startup
45
56
  await this.setState('info.connection', false, true);
46
- this._cookies = ((await this.getStateAsync('info.currentCookies'))?.val as string) || null;
47
- if (!this._cookies) {
48
- if (!this.config.googleUsername || !this.config.googlePassword) {
49
- this.log.error('Google username or password not set in adapter configuration!');
50
- return;
51
- }
52
- await this.loginToGetNewCookies();
53
- }
54
-
57
+ await this.cookie.init();
55
58
  await this.subscribeStatesAsync('info.currentCookies');
56
59
 
57
60
  //sanitize polling interval:
@@ -66,20 +69,67 @@ class GoogleSharedlocations2 extends utils.Adapter {
66
69
  this._pollInterval = MAX_INT32;
67
70
  }
68
71
 
72
+ //read fences:
73
+ for (const fenceConfig of this.config.fences || []) {
74
+ const fence = new Fence(
75
+ fenceConfig.name,
76
+ fenceConfig.latitude,
77
+ fenceConfig.longitude,
78
+ fenceConfig.radius,
79
+ fenceConfig.user,
80
+ fenceConfig.fenceId,
81
+ );
82
+ if (fence.valid) {
83
+ this.fences.push(fence);
84
+ await this.setObjectNotExistsAsync(`fences.${fence.fenceId}`, {
85
+ type: 'state',
86
+ common: {
87
+ name: `${fence.name}`,
88
+ type: 'boolean',
89
+ read: true,
90
+ write: false,
91
+ role: 'sensor',
92
+ },
93
+ native: {},
94
+ });
95
+ } else {
96
+ this.log.warn(`Fence ${fenceConfig.name} is not valid and will be ignored.`);
97
+ }
98
+ }
99
+ //clear old fences:
100
+ const adapterObjects = await this.getAdapterObjectsAsync();
101
+ for (const objId of Object.keys(adapterObjects)) {
102
+ if (objId.startsWith(`${this.namespace}.fences.`) || objId.startsWith('fences.')) {
103
+ const fenceId = objId.split('.').pop() || '';
104
+ const found = this.fences.find(f => f.fenceId === fenceId);
105
+ if (!found) {
106
+ this.log.info(`Deleting old fence state ${objId} as it is not in configuration anymore.`);
107
+ await this.delObjectAsync(objId);
108
+ }
109
+ }
110
+ }
111
+
69
112
  //start polling positions
70
113
  this.pollPositions();
71
- if (this._cookies) {
114
+ if (this.cookie.isValid()) {
72
115
  await this.sendRequest();
73
116
  }
74
117
  }
75
118
 
76
119
  private pollPositions(): void {
77
120
  this._pollTimeout = this.setTimeout(async () => {
78
- if (!this._cookies) {
121
+ if (!this.cookie.isValid()) {
79
122
  this.log.debug('Cannot poll positions, no cookies available!');
80
123
  } else {
81
124
  this.log.debug('Polling positions with current cookies.');
125
+ const lastSuccessPolls = this._successFullPolls;
82
126
  await this.sendRequest();
127
+ if (this._successFullPolls > 0 && lastSuccessPolls !== this._successFullPolls) {
128
+ if (this._successFullPolls % 10 === 0) {
129
+ //try to get some more headers from google:
130
+ await this.cookie.improveCookie();
131
+ }
132
+ }
83
133
  }
84
134
  //schedule next poll
85
135
  return this.pollPositions();
@@ -87,7 +137,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
87
137
  }
88
138
 
89
139
  private async sendRequest(): Promise<void> {
90
- if (!this._cookies) {
140
+ if (!this.cookie.isValid()) {
91
141
  this.log.error('Cannot send request, no cookies available!');
92
142
  await this.setState('info.connection', false, true);
93
143
  return;
@@ -99,7 +149,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
99
149
  method: 'GET',
100
150
  url: 'https://www.google.com/maps/rpc/locationsharing/read',
101
151
  headers: {
102
- Cookie: this._cookies,
152
+ Cookie: this.cookie.currentCookie,
103
153
  },
104
154
  params: {
105
155
  authuser: 2,
@@ -120,71 +170,36 @@ class GoogleSharedlocations2 extends utils.Adapter {
120
170
  this._successFullPolls += 1;
121
171
  await this.setState('info.connection', true, true);
122
172
  for (const location of locations) {
123
- await this.fillIntoObjects(location);
173
+ const user = new User(location);
174
+ if (user.id) {
175
+ this._users[user.id] = user;
176
+ await this.fillIntoObjects(user);
177
+ await this.notifyPlaces(user);
178
+ await this.checkFences(user);
179
+ }
124
180
  }
125
181
  } else {
126
182
  this.log.info('No shared locations found in the response, probably not logged in.');
127
183
  if (this._successFullPolls > 0) {
128
184
  //try to get new cookie:
129
- await this.loginToGetNewCookies();
185
+ this._successFullPolls = 0;
186
+ await this.cookie.loginToGetNewCookies();
130
187
  }
131
188
  }
132
189
  } catch (e) {
133
190
  this.log.error(`Error during request: ${(e as Error).message}`);
134
191
  if (this._successFullPolls > 0) {
135
192
  //try to get new cookie:
136
- await this.loginToGetNewCookies();
193
+ this._successFullPolls = 0;
194
+ await this.cookie.loginToGetNewCookies();
137
195
  }
138
196
  }
139
197
  }
140
198
 
141
- private async fillIntoObjects(locationData: any): Promise<void> {
199
+ private async fillIntoObjects(user: User): Promise<void> {
142
200
  try {
143
- const user = {
144
- id: undefined,
145
- photoURL: undefined,
146
- name: undefined,
147
- lat: undefined,
148
- long: undefined,
149
- address: undefined,
150
- battery: undefined,
151
- timestamp: undefined,
152
- accuracy: undefined,
153
- };
154
-
155
- if (locationData && Array.isArray(locationData)) {
156
- // locationData present
157
- if (locationData[0] && locationData[0][0]) {
158
- user.id = locationData[0][0];
159
- }
160
- if (locationData[0] && locationData[0][1]) {
161
- user.photoURL = locationData[0][1];
162
- }
163
- if (locationData[0] && locationData[0][3]) {
164
- user.name = locationData[0][3];
165
- }
166
- if (locationData[1] && locationData[1][1] && locationData[1][1][2]) {
167
- user.lat = locationData[1][1][2];
168
- }
169
- if (locationData[1] && locationData[1][1] && locationData[1][1][1]) {
170
- user.long = locationData[1][1][1];
171
- }
172
- if (locationData[1] && locationData[1][4]) {
173
- user.address = locationData[1][4];
174
- }
175
- if (locationData[13] && locationData[13][1]) {
176
- user.battery = locationData[13][1];
177
- }
178
- if (locationData[1] && locationData[1][2]) {
179
- user.timestamp = locationData[1][2];
180
- }
181
- if (locationData[1] && locationData[1][3]) {
182
- user.accuracy = locationData[1][3];
183
- }
184
- }
185
-
186
201
  if (user.id) {
187
- const basepath = `users.${user.id as string}`;
202
+ const basepath = `users.${user.id}`;
188
203
  const deviceObj = {
189
204
  _id: basepath,
190
205
  type: 'device',
@@ -307,84 +322,28 @@ class GoogleSharedlocations2 extends utils.Adapter {
307
322
  }
308
323
  }
309
324
 
310
- private async loginToGetNewCookies(): Promise<void> {
311
- try {
312
- if (this._browser) {
313
- this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
314
- return;
315
- }
316
- if (!this.config.googleUsername || !this.config.googlePassword) {
317
- this.log.warn('Google username or password not set in adapter configuration. Can not login.');
318
- return;
319
- }
320
-
321
- this.log.info('Trying to login to Google to get new cookies.');
322
- this._successFullPolls = 0;
323
-
324
- //testing puppeteer:
325
- this.log.debug('Starting browser.');
326
- this._browser = await puppeteer.launch({
327
- headless: true,
328
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'],
329
- ignoreDefaultArgs: ['--enable-automation'], //hide automation flag, did not help.
330
- });
331
- this.log.debug('browser started, opening new page.');
332
- const page = await this._browser.newPage();
333
-
334
- //hide puppeteer automation flag
335
- await page.evaluateOnNewDocument(() => {
336
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
337
- });
338
- await page.setUserAgent({
339
- userAgent:
340
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
325
+ private async notifyPlaces(user: User): Promise<void> {
326
+ if (this.config.placesInstance && user.id && user.lat && user.long) {
327
+ await this.sendToAsync(this.config.placesInstance, {
328
+ user: user.name,
329
+ latitude: user.lat,
330
+ longitude: user.long,
331
+ timestamp: user.timestamp || Date.now(),
332
+ address: user.address,
341
333
  });
334
+ }
335
+ }
342
336
 
343
- this.log.debug('going to google login page.');
344
- await page.goto(
345
- 'https://accounts.google.com/ServiceLogin?hl=de&continue=https://www.google.com/maps&gae=cb-eomtm',
346
- {
347
- waitUntil: 'networkidle2',
348
- timeout: 60000,
349
- },
350
- );
351
-
352
- this.log.debug('filling in username and clicking next.');
353
- await page.locator('#identifierId').fill(this.config.googleUsername);
354
- //is this enough, or do we need to search button in this div?
355
- await page.locator('#identifierNext').click();
356
- //waiting for #password fails in headles.. :-(
357
- await page.waitForNetworkIdle({ idleTime: 2000 });
358
-
359
- this.log.debug('filling in password and clicking next.');
360
- //do we need to wait until page is loaded / rendered here?
361
- await page.locator('input[type="password"]').fill(this.config.googlePassword);
362
- this.log.debug('clicking password next button.');
363
- await page.locator('#passwordNext').click();
364
- //await page.waitForNetworkIdle({ idleTime: 2000 }); -> does never happen in headless.. :-/
365
- await new Promise(resolve => setTimeout(resolve, 3000));
366
-
367
- await page.goto('https://www.google.com/maps');
368
- this.log.debug('getting cookies.');
369
- //using deprecated function, but browser.cookies just does not work...???
370
- const cookies = await page.cookies();
371
-
372
- this._cookies = cookies
373
- .filter(c => c.domain.includes('google'))
374
- .map(c => `${c.name}=${c.value}`)
375
- .join('; ');
376
- //this.log.debug(this._cookies);
377
- //console.log(this._cookies);
378
- await this._browser.close();
379
- if (this._cookies.length < 50) {
380
- this.log.warn('Cookie string seems too short, login probably failed!');
381
- } else {
382
- this.log.info('Obtained new cookies from Google login.');
383
- await this.setState('info.currentCookies', { val: this._cookies, ack: true });
337
+ private async checkFences(user: User): Promise<void> {
338
+ for (const fence of this.fences) {
339
+ if (fence.valid && fence.user === user.id) {
340
+ const inside = fence.isInsideFence(user);
341
+ await this.setStateChangedAsync(`fences.${fence.fenceId}`, {
342
+ val: inside,
343
+ ts: user.timestamp,
344
+ ack: true,
345
+ });
384
346
  }
385
- this._browser = null;
386
- } catch (e) {
387
- this.log.error(`Error in puppeteer: ${(e as Error).message}`);
388
347
  }
389
348
  }
390
349
 
@@ -393,7 +352,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
393
352
  *
394
353
  * @param callback - Callback function
395
354
  */
396
- private onUnload(callback: () => void): void {
355
+ private async onUnload(callback: () => void): Promise<void> {
397
356
  try {
398
357
  // Here you must clear all timeouts or intervals that may still be active
399
358
  // clearTimeout(timeout1);
@@ -403,13 +362,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
403
362
  if (this._pollTimeout) {
404
363
  clearTimeout(this._pollTimeout);
405
364
  }
406
- if (this._browser) {
407
- //ignore results here.
408
- this._browser
409
- .close()
410
- .then(() => {})
411
- .catch(() => {});
412
- }
365
+ await this.cookie.cleanUp();
413
366
  callback();
414
367
  } catch (error) {
415
368
  this.log.error(`Error during unloading: ${(error as Error).message}`);
@@ -442,35 +395,46 @@ class GoogleSharedlocations2 extends utils.Adapter {
442
395
  if (id.endsWith('info.currentCookies') && state && !state.ack) {
443
396
  if (state.val === '') {
444
397
  this.log.info('Current cookies state was cleared, trying to obtain new cookies.');
445
- this._cookies = null;
446
- await this.loginToGetNewCookies();
447
- if (this._cookies) {
398
+ this._successFullPolls = 0;
399
+ await this.cookie.loginToGetNewCookies();
400
+ if (this.cookie.isValid()) {
448
401
  await this.sendRequest();
449
402
  }
450
403
  } else {
451
404
  this.log.info(
452
405
  'Current cookies state was changed from outside the adapter, updating internal cookie store.',
453
406
  );
454
- this._cookies = state.val as string;
407
+ this.cookie.currentCookie = state.val as string;
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
414
+ * Using this method requires "common.messagebox" property to be set to true in io-package.json
415
+ *
416
+ * @param obj - message object
417
+ */
418
+ private onMessage(obj: ioBroker.Message): void {
419
+ this.log.debug(`Received ${obj?.command} message`);
420
+ if (obj?.command === 'getUsers') {
421
+ this.log.debug('Received getUsers message');
422
+ // Send response in callback if required
423
+ if (obj.callback) {
424
+ try {
425
+ const result = Object.values(this._users).map(user => ({
426
+ value: user.id,
427
+ label: user.name || user.id,
428
+ }));
429
+ this.log.debug(`Result: ${JSON.stringify(result)}`);
430
+ this.sendTo(obj.from, obj.command, result, obj.callback);
431
+ } catch (e) {
432
+ this.log.error(`Error processing getUsers message: ${(e as Error).message}`);
433
+ this.sendTo(obj.from, obj.command, [], obj.callback);
434
+ }
455
435
  }
456
436
  }
457
437
  }
458
- // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
459
- // /**
460
- // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
461
- // * Using this method requires "common.messagebox" property to be set to true in io-package.json
462
- // */
463
- //
464
- // private onMessage(obj: ioBroker.Message): void {
465
- // if (typeof obj === 'object' && obj.message) {
466
- // if (obj.command === 'send') {
467
- // // e.g. send email or pushover or whatever
468
- // this.log.info('send command');
469
- // // Send response in callback if required
470
- // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback);
471
- // }
472
- // }
473
- // }
474
438
  }
475
439
  //if (require.main !== module) {
476
440
  // Export the constructor in compact mode