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 +9 -0
- package/admin/jsonConfig.json +131 -47
- package/io-package.json +40 -2
- package/package.json +1 -1
- package/src/lib/Cookie.ts +220 -0
- package/src/lib/Fence.ts +62 -0
- package/src/lib/User.ts +53 -0
- package/src/lib/adapter-config.d.ts +9 -0
- package/src/main.ts +134 -164
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
|
|
package/admin/jsonConfig.json
CHANGED
|
@@ -1,51 +1,135 @@
|
|
|
1
1
|
{
|
|
2
2
|
"i18n": true,
|
|
3
|
-
"type": "
|
|
4
|
-
"items":
|
|
5
|
-
|
|
6
|
-
"type": "
|
|
7
|
-
"label": "
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
"type": "
|
|
17
|
-
"label": "
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
@@ -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
|
+
}
|
package/src/lib/Fence.ts
ADDED
|
@@ -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
|
+
}
|
package/src/lib/User.ts
ADDED
|
@@ -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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
this._successFullPolls = 0;
|
|
194
|
+
await this.cookie.loginToGetNewCookies();
|
|
136
195
|
}
|
|
137
196
|
}
|
|
138
197
|
}
|
|
139
198
|
|
|
140
|
-
private async fillIntoObjects(
|
|
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
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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.
|
|
440
|
-
await this.loginToGetNewCookies();
|
|
441
|
-
if (this.
|
|
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.
|
|
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
|