iobroker.easee 2.0.0 → 3.0.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 +4 -3
- package/admin/jsonConfig.json +11 -1
- package/io-package.json +15 -2
- package/lib/{enum.js → enum.ts} +8 -10
- package/lib/observations.ts +156 -0
- package/lib/{signalr-http-client.js → signalr-http-client.ts} +34 -12
- package/lib/tools.ts +120 -0
- package/{main.js → main.ts} +318 -139
- package/package.json +10 -9
- package/admin/index_m.html +0 -149
- package/admin/style.css +0 -32
- package/admin/words.js +0 -39
- package/lib/observations.js +0 -89
- package/lib/tools.js +0 -97
- package/admin/i18n/{de/translations.json → de.json} +10 -10
- package/admin/i18n/{en/translations.json → en.json} +10 -10
- package/admin/i18n/{es/translations.json → es.json} +10 -10
- package/admin/i18n/{fr/translations.json → fr.json} +10 -10
- package/admin/i18n/{it/translations.json → it.json} +10 -10
- package/admin/i18n/{nl/translations.json → nl.json} +10 -10
- package/admin/i18n/{pl/translations.json → pl.json} +10 -10
- package/admin/i18n/{pt/translations.json → pt.json} +10 -10
- package/admin/i18n/{ru/translations.json → ru.json} +10 -10
- package/admin/i18n/{uk/translations.json → uk.json} +10 -10
- package/admin/i18n/{zh-cn/translations.json → zh-cn.json} +10 -10
package/README.md
CHANGED
|
@@ -40,6 +40,10 @@ This adapter would not have been possible without the great work of @Newan (http
|
|
|
40
40
|
Placeholder for the next version (at the beginning of the line):
|
|
41
41
|
### **WORK IN PROGRESS**
|
|
42
42
|
-->
|
|
43
|
+
### 3.0.0 (2026-10-23)
|
|
44
|
+
- (Voodoo2man) Migrated the adapter to TypeScript; Node.js 22.19.0 or newer is required
|
|
45
|
+
- (Voodoo2man) Updated adapter tooling, Admin configuration, and runtime metadata
|
|
46
|
+
|
|
43
47
|
### 2.0.0 (2026-10-18)
|
|
44
48
|
- (Voodoo2man) Improved charger status and connectivity handling with the Observations API, SignalR resilience, bounded polling, and API retries, incorporating contributions from @Voodoo2man, @Michaelnorge, @Codibris, and @m1chel87, with earlier Node.js and JSON-config groundwork from @NCIceWolf.
|
|
45
49
|
- (Voodoo2man) Improved circuit-current update reliability and added configurable, rate-limit-aware session polling across chargers.
|
|
@@ -60,9 +64,6 @@ This adapter would not have been possible without the great work of @Newan (http
|
|
|
60
64
|
* (walburgf) modified internationalization to use jsonConfig.json. this needs at least ioBroker.admin version 5
|
|
61
65
|
* (walburgf) added dependency to admin >=v5.1.28
|
|
62
66
|
|
|
63
|
-
### 1.0.8 (2023-07-02)
|
|
64
|
-
* (Newan) small fixes
|
|
65
|
-
|
|
66
67
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
67
68
|
|
|
68
69
|
## Donation
|
package/admin/jsonConfig.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"lg": 4,
|
|
18
18
|
"xl": 4,
|
|
19
19
|
"label": "Username",
|
|
20
|
-
"help": "
|
|
20
|
+
"help": "username to access Easee cloud"
|
|
21
21
|
},
|
|
22
22
|
"client_secret": {
|
|
23
23
|
"type": "password",
|
|
@@ -55,6 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"sessionPollMode": {
|
|
57
57
|
"type": "select",
|
|
58
|
+
"xs": 12,
|
|
59
|
+
"sm": 12,
|
|
60
|
+
"md": 6,
|
|
61
|
+
"lg": 4,
|
|
62
|
+
"xl": 4,
|
|
58
63
|
"options": [
|
|
59
64
|
{ "label": "Automatic", "value": "auto" },
|
|
60
65
|
{ "label": "Manual", "value": "manual" }
|
|
@@ -65,6 +70,11 @@
|
|
|
65
70
|
},
|
|
66
71
|
"sessionPollInterval": {
|
|
67
72
|
"type": "number",
|
|
73
|
+
"xs": 12,
|
|
74
|
+
"sm": 12,
|
|
75
|
+
"md": 6,
|
|
76
|
+
"lg": 4,
|
|
77
|
+
"xl": 4,
|
|
68
78
|
"min": 7,
|
|
69
79
|
"step": 1,
|
|
70
80
|
"unit": "min",
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "easee",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"news": {
|
|
6
|
+
"3.0.0": {
|
|
7
|
+
"en": "Migrated the adapter to TypeScript; Node.js 22.19.0 or newer is required\nUpdated adapter tooling, Admin configuration, and runtime metadata",
|
|
8
|
+
"de": "Adapter auf TypeScript migriert; Node.js 22.19.0 oder neuer nötig\nAdapter-Tools, Admin-Konfiguration und Runtime-Metadaten aktualisiert",
|
|
9
|
+
"ru": "Адаптер перенесен на TypeScript; требуется Node.js 22.19.0 или новее\nОбновленные инструменты адаптера, конфигурация администратора и метаданные времени выполнения",
|
|
10
|
+
"pt": "Migrou o adaptador para o TypeScript; requer Node.js 22.19.0 ou maior\nAdaptador, configuração Admin e metadados de tempo de execução atualizados",
|
|
11
|
+
"nl": "De adapter is overgezet naar TypeScript; Node.js 22.19.0 of nieuwer is vereist\nBijgewerkte adapterhulpmiddelen, beheerdersconfiguratie en runtime-metagegevens",
|
|
12
|
+
"fr": "L'adaptateur a migré vers TypeScript ; Node.js 22.19.0 ou plus récent est requis\nOutils d'adaptation, configuration admin et métadonnées d'exécution mis à jour",
|
|
13
|
+
"it": "È migrato l'adattatore a TypeScript; è richiesto Node.js 22.19.0 o più\nAdapter, configurazione admin e metadati di runtime aggiornati",
|
|
14
|
+
"es": "Migró el adaptador a TypeScript; se necesita Node.js 22.19.0 o más\nSe actualizaron los adaptadores, la configuración de administración y los metadatos",
|
|
15
|
+
"pl": "Przeniesiono adapter do TypeScript; wymagany jest plik Node.js 22.19.0 lub nowszy\nZaktualizowano oprzyrządowanie adaptera, konfigurację administratora i metadane środowiska wykonawczego",
|
|
16
|
+
"uk": "Перемістив адаптер на TypeScript; потрібен Node.js 22.19.0 або новіший\nОновлені інструменти адаптера, конфігурація адміністратора та метадані виконання",
|
|
17
|
+
"zh-cn": "已将适配器迁移到 TypeScript;需要 Node.js 22.19.0 或更高版本\n更新了适配器工具、管理员配置和运行时元数据"
|
|
18
|
+
},
|
|
6
19
|
"2.0.0": {
|
|
7
20
|
"en": "Improved charger status and connectivity handling with the Observations API, SignalR resilience, bounded polling, and API retries, incorporating contributions from @Voodoo2man, @Michaelnorge, @Codibris, and @m1chel87, with earlier Node.js and JSON-config groundwork from @NCIceWolf.\nImproved circuit-current update reliability and added configurable, rate-limit-aware session polling across chargers.\nAdded configurable net charging prices and VAT with synchronized gross prices.",
|
|
8
21
|
"de": "Verbesserter Ladestatus und Konnektivität mit Observations API, SignalR Resilience, Bounded Polling und API-Wiederholungen, mit Beiträgen von @Voodoo2man, @Michaelnorge, @Codibris und @m1chel87, mit früheren Node.js- und JSON-Config-Grundlagen von @NCIceWolf.\nZuverlässiger Stromausgleich und konfigurierbares, ratenbegrenztes Session-Polling auf Ladegeräten hinzugefügt.\nNettopreise und Mehrwertsteuer mit synchronisierten Bruttopreisen hinzugefügt.",
|
|
@@ -115,7 +128,7 @@
|
|
|
115
128
|
"tier": 3,
|
|
116
129
|
"mode": "daemon",
|
|
117
130
|
"type": "vehicle",
|
|
118
|
-
"compact":
|
|
131
|
+
"compact": false,
|
|
119
132
|
"connectionType": "cloud",
|
|
120
133
|
"dataSource": "poll",
|
|
121
134
|
"adminUI": {
|
package/lib/{enum.js → enum.ts}
RENAMED
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
#define COMMAND_SET_LISTEN_TO_CONTROL_PULSE_ARG_LISTEN_TO_CONTROL_PULSE 1 //ListenToControlPulse [Boolean]
|
|
316
316
|
#endif
|
|
317
317
|
*/
|
|
318
|
-
const easeeIds = {
|
|
318
|
+
const easeeIds: Record<number, string> = {
|
|
319
319
|
//config
|
|
320
320
|
31: ".config.isEnabled",
|
|
321
321
|
36: ".config.wiFiSSID",
|
|
@@ -358,12 +358,10 @@ const easeeIds = {
|
|
|
358
358
|
199: ".status.inVoltageT4T5",
|
|
359
359
|
};
|
|
360
360
|
Object.freeze(easeeIds);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
},
|
|
369
|
-
};
|
|
361
|
+
export function getNameByEnum(id: number): string | undefined {
|
|
362
|
+
return easeeIds[id];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function getValue(): undefined {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export const CHARGER_STATE_OBSERVATION_IDS = [
|
|
4
|
+
46, 48, 68, 80, 96, 102, 103, 109, 111, 112, 113, 114, 120, 122, 124, 132,
|
|
5
|
+
182, 183, 184, 185, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 250,
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const STATE_FIELDS_BY_OBSERVATION_ID: Record<
|
|
9
|
+
number,
|
|
10
|
+
keyof ChargerObservationState
|
|
11
|
+
> = {
|
|
12
|
+
46: "ledMode",
|
|
13
|
+
48: "dynamicChargerCurrent",
|
|
14
|
+
68: "wiFiAPEnabled",
|
|
15
|
+
80: "chargerFirmware",
|
|
16
|
+
96: "reasonForNoCurrent",
|
|
17
|
+
102: "smartCharging",
|
|
18
|
+
103: "cableLocked",
|
|
19
|
+
109: "chargerOpMode",
|
|
20
|
+
111: "dynamicCircuitCurrentP1",
|
|
21
|
+
112: "dynamicCircuitCurrentP2",
|
|
22
|
+
113: "dynamicCircuitCurrentP3",
|
|
23
|
+
114: "outputCurrent",
|
|
24
|
+
120: "totalPower",
|
|
25
|
+
122: "energyPerHour",
|
|
26
|
+
124: "lifetimeEnergy",
|
|
27
|
+
132: "wiFiRSSI",
|
|
28
|
+
182: "inCurrentT2",
|
|
29
|
+
183: "inCurrentT3",
|
|
30
|
+
184: "inCurrentT4",
|
|
31
|
+
185: "inCurrentT5",
|
|
32
|
+
190: "inVoltageT1T2",
|
|
33
|
+
191: "inVoltageT1T3",
|
|
34
|
+
192: "inVoltageT1T4",
|
|
35
|
+
193: "inVoltageT1T5",
|
|
36
|
+
194: "inVoltageT2T3",
|
|
37
|
+
195: "inVoltageT2T4",
|
|
38
|
+
196: "inVoltageT2T5",
|
|
39
|
+
197: "inVoltageT3T4",
|
|
40
|
+
198: "inVoltageT3T5",
|
|
41
|
+
199: "inVoltageT4T5",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SUPPLY_VOLTAGE_FIELDS = [
|
|
45
|
+
"inVoltageT2T3",
|
|
46
|
+
"inVoltageT1T3",
|
|
47
|
+
"inVoltageT1T4",
|
|
48
|
+
"inVoltageT1T5",
|
|
49
|
+
"inVoltageT2T4",
|
|
50
|
+
"inVoltageT2T5",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Easee observation returned by the API. */
|
|
54
|
+
export interface ChargerObservation {
|
|
55
|
+
id: number;
|
|
56
|
+
value: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Observation collection returned by the API. */
|
|
60
|
+
export interface ChargerObservationResponse {
|
|
61
|
+
observations?: ChargerObservation[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Charger state values produced from observations. */
|
|
65
|
+
export interface ChargerObservationState {
|
|
66
|
+
[field: string]: ioBroker.StateValue | undefined;
|
|
67
|
+
ledMode: ioBroker.StateValue;
|
|
68
|
+
dynamicChargerCurrent: ioBroker.StateValue;
|
|
69
|
+
wiFiAPEnabled: ioBroker.StateValue;
|
|
70
|
+
chargerFirmware: ioBroker.StateValue;
|
|
71
|
+
reasonForNoCurrent: ioBroker.StateValue;
|
|
72
|
+
smartCharging: ioBroker.StateValue;
|
|
73
|
+
cableLocked: ioBroker.StateValue;
|
|
74
|
+
chargerOpMode: ioBroker.StateValue;
|
|
75
|
+
dynamicCircuitCurrentP1: ioBroker.StateValue;
|
|
76
|
+
dynamicCircuitCurrentP2: ioBroker.StateValue;
|
|
77
|
+
dynamicCircuitCurrentP3: ioBroker.StateValue;
|
|
78
|
+
outputCurrent: ioBroker.StateValue;
|
|
79
|
+
totalPower: ioBroker.StateValue;
|
|
80
|
+
energyPerHour: ioBroker.StateValue;
|
|
81
|
+
lifetimeEnergy: ioBroker.StateValue;
|
|
82
|
+
wiFiRSSI: ioBroker.StateValue;
|
|
83
|
+
inCurrentT2: ioBroker.StateValue;
|
|
84
|
+
inCurrentT3: ioBroker.StateValue;
|
|
85
|
+
inCurrentT4: ioBroker.StateValue;
|
|
86
|
+
inCurrentT5: ioBroker.StateValue;
|
|
87
|
+
inVoltageT1T2: ioBroker.StateValue;
|
|
88
|
+
inVoltageT1T3: ioBroker.StateValue;
|
|
89
|
+
inVoltageT1T4: ioBroker.StateValue;
|
|
90
|
+
inVoltageT1T5: ioBroker.StateValue;
|
|
91
|
+
inVoltageT2T3: ioBroker.StateValue;
|
|
92
|
+
inVoltageT2T4: ioBroker.StateValue;
|
|
93
|
+
inVoltageT2T5: ioBroker.StateValue;
|
|
94
|
+
inVoltageT3T4: ioBroker.StateValue;
|
|
95
|
+
inVoltageT3T5: ioBroker.StateValue;
|
|
96
|
+
inVoltageT4T5: ioBroker.StateValue;
|
|
97
|
+
isOnline?: boolean;
|
|
98
|
+
voltage: number | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Converts Easee API observations to the existing charger state fields.
|
|
103
|
+
*
|
|
104
|
+
* @param responseData
|
|
105
|
+
*/
|
|
106
|
+
export function observationsToChargerState(
|
|
107
|
+
responseData: ChargerObservationResponse | null | undefined,
|
|
108
|
+
): ChargerObservationState {
|
|
109
|
+
const observations = Array.isArray(responseData?.observations)
|
|
110
|
+
? responseData.observations
|
|
111
|
+
: [];
|
|
112
|
+
const chargerState: Record<string, ioBroker.StateValue | undefined> =
|
|
113
|
+
Object.fromEntries(
|
|
114
|
+
Object.values(STATE_FIELDS_BY_OBSERVATION_ID).map((field) => [
|
|
115
|
+
field,
|
|
116
|
+
null,
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
for (const observation of observations) {
|
|
121
|
+
const field = STATE_FIELDS_BY_OBSERVATION_ID[observation.id];
|
|
122
|
+
if (field !== undefined && isStateValue(observation.value)) {
|
|
123
|
+
chargerState[field] = observation.value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cloudObservation = observations.find(
|
|
128
|
+
(observation) => observation.id === 250,
|
|
129
|
+
);
|
|
130
|
+
if (cloudObservation) {
|
|
131
|
+
if (isStateValue(cloudObservation.value)) {
|
|
132
|
+
chargerState.isOnline = Boolean(cloudObservation.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
chargerState.voltage = null;
|
|
136
|
+
for (const field of SUPPLY_VOLTAGE_FIELDS) {
|
|
137
|
+
const value = chargerState[field];
|
|
138
|
+
if (
|
|
139
|
+
typeof value === "number" &&
|
|
140
|
+
Number.isFinite(value) &&
|
|
141
|
+
value >= 100 &&
|
|
142
|
+
value <= 300
|
|
143
|
+
) {
|
|
144
|
+
chargerState.voltage = value;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return chargerState as ChargerObservationState;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isStateValue(value: unknown): value is ioBroker.StateValue {
|
|
153
|
+
return (
|
|
154
|
+
value === null || ["string", "number", "boolean"].includes(typeof value)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import * as signalR from "@microsoft/signalr";
|
|
5
|
+
import { CookieJar } from "tough-cookie";
|
|
6
|
+
import type { HttpRequest, HttpResponse } from "@microsoft/signalr";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
type SignalRRequest = HttpRequest;
|
|
9
|
+
|
|
10
|
+
interface SignalRHttpClientLike {
|
|
11
|
+
request: typeof axios.request;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
8
15
|
return Object.keys(headers).some(
|
|
9
16
|
(headerName) => headerName.toLowerCase() === name.toLowerCase(),
|
|
10
17
|
);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
class AxiosSignalRHttpClient extends signalR.HttpClient {
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
httpClient: SignalRHttpClientLike;
|
|
22
|
+
cookieJar: InstanceType<typeof CookieJar>;
|
|
23
|
+
|
|
24
|
+
constructor(httpClient: SignalRHttpClientLike = axios) {
|
|
16
25
|
super();
|
|
17
26
|
this.httpClient = httpClient;
|
|
18
27
|
this.cookieJar = new CookieJar();
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
async send(request) {
|
|
30
|
+
async send(request: SignalRRequest): Promise<HttpResponse> {
|
|
22
31
|
if (!request.url) {
|
|
23
32
|
throw new Error("SignalR HTTP request URL is missing");
|
|
24
33
|
}
|
|
25
|
-
const headers = { ...request.headers };
|
|
34
|
+
const headers = { ...(request.headers || {}) };
|
|
26
35
|
const cookie = this.cookieJar.getCookieStringSync(request.url);
|
|
27
36
|
if (cookie && !hasHeader(headers, "cookie")) {
|
|
28
37
|
headers.Cookie = cookie;
|
|
29
38
|
}
|
|
30
39
|
const cancelSource = axios.CancelToken.source();
|
|
31
|
-
const abortHandler = () =>
|
|
40
|
+
const abortHandler = (): void => {
|
|
32
41
|
cancelSource.cancel("SignalR HTTP request aborted");
|
|
42
|
+
};
|
|
33
43
|
if (request.abortSignal) {
|
|
34
44
|
request.abortSignal.onabort = abortHandler;
|
|
35
45
|
}
|
|
@@ -43,7 +53,7 @@ class AxiosSignalRHttpClient extends signalR.HttpClient {
|
|
|
43
53
|
cancelToken: cancelSource.token,
|
|
44
54
|
responseType:
|
|
45
55
|
request.responseType === "arraybuffer" ? "arraybuffer" : "text",
|
|
46
|
-
transformResponse: [(data) => data],
|
|
56
|
+
transformResponse: [(data: unknown) => data],
|
|
47
57
|
validateStatus: () => true,
|
|
48
58
|
});
|
|
49
59
|
for (const setCookie of response.headers?.["set-cookie"] || []) {
|
|
@@ -75,9 +85,21 @@ class AxiosSignalRHttpClient extends signalR.HttpClient {
|
|
|
75
85
|
}
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
get(url: string, options?: SignalRRequest): Promise<HttpResponse> {
|
|
89
|
+
return this.send({ ...options, url, method: "GET" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
post(url: string, options?: SignalRRequest): Promise<HttpResponse> {
|
|
93
|
+
return this.send({ ...options, url, method: "POST" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
delete(url: string, options?: SignalRRequest): Promise<HttpResponse> {
|
|
97
|
+
return this.send({ ...options, url, method: "DELETE" });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getCookieString(url: string): string {
|
|
79
101
|
return this.cookieJar.getCookieStringSync(url);
|
|
80
102
|
}
|
|
81
103
|
}
|
|
82
104
|
|
|
83
|
-
|
|
105
|
+
export = AxiosSignalRHttpClient;
|
package/lib/tools.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import type { AxiosError } from "axios";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests whether the given variable is a real object and not an Array
|
|
6
|
+
*
|
|
7
|
+
* @param it The variable to test
|
|
8
|
+
* @returns True if the variable is an object, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export function isObject(it: unknown): it is Record<string, unknown> {
|
|
11
|
+
return Object.prototype.toString.call(it) === "[object Object]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tests whether the given variable is really an Array
|
|
16
|
+
*
|
|
17
|
+
* @param it The variable to test
|
|
18
|
+
* @returns True if the variable is an array, false otherwise
|
|
19
|
+
*/
|
|
20
|
+
export function isArray(it: unknown): it is unknown[] {
|
|
21
|
+
return Array.isArray(it);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Translates text to the target language. Automatically chooses the right translation API.
|
|
26
|
+
*
|
|
27
|
+
* @param text The text to translate
|
|
28
|
+
* @param targetLang The target language
|
|
29
|
+
* @param [yandexApiKey] The yandex API key. You can create one for free at https://translate.yandex.com/developers
|
|
30
|
+
* @returns The translated text
|
|
31
|
+
*/
|
|
32
|
+
export async function translateText(
|
|
33
|
+
text: string,
|
|
34
|
+
targetLang: string,
|
|
35
|
+
yandexApiKey?: string,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
if (!text) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
if (targetLang === "en") {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return yandexApiKey
|
|
45
|
+
? translateYandex(text, targetLang, yandexApiKey)
|
|
46
|
+
: translateGoogle(text, targetLang);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Translates text with Yandex API
|
|
51
|
+
*
|
|
52
|
+
* @param text The text to translate
|
|
53
|
+
* @param targetLang The target language
|
|
54
|
+
* @param apiKey The yandex API key. You can create one for free at https://translate.yandex.com/developers
|
|
55
|
+
* @returns The translated text
|
|
56
|
+
*/
|
|
57
|
+
async function translateYandex(
|
|
58
|
+
text: string,
|
|
59
|
+
targetLang: string,
|
|
60
|
+
apiKey: string,
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
if (targetLang === "zh-cn") {
|
|
63
|
+
targetLang = "zh";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const url = `https://translate.yandex.net/api/v1.5/tr.json/translate?key=${apiKey}&text=${encodeURIComponent(text)}&lang=en-${targetLang}`;
|
|
67
|
+
try {
|
|
68
|
+
const response = await axios.get(url, { timeout: 15000 });
|
|
69
|
+
if (response.data?.text && isArray(response.data.text)) {
|
|
70
|
+
return response.data.text[0];
|
|
71
|
+
}
|
|
72
|
+
throw new Error("Invalid response for translate request");
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Could not translate to "${targetLang}": ${getErrorMessage(error)}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Translates text with Google API
|
|
82
|
+
*
|
|
83
|
+
* @param text The text to translate
|
|
84
|
+
* @param targetLang The target language
|
|
85
|
+
* @returns The translated text
|
|
86
|
+
*/
|
|
87
|
+
async function translateGoogle(
|
|
88
|
+
text: string,
|
|
89
|
+
targetLang: string,
|
|
90
|
+
): Promise<string> {
|
|
91
|
+
const url = `http://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}&ie=UTF-8&oe=UTF-8`;
|
|
92
|
+
try {
|
|
93
|
+
const response = await axios.get<unknown>(url, { timeout: 15000 });
|
|
94
|
+
const translation = response.data;
|
|
95
|
+
if (
|
|
96
|
+
Array.isArray(translation) &&
|
|
97
|
+
Array.isArray(translation[0]) &&
|
|
98
|
+
Array.isArray(translation[0][0]) &&
|
|
99
|
+
typeof translation[0][0][0] === "string"
|
|
100
|
+
) {
|
|
101
|
+
return translation[0][0][0];
|
|
102
|
+
}
|
|
103
|
+
throw new Error("Invalid response for translate request");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const response = axios.isAxiosError(error)
|
|
106
|
+
? (error as AxiosError).response
|
|
107
|
+
: undefined;
|
|
108
|
+
const detail =
|
|
109
|
+
response?.status === 429
|
|
110
|
+
? `Rate-limited by Google Translate`
|
|
111
|
+
: getErrorMessage(error);
|
|
112
|
+
throw new Error(`Could not translate to "${targetLang}": ${detail}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getErrorMessage(error: unknown): string {
|
|
117
|
+
return error instanceof Error ? error.message : String(error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { translateYandex, translateGoogle };
|