iobroker.google-sharedlocations2 0.0.2 → 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,15 @@ 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
+
35
+ ### 0.0.3 (2026-01-28)
36
+ * (Garfonso) prevent login if no username and password is set
37
+ * (Garfonso) fix tests
38
+
30
39
  ### 0.0.2 (2026-01-28)
31
40
  * (Garfonso) store password encrypted
32
41
 
@@ -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,34 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "google-sharedlocations2",
4
- "version": "0.0.2",
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
+ },
19
+ "0.0.3": {
20
+ "en": "prevent login if no username and password is set\nfix tests",
21
+ "de": "verhindern login, wenn kein benutzername und passwort eingestellt ist\ntests repariert",
22
+ "ru": "предотвратить вход в систему, если имя пользователя и пароль не установлены\nисправление",
23
+ "pt": "impedir o login se nenhum nome de usuário e senha estiver definido\ncorrigir os testes",
24
+ "nl": "login voorkomen als er geen gebruikersnaam en wachtwoord is ingesteld\nvastleggen van tests",
25
+ "fr": "empêcher le login si aucun nom d'utilisateur et mot de passe n'est défini\nessais de correction",
26
+ "it": "impedire il login se non viene impostato nessun nome utente e password\ntest di correzione",
27
+ "es": "previene el inicio de sesión si no se establece el nombre de usuario y la contraseña\npruebas de reparación",
28
+ "pl": "zapobiec logowaniu, jeśli nie jest ustawiona nazwa użytkownika i hasło\nbadania naprawcze",
29
+ "uk": "заборонити логін, якщо не встановлено ім’я користувача та пароль\nфіксувати тести",
30
+ "zh-cn": "如果没有设置用户名和密码, 请防止登录\n固定测试"
31
+ },
6
32
  "0.0.2": {
7
33
  "en": "store password encrypted",
8
34
  "de": "speicher passwort verschlüsselt",
@@ -82,6 +108,7 @@
82
108
  "compact": true,
83
109
  "connectionType": "cloud",
84
110
  "dataSource": "poll",
111
+ "messagebox": true,
85
112
  "adminUI": {
86
113
  "config": "json"
87
114
  },
@@ -140,7 +167,9 @@
140
167
  "native": {
141
168
  "googleUsername": "",
142
169
  "googlePassword": "",
143
- "pollInterval": 60
170
+ "pollInterval": 60,
171
+ "placesInstance": "",
172
+ "fences": []
144
173
  },
145
174
  "protectedNative": [
146
175
  "googlePassword"
@@ -184,6 +213,15 @@
184
213
  "def": ""
185
214
  }
186
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
+ },
187
225
  {
188
226
  "_id": "users",
189
227
  "type": "folder",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.google-sharedlocations2",
3
- "version": "0.0.2",
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,7 +6,9 @@
6
6
  // you need to create an adapter
7
7
  import * as utils from '@iobroker/adapter-core';
8
8
 
9
- import puppeteer, { type Browser } from 'puppeteer';
9
+ import { User } from './lib/User';
10
+ import { Fence } from './lib/Fence';
11
+ import { Cookie } from './lib/Cookie';
10
12
  import axios from 'axios';
11
13
 
12
14
  //used to test timeout against
@@ -15,13 +17,22 @@ const MAX_INT32 = 2 ** 31 - 1; // 2147483647 (hex 0x7FFFFFFF)
15
17
  // Load your modules here, e.g.:
16
18
  // import * as fs from 'fs';
17
19
 
18
- class GoogleSharedlocations2 extends utils.Adapter {
19
- _cookies: string | null = null;
20
+ /**
21
+ * The adapter class
22
+ */
23
+ export class GoogleSharedlocations2 extends utils.Adapter {
20
24
  _pollTimeout: ioBroker.Timeout | undefined;
21
25
  _pollInterval: number = 300;
22
26
  _successFullPolls: number = 1; // let us try a relogin at start, if cookie does not work.
23
- _browser: Browser | null = null;
27
+ _users: Record<string, User> = {};
28
+ fences: Fence[] = [];
29
+ cookie: Cookie;
24
30
 
31
+ /**
32
+ * Creates an instance of the adapter.
33
+ *
34
+ * @param options - adapter options
35
+ */
25
36
  public constructor(options: Partial<utils.AdapterOptions> = {}) {
26
37
  super({
27
38
  ...options,
@@ -30,8 +41,9 @@ class GoogleSharedlocations2 extends utils.Adapter {
30
41
  this.on('ready', this.onReady.bind(this));
31
42
  this.on('stateChange', this.onStateChange.bind(this));
32
43
  // this.on('objectChange', this.onObjectChange.bind(this));
33
- // this.on('message', this.onMessage.bind(this));
44
+ this.on('message', this.onMessage.bind(this));
34
45
  this.on('unload', this.onUnload.bind(this));
46
+ this.cookie = new Cookie(this);
35
47
  }
36
48
 
37
49
  /**
@@ -42,15 +54,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
42
54
 
43
55
  // Reset the connection indicator during startup
44
56
  await this.setState('info.connection', false, true);
45
- this._cookies = ((await this.getStateAsync('info.currentCookies'))?.val as string) || null;
46
- if (!this._cookies) {
47
- if (!this.config.googleUsername || !this.config.googlePassword) {
48
- this.log.error('Google username or password not set in adapter configuration!');
49
- return;
50
- }
51
- await this.loginToGetNewCookies();
52
- }
53
-
57
+ await this.cookie.init();
54
58
  await this.subscribeStatesAsync('info.currentCookies');
55
59
 
56
60
  //sanitize polling interval:
@@ -65,20 +69,67 @@ class GoogleSharedlocations2 extends utils.Adapter {
65
69
  this._pollInterval = MAX_INT32;
66
70
  }
67
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
+
68
112
  //start polling positions
69
113
  this.pollPositions();
70
- if (this._cookies) {
114
+ if (this.cookie.isValid()) {
71
115
  await this.sendRequest();
72
116
  }
73
117
  }
74
118
 
75
119
  private pollPositions(): void {
76
120
  this._pollTimeout = this.setTimeout(async () => {
77
- if (!this._cookies) {
121
+ if (!this.cookie.isValid()) {
78
122
  this.log.debug('Cannot poll positions, no cookies available!');
79
123
  } else {
80
124
  this.log.debug('Polling positions with current cookies.');
125
+ const lastSuccessPolls = this._successFullPolls;
81
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
+ }
82
133
  }
83
134
  //schedule next poll
84
135
  return this.pollPositions();
@@ -86,7 +137,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
86
137
  }
87
138
 
88
139
  private async sendRequest(): Promise<void> {
89
- if (!this._cookies) {
140
+ if (!this.cookie.isValid()) {
90
141
  this.log.error('Cannot send request, no cookies available!');
91
142
  await this.setState('info.connection', false, true);
92
143
  return;
@@ -98,7 +149,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
98
149
  method: 'GET',
99
150
  url: 'https://www.google.com/maps/rpc/locationsharing/read',
100
151
  headers: {
101
- Cookie: this._cookies,
152
+ Cookie: this.cookie.currentCookie,
102
153
  },
103
154
  params: {
104
155
  authuser: 2,
@@ -119,71 +170,36 @@ class GoogleSharedlocations2 extends utils.Adapter {
119
170
  this._successFullPolls += 1;
120
171
  await this.setState('info.connection', true, true);
121
172
  for (const location of locations) {
122
- 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
+ }
123
180
  }
124
181
  } else {
125
182
  this.log.info('No shared locations found in the response, probably not logged in.');
126
183
  if (this._successFullPolls > 0) {
127
184
  //try to get new cookie:
128
- await this.loginToGetNewCookies();
185
+ this._successFullPolls = 0;
186
+ await this.cookie.loginToGetNewCookies();
129
187
  }
130
188
  }
131
189
  } catch (e) {
132
190
  this.log.error(`Error during request: ${(e as Error).message}`);
133
191
  if (this._successFullPolls > 0) {
134
192
  //try to get new cookie:
135
- await this.loginToGetNewCookies();
193
+ this._successFullPolls = 0;
194
+ await this.cookie.loginToGetNewCookies();
136
195
  }
137
196
  }
138
197
  }
139
198
 
140
- private async fillIntoObjects(locationData: any): Promise<void> {
199
+ private async fillIntoObjects(user: User): Promise<void> {
141
200
  try {
142
- const user = {
143
- id: undefined,
144
- photoURL: undefined,
145
- name: undefined,
146
- lat: undefined,
147
- long: undefined,
148
- address: undefined,
149
- battery: undefined,
150
- timestamp: undefined,
151
- accuracy: undefined,
152
- };
153
-
154
- if (locationData && Array.isArray(locationData)) {
155
- // locationData present
156
- if (locationData[0] && locationData[0][0]) {
157
- user.id = locationData[0][0];
158
- }
159
- if (locationData[0] && locationData[0][1]) {
160
- user.photoURL = locationData[0][1];
161
- }
162
- if (locationData[0] && locationData[0][3]) {
163
- user.name = locationData[0][3];
164
- }
165
- if (locationData[1] && locationData[1][1] && locationData[1][1][2]) {
166
- user.lat = locationData[1][1][2];
167
- }
168
- if (locationData[1] && locationData[1][1] && locationData[1][1][1]) {
169
- user.long = locationData[1][1][1];
170
- }
171
- if (locationData[1] && locationData[1][4]) {
172
- user.address = locationData[1][4];
173
- }
174
- if (locationData[13] && locationData[13][1]) {
175
- user.battery = locationData[13][1];
176
- }
177
- if (locationData[1] && locationData[1][2]) {
178
- user.timestamp = locationData[1][2];
179
- }
180
- if (locationData[1] && locationData[1][3]) {
181
- user.accuracy = locationData[1][3];
182
- }
183
- }
184
-
185
201
  if (user.id) {
186
- const basepath = `users.${user.id as string}`;
202
+ const basepath = `users.${user.id}`;
187
203
  const deviceObj = {
188
204
  _id: basepath,
189
205
  type: 'device',
@@ -306,79 +322,28 @@ class GoogleSharedlocations2 extends utils.Adapter {
306
322
  }
307
323
  }
308
324
 
309
- private async loginToGetNewCookies(): Promise<void> {
310
- try {
311
- if (this._browser) {
312
- this.log.info('Seems we are already trying to log in. Aborting new login attempt.');
313
- return;
314
- }
315
- this.log.info('Trying to login to Google to get new cookies.');
316
- this._successFullPolls = 0;
317
-
318
- //testing puppeteer:
319
- this.log.debug('Starting browser.');
320
- this._browser = await puppeteer.launch({
321
- headless: true,
322
- args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled'],
323
- ignoreDefaultArgs: ['--enable-automation'], //hide automation flag, did not help.
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,
324
333
  });
325
- this.log.debug('browser started, opening new page.');
326
- const page = await this._browser.newPage();
327
-
328
- //hide puppeteer automation flag
329
- await page.evaluateOnNewDocument(() => {
330
- Object.defineProperty(navigator, 'webdriver', { get: () => false });
331
- });
332
- await page.setUserAgent({
333
- userAgent:
334
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
335
- });
336
-
337
- this.log.debug('going to google login page.');
338
- await page.goto(
339
- 'https://accounts.google.com/ServiceLogin?hl=de&continue=https://www.google.com/maps&gae=cb-eomtm',
340
- {
341
- waitUntil: 'networkidle2',
342
- timeout: 60000,
343
- },
344
- );
345
-
346
- this.log.debug('filling in username and clicking next.');
347
- await page.locator('#identifierId').fill(this.config.googleUsername);
348
- //is this enough, or do we need to search button in this div?
349
- await page.locator('#identifierNext').click();
350
- //waiting for #password fails in headles.. :-(
351
- await page.waitForNetworkIdle({ idleTime: 2000 });
352
-
353
- this.log.debug('filling in password and clicking next.');
354
- //do we need to wait until page is loaded / rendered here?
355
- await page.locator('input[type="password"]').fill(this.config.googlePassword);
356
- this.log.debug('clicking password next button.');
357
- await page.locator('#passwordNext').click();
358
- //await page.waitForNetworkIdle({ idleTime: 2000 }); -> does never happen in headless.. :-/
359
- await new Promise(resolve => setTimeout(resolve, 3000));
360
-
361
- await page.goto('https://www.google.com/maps');
362
- this.log.debug('getting cookies.');
363
- //using deprecated function, but browser.cookies just does not work...???
364
- const cookies = await page.cookies();
334
+ }
335
+ }
365
336
 
366
- this._cookies = cookies
367
- .filter(c => c.domain.includes('google'))
368
- .map(c => `${c.name}=${c.value}`)
369
- .join('; ');
370
- //this.log.debug(this._cookies);
371
- //console.log(this._cookies);
372
- await this._browser.close();
373
- if (this._cookies.length < 50) {
374
- this.log.warn('Cookie string seems too short, login probably failed!');
375
- } else {
376
- this.log.info('Obtained new cookies from Google login.');
377
- 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
+ });
378
346
  }
379
- this._browser = null;
380
- } catch (e) {
381
- this.log.error(`Error in puppeteer: ${(e as Error).message}`);
382
347
  }
383
348
  }
384
349
 
@@ -387,7 +352,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
387
352
  *
388
353
  * @param callback - Callback function
389
354
  */
390
- private onUnload(callback: () => void): void {
355
+ private async onUnload(callback: () => void): Promise<void> {
391
356
  try {
392
357
  // Here you must clear all timeouts or intervals that may still be active
393
358
  // clearTimeout(timeout1);
@@ -397,13 +362,7 @@ class GoogleSharedlocations2 extends utils.Adapter {
397
362
  if (this._pollTimeout) {
398
363
  clearTimeout(this._pollTimeout);
399
364
  }
400
- if (this._browser) {
401
- //ignore results here.
402
- this._browser
403
- .close()
404
- .then(() => {})
405
- .catch(() => {});
406
- }
365
+ await this.cookie.cleanUp();
407
366
  callback();
408
367
  } catch (error) {
409
368
  this.log.error(`Error during unloading: ${(error as Error).message}`);
@@ -436,35 +395,46 @@ class GoogleSharedlocations2 extends utils.Adapter {
436
395
  if (id.endsWith('info.currentCookies') && state && !state.ack) {
437
396
  if (state.val === '') {
438
397
  this.log.info('Current cookies state was cleared, trying to obtain new cookies.');
439
- this._cookies = null;
440
- await this.loginToGetNewCookies();
441
- if (this._cookies) {
398
+ this._successFullPolls = 0;
399
+ await this.cookie.loginToGetNewCookies();
400
+ if (this.cookie.isValid()) {
442
401
  await this.sendRequest();
443
402
  }
444
403
  } else {
445
404
  this.log.info(
446
405
  'Current cookies state was changed from outside the adapter, updating internal cookie store.',
447
406
  );
448
- 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
+ }
449
435
  }
450
436
  }
451
437
  }
452
- // If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
453
- // /**
454
- // * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
455
- // * Using this method requires "common.messagebox" property to be set to true in io-package.json
456
- // */
457
- //
458
- // private onMessage(obj: ioBroker.Message): void {
459
- // if (typeof obj === 'object' && obj.message) {
460
- // if (obj.command === 'send') {
461
- // // e.g. send email or pushover or whatever
462
- // this.log.info('send command');
463
- // // Send response in callback if required
464
- // if (obj.callback) this.sendTo(obj.from, obj.command, 'Message received', obj.callback);
465
- // }
466
- // }
467
- // }
468
438
  }
469
439
  //if (require.main !== module) {
470
440
  // Export the constructor in compact mode