iobroker.zendure-solarflow 1.6.2 → 1.6.3
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 -0
- package/build/services/jobSchedule.js +23 -5
- package/build/services/jobSchedule.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,10 @@ If you find the adapter useful for you and want to support my work, feel free to
|
|
|
46
46
|
[](https://www.paypal.com/paypalme/PeterFrommert)
|
|
47
47
|
|
|
48
48
|
## Changelog
|
|
49
|
+
### 1.6.3 (2024-06-03)
|
|
50
|
+
|
|
51
|
+
- Fixed reconnect when connection seems to be dead.
|
|
52
|
+
|
|
49
53
|
### 1.6.2 (2024-05-21)
|
|
50
54
|
|
|
51
55
|
- Changed standby usage to 10W
|
|
@@ -29,14 +29,26 @@ var import_mqttService = require("./mqttService");
|
|
|
29
29
|
var import_webService = require("./webService");
|
|
30
30
|
var import_calculationService = require("./calculationService");
|
|
31
31
|
const refreshAccessToken = (adapter) => {
|
|
32
|
-
var _a;
|
|
32
|
+
var _a, _b;
|
|
33
33
|
adapter.log.info(`[startRefreshAccessTokenTimerJob] Refreshing accessToken!`);
|
|
34
|
+
if (adapter.resetValuesJob) {
|
|
35
|
+
adapter.resetValuesJob.cancel();
|
|
36
|
+
adapter.resetValuesJob = void 0;
|
|
37
|
+
}
|
|
38
|
+
if (adapter.checkStatesJob) {
|
|
39
|
+
(_a = adapter.checkStatesJob) == null ? void 0 : _a.cancel();
|
|
40
|
+
adapter.checkStatesJob = void 0;
|
|
41
|
+
}
|
|
42
|
+
if (adapter.calculationJob) {
|
|
43
|
+
adapter.calculationJob.cancel();
|
|
44
|
+
adapter.calculationJob = void 0;
|
|
45
|
+
}
|
|
34
46
|
if (adapter.mqttClient) {
|
|
35
47
|
adapter.mqttClient.end();
|
|
36
48
|
adapter.mqttClient = void 0;
|
|
37
49
|
}
|
|
38
50
|
if (adapter.config.userName && adapter.config.password) {
|
|
39
|
-
(
|
|
51
|
+
(_b = (0, import_webService.login)(adapter)) == null ? void 0 : _b.then((_accessToken) => {
|
|
40
52
|
adapter.accessToken = _accessToken;
|
|
41
53
|
adapter.lastLogin = /* @__PURE__ */ new Date();
|
|
42
54
|
adapter.setState("info.connection", true, true);
|
|
@@ -72,7 +84,10 @@ const startCheckStatesAndConnectionJob = async (adapter) => {
|
|
|
72
84
|
"solarInputPower"
|
|
73
85
|
];
|
|
74
86
|
let refreshAccessTokenNeeded = false;
|
|
75
|
-
adapter.
|
|
87
|
+
adapter.log.debug(
|
|
88
|
+
`[checkStatesJob] Starting check of states and connection!`
|
|
89
|
+
);
|
|
90
|
+
adapter.checkStatesJob = (0, import_node_schedule.scheduleJob)("*/5 * * * *", async () => {
|
|
76
91
|
adapter.deviceList.forEach(async (device) => {
|
|
77
92
|
if (refreshAccessTokenNeeded) {
|
|
78
93
|
return;
|
|
@@ -83,8 +98,11 @@ const startCheckStatesAndConnectionJob = async (adapter) => {
|
|
|
83
98
|
const wifiState = await (adapter == null ? void 0 : adapter.getStateAsync(
|
|
84
99
|
device.productKey + "." + device.deviceKey + ".wifiState"
|
|
85
100
|
));
|
|
86
|
-
const fiveMinutesAgo = Date.now() / 1e3 - 5 * 60;
|
|
87
|
-
const tenMinutesAgo = Date.now() / 1e3 - 10 * 60;
|
|
101
|
+
const fiveMinutesAgo = (Date.now() / 1e3 - 5 * 60) * 1e3;
|
|
102
|
+
const tenMinutesAgo = (Date.now() / 1e3 - 10 * 60) * 1e3;
|
|
103
|
+
adapter.log.debug(
|
|
104
|
+
`[checkStatesJob] lastUpdate for device ${device.deviceKey} was at ${lastUpdate == null ? void 0 : lastUpdate.val}, timestamp fiveMinutes ago: ${fiveMinutesAgo}, Wifi State: ${wifiState == null ? void 0 : wifiState.val}!`
|
|
105
|
+
);
|
|
88
106
|
if (lastUpdate && lastUpdate.val && Number(lastUpdate.val) < fiveMinutesAgo && (wifiState == null ? void 0 : wifiState.val) == "Connected") {
|
|
89
107
|
adapter.log.debug(
|
|
90
108
|
`[checkStatesJob] Last update for deviceKey ${device.deviceKey} was at ${new Date(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/services/jobSchedule.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable @typescript-eslint/indent */\r\nimport { scheduleJob } from \"node-schedule\";\r\nimport { ZendureSolarflow } from \"../main\";\r\nimport { connectMqttClient } from \"./mqttService\";\r\nimport { login } from \"./webService\";\r\nimport { ISolarFlowDeviceDetails } from \"../models/ISolarFlowDeviceDetails\";\r\nimport { calculateEnergy, resetTodaysValues } from \"./calculationService\";\r\n\r\nconst refreshAccessToken = (adapter: ZendureSolarflow): void => {\r\n // Relogin every 3 hours to get a fresh accessToken!\r\n adapter.log.info(`[startRefreshAccessTokenTimerJob] Refreshing accessToken!`);\r\n\r\n if (adapter.mqttClient) {\r\n adapter.mqttClient.end();\r\n adapter.mqttClient = undefined;\r\n }\r\n\r\n if (adapter.config.userName && adapter.config.password) {\r\n login(adapter)?.then((_accessToken: string) => {\r\n adapter.accessToken = _accessToken;\r\n adapter.lastLogin = new Date();\r\n adapter.setState(\"info.connection\", true, true);\r\n\r\n connectMqttClient(adapter);\r\n });\r\n }\r\n};\r\n\r\nexport const startRefreshAccessTokenTimerJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.refreshAccessTokenInterval = adapter.setInterval(\r\n () => {\r\n refreshAccessToken(adapter);\r\n },\r\n 3 * 60 * 60 * 1000,\r\n );\r\n};\r\n\r\nexport const startResetValuesJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.resetValuesJob = scheduleJob(\"5 0 0 * * *\", () => {\r\n // Reset Values\r\n resetTodaysValues(adapter);\r\n });\r\n};\r\n\r\nexport const startCalculationJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.calculationJob = scheduleJob(\"*/30 * * * * *\", () => {\r\n adapter.deviceList.forEach((device) => {\r\n calculateEnergy(adapter, device.productKey, device.deviceKey);\r\n });\r\n });\r\n};\r\n\r\nexport const startCheckStatesAndConnectionJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n // Check for states that has no updates in the last 5 minutes and set them to 0\r\n const statesToReset: string[] = [\r\n \"outputHomePower\",\r\n \"outputPackPower\",\r\n \"packInputPower\",\r\n \"solarInputPower\",\r\n ];\r\n\r\n let refreshAccessTokenNeeded = false;\r\n\r\n adapter.checkStatesJob = scheduleJob(\"*/
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,2BAA4B;AAE5B,yBAAkC;AAClC,wBAAsB;AAEtB,gCAAmD;AAEnD,MAAM,qBAAqB,CAAC,YAAoC;AARhE;AAUE,UAAQ,IAAI,KAAK,2DAA2D;
|
|
4
|
+
"sourcesContent": ["/* eslint-disable @typescript-eslint/indent */\r\nimport { scheduleJob } from \"node-schedule\";\r\nimport { ZendureSolarflow } from \"../main\";\r\nimport { connectMqttClient } from \"./mqttService\";\r\nimport { login } from \"./webService\";\r\nimport { ISolarFlowDeviceDetails } from \"../models/ISolarFlowDeviceDetails\";\r\nimport { calculateEnergy, resetTodaysValues } from \"./calculationService\";\r\n\r\nconst refreshAccessToken = (adapter: ZendureSolarflow): void => {\r\n // Relogin every 3 hours to get a fresh accessToken!\r\n adapter.log.info(`[startRefreshAccessTokenTimerJob] Refreshing accessToken!`);\r\n\r\n // Scheduler beenden\r\n if (adapter.resetValuesJob) {\r\n adapter.resetValuesJob.cancel();\r\n adapter.resetValuesJob = undefined;\r\n }\r\n\r\n if (adapter.checkStatesJob) {\r\n adapter.checkStatesJob?.cancel();\r\n adapter.checkStatesJob = undefined;\r\n }\r\n\r\n if (adapter.calculationJob) {\r\n adapter.calculationJob.cancel();\r\n adapter.calculationJob = undefined;\r\n }\r\n\r\n if (adapter.mqttClient) {\r\n adapter.mqttClient.end();\r\n adapter.mqttClient = undefined;\r\n }\r\n\r\n if (adapter.config.userName && adapter.config.password) {\r\n login(adapter)?.then((_accessToken: string) => {\r\n adapter.accessToken = _accessToken;\r\n adapter.lastLogin = new Date();\r\n adapter.setState(\"info.connection\", true, true);\r\n\r\n connectMqttClient(adapter);\r\n });\r\n }\r\n};\r\n\r\nexport const startRefreshAccessTokenTimerJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.refreshAccessTokenInterval = adapter.setInterval(\r\n () => {\r\n refreshAccessToken(adapter);\r\n },\r\n 3 * 60 * 60 * 1000,\r\n );\r\n};\r\n\r\nexport const startResetValuesJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.resetValuesJob = scheduleJob(\"5 0 0 * * *\", () => {\r\n // Reset Values\r\n resetTodaysValues(adapter);\r\n });\r\n};\r\n\r\nexport const startCalculationJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n adapter.calculationJob = scheduleJob(\"*/30 * * * * *\", () => {\r\n adapter.deviceList.forEach((device) => {\r\n calculateEnergy(adapter, device.productKey, device.deviceKey);\r\n });\r\n });\r\n};\r\n\r\nexport const startCheckStatesAndConnectionJob = async (\r\n adapter: ZendureSolarflow,\r\n): Promise<void> => {\r\n // Check for states that has no updates in the last 5 minutes and set them to 0\r\n const statesToReset: string[] = [\r\n \"outputHomePower\",\r\n \"outputPackPower\",\r\n \"packInputPower\",\r\n \"solarInputPower\",\r\n ];\r\n\r\n let refreshAccessTokenNeeded = false;\r\n\r\n adapter.log.debug(\r\n `[checkStatesJob] Starting check of states and connection!`,\r\n );\r\n\r\n adapter.checkStatesJob = scheduleJob(\"*/5 * * * *\", async () => {\r\n adapter.deviceList.forEach(async (device: ISolarFlowDeviceDetails) => {\r\n if (refreshAccessTokenNeeded) {\r\n return;\r\n }\r\n\r\n const lastUpdate = await adapter?.getStateAsync(\r\n device.productKey + \".\" + device.deviceKey + \".lastUpdate\",\r\n );\r\n\r\n const wifiState = await adapter?.getStateAsync(\r\n device.productKey + \".\" + device.deviceKey + \".wifiState\",\r\n );\r\n\r\n const fiveMinutesAgo = (Date.now() / 1000 - 5 * 60) * 1000; // Five minutes ago\r\n const tenMinutesAgo = (Date.now() / 1000 - 10 * 60) * 1000; // Ten minutes ago\r\n\r\n adapter.log.debug(\r\n `[checkStatesJob] lastUpdate for device ${device.deviceKey} was at ${lastUpdate?.val}, timestamp fiveMinutes ago: ${fiveMinutesAgo}, Wifi State: ${wifiState?.val}!`,\r\n );\r\n\r\n if (\r\n lastUpdate &&\r\n lastUpdate.val &&\r\n Number(lastUpdate.val) < fiveMinutesAgo &&\r\n wifiState?.val == \"Connected\"\r\n ) {\r\n adapter.log.debug(\r\n `[checkStatesJob] Last update for deviceKey ${\r\n device.deviceKey\r\n } was at ${new Date(\r\n Number(lastUpdate)\r\n )}, device seems to be online - so maybe connection is broken - reconnect!`,\r\n );\r\n\r\n refreshAccessToken(adapter);\r\n\r\n // set marker, so we discontinue the forEach Loop because of reconnect!\r\n refreshAccessTokenNeeded = true;\r\n }\r\n\r\n if (\r\n lastUpdate &&\r\n lastUpdate.val &&\r\n Number(lastUpdate.val) < tenMinutesAgo\r\n ) {\r\n adapter.log.debug(\r\n `[checkStatesJob] Last update for deviceKey ${\r\n device.deviceKey\r\n } was at ${new Date(\r\n Number(lastUpdate),\r\n )}, checking for pseudo power values!`,\r\n );\r\n // State was not updated in the last 10 minutes... set states to 0\r\n await statesToReset.forEach(async (stateName: string) => {\r\n await adapter?.setStateAsync(\r\n device.productKey + \".\" + device.deviceKey + \".\" + stateName,\r\n 0,\r\n true,\r\n );\r\n });\r\n\r\n // set electricLevel from deviceList\r\n if (device.electricity) {\r\n await adapter?.setStateAsync(\r\n device.productKey + \".\" + device.deviceKey + \".electricLevel\",\r\n device.electricity,\r\n true,\r\n );\r\n }\r\n }\r\n });\r\n });\r\n};\r\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "zendure-solarflow",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.6.3": {
|
|
7
|
+
"en": "Fixed reconnect when connection seems to be dead.",
|
|
8
|
+
"de": "Feste Wiederverbindung, wenn die Verbindung tot ist.",
|
|
9
|
+
"ru": "Переключение, когда связь кажется мертвой.",
|
|
10
|
+
"pt": "Reconexão fixa quando a conexão parece estar morta.",
|
|
11
|
+
"nl": "Vaste verbinding wanneer de verbinding lijkt te zijn uitgeschakeld.",
|
|
12
|
+
"fr": "Réconnecter quand la connexion semble morte.",
|
|
13
|
+
"it": "Ricollegamento fisso quando la connessione sembra essere morta.",
|
|
14
|
+
"es": "Reconexión fija cuando la conexión parece estar muerta.",
|
|
15
|
+
"pl": "Naprawiono połączenie, gdy połączenie wydaje się martwe.",
|
|
16
|
+
"uk": "Виправлено відключення при з'єднанні з'являється мертвим.",
|
|
17
|
+
"zh-cn": "连接似乎已失效时固定重联 ."
|
|
18
|
+
},
|
|
6
19
|
"1.6.2": {
|
|
7
20
|
"en": "Changed standby usage to 10W",
|
|
8
21
|
"de": "Geänderte Standby-Nutzung auf 10W",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Niewielkie poprawki i ulepszenia",
|
|
81
94
|
"uk": "Мінорні виправлення та вдосконалення",
|
|
82
95
|
"zh-cn": "小错误修正和改进"
|
|
83
|
-
},
|
|
84
|
-
"1.5.0": {
|
|
85
|
-
"en": "Add the possibility to connect to the 'Fallback' MQTT server known as 'Developer MQTT'. This server is read-only - so no control is possible!",
|
|
86
|
-
"de": "Fügen Sie die Möglichkeit hinzu, sich mit dem MQTT-Server 'Fallback' zu verbinden. Dieser Server ist nur lesbar - so ist keine Kontrolle möglich!",
|
|
87
|
-
"ru": "Добавить возможность подключения к серверу MQTT 'Fallback', известному как 'Developer MQTT'. Этот сервер только для чтения - так что никакого контроля невозможно!",
|
|
88
|
-
"pt": "Adicione a possibilidade de se conectar ao servidor MQTT 'Fallback' conhecido como 'Developer MQTT'. Este servidor é somente leitura - então nenhum controle é possível!",
|
|
89
|
-
"nl": "Voeg de mogelijkheid toe om verbinding te maken met de 'Fallback' MQTT server bekend als 'Developer MQTT'. Deze server is alleen-lezen - dus geen controle mogelijk!",
|
|
90
|
-
"fr": "Ajoutez la possibilité de vous connecter au serveur MQTT 'Fallback' connu sous le nom de 'Developer MQTT'. Ce serveur est en lecture seule - donc aucun contrôle n'est possible!",
|
|
91
|
-
"it": "Aggiungi la possibilità di connettersi al server MQTT 'Fallback' conosciuto come 'Developer MQTT'. Questo server è in sola lettura - quindi nessun controllo è possibile!",
|
|
92
|
-
"es": "Añadir la posibilidad de conectarse al servidor MQTT 'Fallback' conocido como 'Developer MQTT'. Este servidor es sólo lectura - así que ningún control es posible!",
|
|
93
|
-
"pl": "Dodaj możliwość podłączenia do 'Fallback' serwera MQTT znanego jako 'Developer MQTT'. Ten serwer jest tylko read- więc nie jest możliwe sterowanie!",
|
|
94
|
-
"uk": "Додайте можливість підключення до сервера MQTT «Fallback» від «Developer MQTT». Цей сервер читає-тільки - так не можна керувати!",
|
|
95
|
-
"zh-cn": "添加连接到名为“ 开发者 MQTT” 的“ Fallback” MQTT 服务器的可能性 。 此服务器只读 - 因此无法控制 !"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|