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 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
@@ -17,7 +17,7 @@
17
17
  "lg": 4,
18
18
  "xl": 4,
19
19
  "label": "Username",
20
- "help": "Username to access Easee cloud"
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": "2.0.0",
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": true,
131
+ "compact": false,
119
132
  "connectionType": "cloud",
120
133
  "dataSource": "poll",
121
134
  "adminUI": {
@@ -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
- module.exports = {
362
- getNameByEnum: function (id) {
363
- return easeeIds[id];
364
- //return "Not Found"
365
- },
366
- getValue: function () {
367
- // whatever
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
- const axios = require("axios").default;
4
- const signalR = require("@microsoft/signalr");
5
- const { CookieJar } = require("tough-cookie");
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
- function hasHeader(headers, name) {
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
- /** @param {any} [httpClient] */
15
- constructor(httpClient = axios) {
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
- getCookieString(url) {
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
- module.exports = AxiosSignalRHttpClient;
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 };