iobroker.senec 1.6.17 → 2.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2025 NoBl <github@bluemle.org>
1
+ Copyright (c) 2020-2026 NoBl <github@bluemle.org>
2
2
 
3
3
  MIT License
4
4
 
package/README.md CHANGED
@@ -299,6 +299,10 @@ This channel contains values polled from SENEC App-API.
299
299
 
300
300
  ## Changelog
301
301
 
302
+ ### 2.0.0 (maett81, NoBl)
303
+ * Updated to use new SENEC API via mein-senec.de - Thanks to @maett81
304
+ * Some code and dependency housekeeping
305
+
302
306
  ### 1.6.17
303
307
  * License update
304
308
 
@@ -338,7 +342,7 @@ This channel contains values polled from SENEC App-API.
338
342
  ## License
339
343
  MIT License
340
344
 
341
- Copyright (c) 2025 Norbert Bluemle <github@bluemle.org>
345
+ Copyright (c) 2020-2026 Norbert Bluemle <github@bluemle.org>
342
346
 
343
347
  Permission is hereby granted, free of charge, to any person obtaining a copy
344
348
  of this software and associated documentation files (the "Software"), to deal
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "senec",
4
- "version": "1.6.17",
4
+ "version": "2.0.0",
5
5
  "news": {
6
+ "2.0.0": {
7
+ "en": "Update to use API via mein-senec.de (Thanks to maett81)",
8
+ "de": "Aktualisierung der API über mein-senec.de (Danke an maett81)",
9
+ "ru": "Использование API через mein-senec.de (перенаправлено с «Maett81»)",
10
+ "pt": "Atualizar para usar API via mein-senec.de (Graças a Maett81)",
11
+ "nl": "Update om API te gebruiken via mein-senec.de (Dankzij maett81)",
12
+ "fr": "Mise à jour pour utiliser l'API via mein-senec.de (Merci à maett81)",
13
+ "it": "Aggiornamento all'utilizzo API tramite mein-senec.de (Grazie a maett81)",
14
+ "es": "Actualización para utilizar API a través de mein-senec.de (Gracias a maett81)",
15
+ "pl": "Aktualizacja do użycia API poprzez mein- senec.de (Dzięki maett81)",
16
+ "uk": "Update to use API через mein-senec.de (Шанки до maett81)",
17
+ "zh-cn": "通过 mein-senec.de 使用 API 的更新 (谢谢Maett81)"
18
+ },
6
19
  "1.6.17": {
7
20
  "en": "License update",
8
21
  "de": "Lizenz-Update",
@@ -80,19 +93,6 @@
80
93
  "pl": "Aktualizacja licencji",
81
94
  "uk": "Оновлена ліцензія",
82
95
  "zh-cn": "更新许可证"
83
- },
84
- "1.6.11": {
85
- "en": "Moving from Senec App API 3.12.0 to 4.3.3 (thanks to oakdesign@github for providing the new API!). This WILL invalidate all current API datapoints in the Statistik branch. Easiest solution to this: Delete the Statistik branch. Remember to force a rebuild of historic data in adapter settings!",
86
- "de": "Update der Senec App API 3.12.0 auf 4.3.3 (Dank an Oakdesign@github für die Analyse der neuen API.)! Dadurch werden leider alle aktuellen API-Datenpunkte im Statistik-Zweig ungültig. Einfachste Lösung hierfür: Den gesamten Statistik-Zweig löschen. Beachten: Die Gesamthistorie muss neu berechnet werden (Parameter in den Adaptereinstellungen)!",
87
- "ru": "Переезд с Senec App API 3.12.0 до 4.3.3 (спасибо oakdesign@github для предоставления нового API.)! Это недействит все текущие точки API в филиале Statistik. Самое легкое решение для этого: Удалите филиал Statistik. Не забудьте заставить восстановить исторические данные в настройках адаптера!",
88
- "pt": "Movendo de Senec App API 3.12.0 para 4.3.3 (graças para oakdesign@github para fornecer a nova API.)! Esta WILL invalida todos os datapoints API atuais no ramo Statistik. Solução mais fácil para isso: Excluir o ramo Statistik. Lembre-se de forçar uma reconstrução de dados históricos em configurações de adaptador!",
89
- "nl": "Verhuizen van Senec App API 3.12.0 tot 4.3.3.3 uur tot Oakdesign @github voor de nieuwe API. Dit zal alle huidige API gegevens in de Statistik Branch invaliden. Verwijder de statistik tak. Vergeet niet een herbouw van historische gegevens te forceren in adapter settings!",
90
- "fr": "Déplacement de Senec App API 3.12.0 à 4.3.3 (merci à Oakdesign@github pour fournir la nouvelle API.)! Ce WILL invalide tous les points de données API actuels dans la branche Statistik. Solution la plus facile à cela: Supprimer la branche Statistik. N'oubliez pas de forcer une reconstruction des données historiques dans les réglages d'adaptateur!",
91
- "it": "Passando da Senec App API 3.12.0 a 4.3.3 (grazie a Oakdesign@github per fornire la nuova API.)! Questo WILL invalida tutti i datapoint API attuali nel ramo Statistik. Soluzione più semplice a questo: Elimina il ramo Statistik. Ricordatevi di forzare una ricostruzione dei dati storici nelle impostazioni dell'adattatore!",
92
- "es": "Moving from Senec App API 3.12.0 to 4.3.3 (gracias a oakdesign@github para proporcionar la nueva API.)! Esto invalidará todos los puntos de datos actuales de la API en la rama Statistik. Solución más fácil: Eliminar la rama Statistik. Recuerde forzar una reconstrucción de datos históricos en la configuración del adaptador!",
93
- "pl": "Przewidując się z interfejsu Senec App API 3,12.0 to 4.3.3 (dodatki do dębowego @github dla zapewnienia nowego API). WILL uszkodził wszystkie aktualne punkty danych API w oddziale Statistik. Najwięcej rozwiązań do tego: Dete the Statistik branch. Pamiętaj, aby odbudować historyczne dane w ustawieniach adapterów!",
94
- "uk": "Переміщення від Senec App API 3.12.0 до 4.3.3 (завдяки до дубdesign@github для надання нового API.)! Це недійсне використання всіх поточних точок даних API у відділенні Statistik. Найсвіжіші рішення для цього: Видаліть статутне відділення. Пам'ятайте, що змусити перебудувати історичні дані в налаштуваннях адаптера!",
95
- "zh-cn": "Moving from Senecapp API3.12.0 to 4.3.3 (as to oakdesign@github for the new API.!) 本协会在斯塔特斯科分处停止了目前所有阿联酋数据点。 最容易解决这一问题:删除斯塔特主义分支。 重新成员在适应环境中重建历史数据!"
96
96
  }
97
97
  },
98
98
  "docs": {
@@ -153,12 +153,12 @@
153
153
  },
154
154
  "dependencies": [
155
155
  {
156
- "js-controller": ">=5.0.19"
156
+ "js-controller": ">=6.0.11"
157
157
  }
158
158
  ],
159
159
  "globalDependencies": [
160
160
  {
161
- "admin": ">=5.1.13"
161
+ "admin": ">=7.6.17"
162
162
  }
163
163
  ]
164
164
  },
@@ -197,7 +197,7 @@
197
197
  "api_interval": 5,
198
198
  "api_alltimeRebuild": false
199
199
  },
200
- "nativeEncrypted": {
200
+ "encryptedNative": {
201
201
  "api_mail": "",
202
202
  "api_pwd": ""
203
203
  },
package/main.js CHANGED
@@ -12,29 +12,29 @@ const utils = require("@iobroker/adapter-core");
12
12
  const axios = require("axios").default;
13
13
  axios.defaults.headers.post["Content-Type"] = "application/json";
14
14
 
15
+ const tough = require("tough-cookie");
16
+ const cheerio = require("cheerio");
17
+ let wrapper;
18
+ (async () => {
19
+ wrapper = (await import("axios-cookiejar-support")).wrapper;
20
+ })();
21
+
15
22
  const state_attr = require(__dirname + "/lib/state_attr.js");
16
23
  const state_trans = require(__dirname + "/lib/state_trans.js");
17
- const api_trans = require(__dirname + "/lib/api_trans.js");
18
24
  const kiloList = ["W", "Wh"];
19
25
 
20
- const apiUrl = "https://app-gateway.prod.senec.dev/v1/senec";
21
- const api2Url = "https://app-gateway.prod.senec.dev/v2/senec";
22
- const apiLoginUrl = apiUrl + "/login";
23
- const apiSystemsUrl = apiUrl + "/systems";
24
- const api2SystemsUrl = api2Url + "/systems";
25
- const apiMonitorUrl = apiUrl + "/monitor";
26
+ const apiLoginUrl = "https://mein-senec.de/endkunde/oauth2/authorization/endkunde-portal";
26
27
  const apiKnownSystems = [];
27
28
 
28
29
  const batteryOn =
29
30
  '{"ENERGY":{"SAFE_CHARGE_FORCE":"u8_01","SAFE_CHARGE_PROHIBIT":"","SAFE_CHARGE_RUNNING":"","LI_STORAGE_MODE_START":"","LI_STORAGE_MODE_STOP":"","LI_STORAGE_MODE_RUNNING":"","STAT_STATE":""}}';
30
31
  const batteryOff =
31
32
  '{"ENERGY":{"SAFE_CHARGE_FORCE":"","SAFE_CHARGE_PROHIBIT":"u8_01","SAFE_CHARGE_RUNNING":"","LI_STORAGE_MODE_START":"","LI_STORAGE_MODE_STOP":"","LI_STORAGE_MODE_RUNNING":"","STAT_STATE":""}}';
32
- //const blockDischargeOn = '{"ENERGY":{"SAFE_CHARGE_FORCE":"","SAFE_CHARGE_PROHIBIT":"","SAFE_CHARGE_RUNNING":"","LI_STORAGE_MODE_START":"","LI_STORAGE_MODE_STOP":"","LI_STORAGE_MODE_RUNNING":"","STAT_STATE":""}}';
33
+ //const blockDischargeOn = '{"ENERGY":{"SAFE_CHARGE_FORCE":"","SAFE_CHARGE_PROHIBIT":"","SAFE_CHARGE_RUNNING":"","LI_STORAGE_MODE_START":"","LI_STORAGE_MODE_STOP":"","LI_STORAGE_MODE_RUNNING":"","STAT_STATE":""}}';
33
34
  //const blockDischargeOff = '{"ENERGY":{"SAFE_CHARGE_FORCE":"","SAFE_CHARGE_PROHIBIT":"","SAFE_CHARGE_RUNNING":"","LI_STORAGE_MODE_START":"","LI_STORAGE_MODE_STOP":"","LI_STORAGE_MODE_RUNNING":"","STAT_STATE":""}}';
34
35
 
35
36
  let apiConnected = false;
36
37
  let lalaConnected = false;
37
- let apiLoginToken = "";
38
38
  let connectVia = "http://";
39
39
 
40
40
  const allKnownObjects = new Set([
@@ -119,8 +119,8 @@ class Senec extends utils.Adapter {
119
119
  this.log.info("Usage of SENEC App API configured.");
120
120
  await this.initSenecAppApi();
121
121
  if (apiConnected) {
122
- await this.getApiSystems();
123
- await this.pollSenecAppApi(0); // App API
122
+ await this.getWebApiSystems();
123
+ await this.pollSenecWebApi(0); // Web API
124
124
  }
125
125
  } else {
126
126
  this.log.warn(
@@ -451,50 +451,135 @@ class Senec extends utils.Adapter {
451
451
  this.log.info("Usage of SENEC App API not configured. Not using it");
452
452
  return;
453
453
  }
454
- this.log.info("connecting to Senec App API: " + apiLoginUrl);
455
- const loginData = JSON.stringify({
456
- password: this.config.api_pwd,
457
- username: this.config.api_mail,
458
- });
454
+ /** new for WEB API */
455
+ this.log.info("Connecting to SENEC Portal (mein-senec.de) login...");
456
+
459
457
  try {
460
- const body = await this.doGet(apiLoginUrl, loginData, this, this.config.pollingTimeout, true);
461
- this.log.info("connected to Senec AppAPI.");
462
- apiLoginToken = JSON.parse(body).token;
458
+ // create cookie jar and a wrapped axios client that keeps cookies
459
+ const jar = new tough.CookieJar();
460
+ const webClient = wrapper(
461
+ axios.create({
462
+ jar,
463
+ withCredentials: true,
464
+ //httpsAgent: agent,
465
+ headers: {
466
+ "User-Agent": "Mozilla/5.0 (iobroker-senec-adapter)",
467
+ },
468
+ timeout: this.config.pollingTimeout || 5000,
469
+ maxRedirects: 5,
470
+ validateStatus: () => true,
471
+ }),
472
+ );
473
+
474
+ // STEP 1: fetch login page
475
+ this.log.info("STEP 1: fetch login portal page...");
476
+ const resp1 = await webClient.get(apiLoginUrl);
477
+ this.log.debug("Portal GET status: " + resp1.status);
478
+ this.log.silly(
479
+ "Portal GET response url: " + (resp1.request && resp1.request.res ? resp1.request.res.responseUrl : ""),
480
+ );
481
+
482
+ // parse form
483
+ const $ = cheerio.load(resp1.data);
484
+ const form = $("form#kc-form-login");
485
+ if (!form.length) {
486
+ throw new Error("Login form not found on portal page");
487
+ }
488
+ const actionAttr = form.attr("action");
489
+ const actionUrl = new URL(actionAttr, resp1.request.res.responseUrl).href;
490
+
491
+ // collect hidden inputs
492
+ const formData = {};
493
+ form.find("input").each((i, el) => {
494
+ const name = $(el).attr("name");
495
+ const value = $(el).attr("value") || "";
496
+ if (name) formData[name] = value;
497
+ });
498
+ formData.username = this.config.api_mail;
499
+ formData.password = this.config.api_pwd;
500
+
501
+ this.log.debug("Posting login to Keycloak action URL: " + actionUrl);
502
+
503
+ const resp2 = await webClient.post(actionUrl, new URLSearchParams(formData).toString(), {
504
+ headers: {
505
+ "Content-Type": "application/x-www-form-urlencoded",
506
+ Referer: resp1.request.res.responseUrl,
507
+ },
508
+ maxRedirects: 5,
509
+ validateStatus: () => true,
510
+ });
511
+
512
+ this.log.debug("Login POST status: " + resp2.status);
513
+ // check cookies in jar
514
+ const cookies = await jar.getCookies("https://mein-senec.de");
515
+ this.log.debug("Cookies after login: " + cookies.map((c) => c.cookieString()).join("; "));
516
+
517
+ if (resp2.status >= 400) {
518
+ throw new Error("Portal login failed: " + resp2.status);
519
+ }
520
+
521
+ // store web client for subsequent portal requests
522
+ this.senecWebClient = webClient;
523
+ this.senecWebJar = jar;
463
524
  apiConnected = true;
464
- axios.defaults.headers.get["authorization"] = apiLoginToken;
525
+ this.log.info("Connected to SENEC Portal via web login.");
465
526
  } catch (error) {
466
527
  apiConnected = false;
467
- throw new Error("Error connecting to Senec AppAPI. Exiting! (" + error + ").");
528
+ this.log.error("Error connecting to SENEC Portal: " + error);
529
+ throw new Error("Error connecting to SENEC Portal. (" + error + ")");
468
530
  }
531
+ /** end new WEB API */
469
532
  }
470
533
 
471
534
  /**
472
535
  * Reads system data from senec app api
536
+ *
537
+ * Replaced: reads portal status overview and creates _api.Portal.Overview
473
538
  */
474
- async getApiSystems() {
475
- const pfx = "_api.Anlagen.";
476
- if (!this.config.api_use || !apiConnected) {
477
- this.log.info("Usage of SENEC App API not configured or not connected.");
539
+ async getWebApiSystems() {
540
+ const pfx = "_api.Portal.Profile.";
541
+ if (!this.config.api_use || !apiConnected || !this.senecWebClient) {
542
+ this.log.info("Usage of SENEC Portal not configured or not connected.");
478
543
  return;
479
544
  }
480
- this.log.info("Reading Systems Information from Senec App API " + apiSystemsUrl);
545
+ this.log.info("Reading Systems Information from SENEC Portal (status overview)");
546
+
547
+ const PROFILE_SETTINGS = "https://mein-senec.de/endkunde/api/settings/getProfileSettings";
548
+
481
549
  try {
482
- const body = await this.doGet(apiSystemsUrl, "", this, this.config.pollingTimeout, false);
483
- this.log.info("Read Systems Information from Senec AppAPI.");
484
- const obj = JSON.parse(body);
550
+ const resp = await this.senecWebClient.get(PROFILE_SETTINGS);
551
+ if (resp.status !== 200) {
552
+ throw new Error("Profile Settings returned HTTP " + resp.status);
553
+ }
554
+ const obj = resp.data;
555
+
556
+ // try to populate profile settings
557
+ apiKnownSystems.length = 0;
558
+
485
559
  for (const [key, value] of Object.entries(obj)) {
486
- this.log.debug("ApiPull: " + key + ":" + JSON.stringify(value));
487
- const systemId = value.id;
488
- apiKnownSystems.push(systemId);
489
- for (const [key2, value2] of Object.entries(value)) {
490
- if (typeof value2 === "object")
491
- await this.doState(pfx + systemId + "." + key2, JSON.stringify(value2), "", "", false);
492
- else await this.doState(pfx + systemId + "." + key2, value2, "", "", false);
560
+ if (key == "id") apiKnownSystems.push(value);
561
+ if (key == "land" || key == "sprache") {
562
+ for (const [key2, value2] of Object.entries(value)) {
563
+ this.log.debug("profileSetting: " + pfx + key + "." + key2 + ":" + value);
564
+ await this.doState(pfx + key + "." + key2, JSON.stringify(value2), "", "", false);
565
+ }
566
+ } else {
567
+ this.log.debug("profileSetting: " + pfx + key + ":" + value);
568
+ await this.doState(pfx + key, value, "", "", false);
493
569
  }
494
570
  }
495
- await this.doState(pfx + "IDs", JSON.stringify(apiKnownSystems), "Anlagen IDs", "", false);
571
+
572
+ /*
573
+ ** ensure uniqueness, if there are multiple systems
574
+ const uniq = [...new Set(apiKnownSystems)];
575
+ apiKnownSystems.length = 0;
576
+ uniq.forEach((x) => apiKnownSystems.push(x));
577
+ */
578
+
579
+ this.log.info("Detected systems from portal overview: " + JSON.stringify(apiKnownSystems));
580
+ //await this.doState(pfx + "IDs", JSON.stringify(apiKnownSystems), "Portal detected system IDs", "", false);
496
581
  } catch (error) {
497
- throw new Error("Error reading Systems Information from Senec AppAPI. (" + error + ").");
582
+ throw new Error("Error reading Systems Information from SENEC Portal. (" + error + ").");
498
583
  }
499
584
  }
500
585
 
@@ -573,6 +658,7 @@ class Senec extends utils.Adapter {
573
658
  this.log.info("(Poll) Double escapes autofixed! Body out: " + body);
574
659
  }
575
660
  const obj = JSON.parse(body, reviverNumParse);
661
+ this.log.debug("(Poll) Parsed object: " + JSON.stringify(obj));
576
662
  await this.evalPoll(obj);
577
663
 
578
664
  retry = 0;
@@ -618,61 +704,103 @@ class Senec extends utils.Adapter {
618
704
  }
619
705
 
620
706
  /**
621
- * Read values from Senec App API
707
+ * Read values from Senec App API (replaced to query the mein-senec portal endpoints)
622
708
  */
623
- async pollSenecAppApi(retry) {
624
- if (!this.config.api_use || !apiConnected) {
625
- this.log.info("Usage of SENEC App API not configured or not connected.");
709
+ async pollSenecWebApi(retry) {
710
+ if (!this.config.api_use || !apiConnected || !this.senecWebClient) {
711
+ this.log.info("Usage of SENEC Portal not configured or not connected.");
626
712
  return;
627
713
  }
714
+
628
715
  const interval = this.config.api_interval * 60000;
629
- const dates = new Map([
630
- ["THIS_DAY", new Date().toISOString().split("T")[0]],
631
- ["LAST_DAY", new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split("T")[0]],
632
- ["THIS_MONTH", new Date().toISOString().split("T")[0]],
633
- ["LAST_MONTH", new Date(new Date().setDate(0)).toISOString().split("T")[0]],
634
- ["THIS_YEAR", new Date().toISOString().split("T")[0]],
635
- ["LAST_YEAR", new Date(new Date().getFullYear() - 1, 1, 1).toISOString().split("T")[0]],
636
- ]);
637
-
638
- this.log.debug("Polling API ...");
639
- let body = "";
716
+ const base = "https://mein-senec.de/endkunde/api/status";
717
+ const endpoints = {
718
+ statusoverview: base + "/getstatusoverview.php?anlageNummer=0",
719
+ technischeDaten: base + "/technischeDaten?anlageNummer=0",
720
+ status24: base + "/getstatus24.php?anlageNummer=0",
721
+ autarky: base + "/getautarky.php?anlageNummer=0",
722
+ accustate: base + "/getaccustate.php?anlageNummer=0",
723
+ accusavings: base + "/getaccusavings.php?anlageNummer=0",
724
+ };
725
+ // also interesting endpoints:
726
+
727
+ // also prepare getstatus types
728
+ const statusTypes = ["accuexport", "accuimport", "gridexport", "gridimport", "powergenerated", "consumption"];
729
+
730
+ this.log.debug("Polling Portal API ...");
731
+
640
732
  try {
641
- for (let i = 0; i < apiKnownSystems.length; i++) {
642
- // const baseUrl = apiSystemsUrl + "/" + apiKnownSystems[i];
643
- const baseUrl = api2SystemsUrl + "/" + apiKnownSystems[i];
644
- const baseUrlMonitor = apiMonitorUrl + "/" + apiKnownSystems[i];
645
- let url = "";
646
- const tzObj = await this.getStateAsync("_api.Anlagen." + apiKnownSystems[i] + ".zeitzone");
647
- const tz = tzObj ? encodeURIComponent(tzObj.val) : encodeURIComponent("Europe/Berlin");
648
-
649
- // dashboard
650
- url = baseUrl + "/dashboard";
651
- body = await this.doGet(url, "", this, this.config.pollingTimeout, false);
652
- await this.decodeDashboard(apiKnownSystems[i], JSON.parse(body));
653
-
654
- for (const [key, value] of dates.entries()) {
655
- // statistik for period
656
- url =
657
- baseUrlMonitor +
658
- "/data?period=" +
659
- api_trans[key].api +
660
- "&date=" +
661
- value +
662
- "&locale=de_DE&timezone=" +
663
- tz;
664
- body = await this.doGet(url, "", this, this.config.pollingTimeout, false);
665
- await this.decodeStatistik(apiKnownSystems[i], JSON.parse(body), api_trans[key].dp);
733
+ // statusoverview
734
+ let r = await this.senecWebClient.get(endpoints.statusoverview);
735
+ if (r.status === 200) {
736
+ await this.decodeStatusOverview(r.data);
737
+ } else {
738
+ this.log.warn("statusoverview returned HTTP " + r.status);
739
+ }
740
+
741
+ // technischeDaten
742
+ r = await this.senecWebClient.get(endpoints.technischeDaten);
743
+ if (r.status === 200) {
744
+ await this.decodeTechnischeDaten(r.data);
745
+ } else {
746
+ this.log.warn("technischeDaten returned HTTP " + r.status);
747
+ }
748
+
749
+ // status24
750
+ r = await this.senecWebClient.get(endpoints.status24);
751
+ if (r.status === 200) {
752
+ await this.decodeStatus24(r.data);
753
+ } else {
754
+ this.log.warn("stats24 returned HTTP " + r.status);
755
+ }
756
+
757
+ // autarky
758
+ r = await this.senecWebClient.get(endpoints.autarky);
759
+ if (r.status === 200) {
760
+ await this.decodeAutarky(r.data);
761
+ } else {
762
+ this.log.warn("autarky returned HTTP " + r.status);
763
+ }
764
+
765
+ // accustate
766
+ r = await this.senecWebClient.get(endpoints.accustate);
767
+ if (r.status === 200) {
768
+ await this.decodeAccuState(r.data);
769
+ } else {
770
+ this.log.warn("accustate returned HTTP " + r.status);
771
+ }
772
+
773
+ // accusavings
774
+ r = await this.senecWebClient.get(endpoints.accusavings);
775
+ if (r.status === 200) {
776
+ await this.decodeAccuSavings(r.data);
777
+ } else {
778
+ this.log.warn("accusavings returned HTTP " + r.status);
779
+ }
780
+
781
+ // getstatus with many types
782
+ for (let i = 0; i < statusTypes.length; i++) {
783
+ const t = statusTypes[i];
784
+ const url = base + "/getstatus.php?type=" + encodeURIComponent(t) + "&period=all&anlageNummer=0";
785
+ try {
786
+ const res = await this.senecWebClient.get(url);
787
+ if (res.status === 200) {
788
+ await this.decodeStatus(res.data, t);
789
+ } else {
790
+ this.log.warn("getstatus(" + t + ") returned HTTP " + res.status);
791
+ }
792
+ } catch (err) {
793
+ this.log.warn("Error fetching getstatus(" + t + "): " + err);
666
794
  }
667
- if (this.config.api_alltimeRebuild) await this.rebuildAllTimeHistory(apiKnownSystems[i]);
668
795
  }
796
+
669
797
  retry = 0;
670
798
  if (unloaded) return;
671
- this.timerAPI = setTimeout(() => this.pollSenecAppApi(retry), interval);
799
+ this.timerAPI = setTimeout(() => this.pollSenecWebApi(retry), interval);
672
800
  } catch (error) {
673
801
  if (retry == this.config.retries && this.config.retries < 999) {
674
802
  this.log.error(
675
- "Error reading from Senec AppAPI. Retried " +
803
+ "Error reading from SENEC Portal. Retried " +
676
804
  retry +
677
805
  " times. Giving up now. Check config and restart adapter. (" +
678
806
  error +
@@ -682,101 +810,266 @@ class Senec extends utils.Adapter {
682
810
  } else {
683
811
  retry += 1;
684
812
  this.log.warn(
685
- "Error reading from Senec AppAPI. Retry " +
813
+ "Error reading from SENEC Portal. Retry " +
686
814
  retry +
687
815
  "/" +
688
816
  this.config.retries +
689
817
  " in " +
690
- (interval * this.config.retrymultiplier * retry) / 1000 +
818
+ (this.config.api_interval * 60000 * this.config.retrymultiplier * retry) / 1000 +
691
819
  " seconds! (" +
692
820
  error +
693
821
  ")",
694
822
  );
695
823
  this.timerAPI = setTimeout(
696
- () => this.pollSenecAppApi(retry),
697
- interval * this.config.retrymultiplier * retry,
824
+ () => this.pollSenecWebApi(retry),
825
+ this.config.api_interval * 60000 * this.config.retrymultiplier * retry,
698
826
  );
699
827
  }
700
828
  }
701
829
  }
702
830
 
703
831
  /**
704
- * Decodes Dashboard information from SENEC App API
832
+ * Decodes StatusOverview from WebAPI
705
833
  */
706
- async decodeDashboard(system, obj) {
707
- const pfx = "_api.Anlagen." + system + ".Dashboard.";
834
+ async decodeStatusOverview(obj) {
835
+ const pfx = "_api.Portal.StatusOverview.";
836
+ // store raw data
837
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Status Overview", "", false);
708
838
  for (const [key, value] of Object.entries(obj)) {
709
- this.log.debug("(decodeDashboard) Key: " + key + " - Value:" + JSON.stringify(value));
710
- if (key == "timestamp" || key == "electricVehicleConnected") {
711
- await this.doState(pfx + key, value, "", "", false);
712
- } else {
839
+ if (
840
+ key == "wartungsplan" ||
841
+ key === "gridimport" ||
842
+ key === "gridexport" ||
843
+ key === "powergenerated" ||
844
+ key === "consumption" ||
845
+ key === "accuexport" ||
846
+ key === "accuimport" ||
847
+ key === "acculevel"
848
+ ) {
713
849
  for (const [key2, value2] of Object.entries(value)) {
714
- this.log.debug("(decodeDashboard) Key2: " + key2 + " - Value: " + JSON.stringify(value2));
715
- const keyParts = ParseApi2KeyParts(key2);
716
- await this.doState(pfx + key + "." + key2, Number(value2.toFixed(2)), "", keyParts.unit, false);
717
- if (kiloList.includes(keyParts.unit)) {
718
- await this.doState(
719
- pfx + key + "." + keyParts.prefix + " (k" + keyParts.unit + ")",
720
- Number((value2 / 1000).toFixed(2)),
721
- "",
722
- "k" + keyParts.unit,
723
- false,
724
- );
725
- }
850
+ if (key2 == "possibleMaintenanceTypes") continue; // skip this one
851
+ this.log.debug("decodeStatusOverview: " + pfx + key + "." + key2 + ":" + value);
852
+ await this.doState(
853
+ pfx + key + "." + key2,
854
+ ValueTyping(key2, JSON.stringify(value2)),
855
+ "",
856
+ "",
857
+ false,
858
+ );
726
859
  }
860
+ } else if (key === "lastupdated") {
861
+ const date = new Date(value);
862
+ this.log.debug("decodeStatusOverview: " + pfx + key + ":" + date.toString());
863
+ await this.doState(pfx + key, date.toString(), "", "", false);
864
+ } else {
865
+ if (key === "suppressedNotificationIds") continue; // skip this one - empty array
866
+ this.log.debug("decodeStatusOverview: " + pfx + key + ":" + value);
867
+ await this.doState(pfx + key, ValueTyping(key, value), "", "", false);
727
868
  }
728
869
  }
729
870
  }
730
871
 
731
872
  /**
732
- * Decodes Statistik information from SENEC App API
873
+ * Decodes technischeDaten from WebAPI
733
874
  */
734
- async decodeStatistik(system, obj, period) {
735
- if (obj == null || obj == undefined || obj.aggregation == null || obj.aggregation == undefined) return; // could happen (e.g.) if we pull information for "last year" when the appliance isn't that old yet
736
- const pfx = "_api.Anlagen." + system + ".Statistik." + period + ".";
737
- for (const [key, value] of Object.entries(obj.aggregation)) {
738
- this.log.debug("decodeStatistic: " + pfx + key + ":" + value);
739
- // only reading 'aggregation' - no interest in fine granular information
740
- if (key == "startDate") {
875
+ async decodeTechnischeDaten(obj) {
876
+ const pfx = "_api.Portal.TechnischeDaten.";
877
+ // store raw data
878
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Status Overview", "", false);
879
+ for (const [key, value] of Object.entries(obj)) {
880
+ if (key == "installationsdatum") {
881
+ const date = new Date(value);
882
+ this.log.debug("decodeTechnischeDaten: " + pfx + key + ":" + date.toString());
883
+ await this.doState(pfx + key, date.toString(), "", "", false);
884
+ } else {
885
+ this.log.debug("decodeTechnischeDaten: " + pfx + key + ":" + value);
741
886
  await this.doState(pfx + key, value, "", "", false);
887
+ }
888
+ }
889
+ }
890
+
891
+ /**
892
+ * Decodes Status24 from WebAPI
893
+ */
894
+ async decodeStatus24(obj) {
895
+ const pfx = "_api.Portal.Status24.";
896
+ // store raw data
897
+ await this.doState(pfx + "json", JSON.stringify(obj), "Portal Status24", "", false);
898
+ for (const [key, value] of Object.entries(obj)) {
899
+ if (key === "val") {
900
+ const accuExportArr = value[0];
901
+ const accuImportArr = value[1];
902
+ const gridExportArr = value[2];
903
+ const gridImportArr = value[3];
904
+ const powergeneratedArr = value[4];
905
+ const consumptionArr = value[5];
906
+
907
+ // AccuExport
908
+ let i = 0;
909
+ for (const [ts, val] of accuExportArr) {
910
+ i++;
911
+ const dateStr = new Date(ts).toString();
912
+ await this.doState(pfx + "AccuExport." + i + ".ts", dateStr, "Timestampe", "", false);
913
+ await this.doState(pfx + "AccuExport." + i + ".value", Number(val.toFixed(3)), "", "", false);
914
+ }
915
+ await this.doState(pfx + "AccuExport.json", JSON.stringify(accuExportArr), "", "", false);
916
+
917
+ // AccuImport
918
+ i = 0;
919
+ for (const [ts, val] of accuImportArr) {
920
+ i++;
921
+ const dateStr = new Date(ts).toString();
922
+ await this.doState(pfx + "AccuImport." + i + ".ts", dateStr, "Timestampe", "", false);
923
+ await this.doState(pfx + "AccuImport." + i + ".value", Number(val.toFixed(3)), "", "", false);
924
+ }
925
+ await this.doState(pfx + "AccuImport.json", JSON.stringify(accuImportArr), "", "", false);
926
+
927
+ // GridExport
928
+ i = 0;
929
+ for (const [ts, val] of gridExportArr) {
930
+ i++;
931
+ const dateStr = new Date(ts).toString();
932
+ await this.doState(pfx + "GridExport." + i + ".ts", dateStr, "Timestampe", "", false);
933
+ await this.doState(pfx + "GridExport." + i + ".value", Number(val.toFixed(3)), "", "", false);
934
+ }
935
+ await this.doState(pfx + "GridExport.json", JSON.stringify(gridExportArr), "", "", false);
936
+
937
+ // GridImport
938
+ i = 0;
939
+ for (const [ts, val] of gridImportArr) {
940
+ i++;
941
+ const dateStr = new Date(ts).toString();
942
+ await this.doState(pfx + "GridImport." + i + ".ts", dateStr, "Timestampe", "", false);
943
+ await this.doState(pfx + "GridImport." + i + ".value", Number(val.toFixed(3)), "", "", false);
944
+ }
945
+ await this.doState(pfx + "GridImport.json", JSON.stringify(gridImportArr), "", "", false);
946
+
947
+ // PowerGenerated
948
+ i = 0;
949
+ for (const [ts, val] of powergeneratedArr) {
950
+ i++;
951
+ const dateStr = new Date(ts).toString();
952
+ await this.doState(pfx + "PowerGenerated." + i + ".ts", dateStr, "Timestampe", "", false);
953
+ await this.doState(pfx + "PowerGenerated." + i + ".value", Number(val.toFixed(3)), "", "", false);
954
+ }
955
+ await this.doState(pfx + "PowerGenerated.json", JSON.stringify(powergeneratedArr), "", "", false);
956
+
957
+ // Consumption
958
+ i = 0;
959
+ for (const [ts, val] of consumptionArr) {
960
+ i++;
961
+ const dateStr = new Date(ts).toString();
962
+ await this.doState(pfx + "Consumption." + i + ".ts", dateStr, "Timestamp", "", false);
963
+ await this.doState(pfx + "Consumption." + i + ".value", Number(val.toFixed(3)), "", "", false);
964
+ }
965
+ await this.doState(pfx + "Consumption.json", JSON.stringify(consumptionArr), "", "", false);
742
966
  } else {
743
- if (!this.config.api_alltimeRebuild) {
744
- // don't update DPs if we are AllTime-Rebuild-Process
745
- await this.doState(pfx + key, Number(value.value.toFixed(2)), "", value.unit, false);
746
- if (kiloList.includes(value.unit)) {
747
- await this.doState(
748
- pfx + key + " (k" + value.unit + ")",
749
- Number((value.value / 1000).toFixed(2)),
750
- "",
751
- "k" + value.unit,
752
- false,
753
- );
754
- }
967
+ await this.doState(pfx + key, ValueTyping(key, value), "", "", false);
968
+ }
969
+ }
970
+ }
971
+
972
+ /**
973
+ * Decodes decodeAutarky from WebAPI *
974
+ */
975
+ async decodeAutarky(obj) {
976
+ const pfx = "_api.Portal.Autarky.";
977
+ // store raw data
978
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Autarky", "", false);
979
+ for (const [key, value] of Object.entries(obj)) {
980
+ this.log.debug("decodeAutarky: " + pfx + key + ":" + value);
981
+ await this.doState(pfx + key, ValueTyping(key, value), "", "%", false);
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Decodes AccuState from WebAPI
987
+ */
988
+ async decodeAccuState(obj) {
989
+ const pfx = "_api.Portal.AccuState.";
990
+ // store raw json
991
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Accu State", "", false);
992
+ for (const [key, value] of Object.entries(obj)) {
993
+ if (key === "val") {
994
+ const voltageArr = value[0];
995
+ const currentArr = value[1];
996
+
997
+ // Voltage
998
+ let i = 0;
999
+ for (const [ts, val] of voltageArr) {
1000
+ i++;
1001
+ const dateStr = new Date(ts).toString();
1002
+ await this.doState(pfx + "Voltage." + i + ".ts", dateStr, "Timestamp", "", false);
1003
+ await this.doState(pfx + "Voltage." + i + ".value", Number(val.toFixed(3)), "Voltage", "V", false);
755
1004
  }
756
- if (period == api_trans["THIS_YEAR"].dp)
757
- await this.insertAllTimeHistory(
758
- system,
759
- key,
760
- new Date(obj.aggregation.startDate).getFullYear(),
761
- Number(value.value.toFixed(0)),
762
- value.unit,
763
- );
1005
+
1006
+ // Current
1007
+ i = 0;
1008
+ for (const [ts, val] of currentArr) {
1009
+ i++;
1010
+ const dateStr = new Date(ts).toString();
1011
+ await this.doState(pfx + "Current." + i + ".ts", dateStr, "Timestampe", "", false);
1012
+ await this.doState(pfx + "Current." + i + ".value", Number(val.toFixed(3)), "Power", "A", false);
1013
+ }
1014
+ } else if (key === "lastupdated") {
1015
+ await this.doState(pfx + key, new Date(value).toString(), "Last updated", "", false);
1016
+ } else {
1017
+ await this.doState(pfx + key, ValueTyping(key, value), "", "", false);
764
1018
  }
765
1019
  }
766
- if (obj.aggregation.totalUsage.value != 0) {
767
- const autarky = Number(
768
- (
769
- ((obj.aggregation.generation.value -
770
- obj.aggregation.gridFeedIn.value -
771
- obj.aggregation.storageLoad.value +
772
- obj.aggregation.storageConsumption.value) /
773
- obj.aggregation.totalUsage.value) *
774
- 100
775
- ).toFixed(2),
776
- );
777
- await this.doState(pfx + "Autarkie", autarky, "", "%", false);
1020
+ }
1021
+
1022
+ /**
1023
+ * Decodes AccuSavings from WebAPI
1024
+ */
1025
+ async decodeAccuSavings(obj) {
1026
+ const pfx = "_api.Portal.AccuSavings.";
1027
+ // store raw json
1028
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Accu Savings", "", false);
1029
+ for (const [key, value] of Object.entries(obj)) {
1030
+ if (key == "lastupdated") {
1031
+ await this.doState(pfx + key, new Date(value).toString(), "Last Update", "", false);
1032
+ } else {
1033
+ await this.doState(pfx + key, ValueTyping(key, value), "", "", false);
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * Decodes Status from WebAPI
1040
+ */
1041
+ async decodeStatus(obj, typ) {
1042
+ const pfx = "_api.Portal.Status." + typ + ".";
1043
+ // store raw json
1044
+ //await this.doState(pfx + "_json", JSON.stringify(obj), "Portal Status", "", false);
1045
+ for (const [key, value] of Object.entries(obj)) {
1046
+ if (key == "val") {
1047
+ // includes six arrays for the last 24 hours for different metrics (order? : accu import, accu export, grid import, grid export, power generated, consumption)
1048
+ const yearly = await this.decodeYearlyValues(value);
1049
+ for (const [year, aggregation] of Object.entries(yearly)) {
1050
+ await this.doState(pfx + year, Number(aggregation.toFixed(3)), "", "", false);
1051
+ }
1052
+ } else if (key == "lastupdated") {
1053
+ await this.doState(pfx + key, new Date(value).toString(), "Last Updated", "", false);
1054
+ } else {
1055
+ await this.doState(pfx + key, ValueTyping(key, value), "", "", false);
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Converts SENEC yearly arrays into {year: value} object.
1062
+ */
1063
+ async decodeYearlyValues(val) {
1064
+ if (!val || !Array.isArray(val)) return {};
1065
+
1066
+ const result = {};
1067
+
1068
+ for (const [ts, value] of val) {
1069
+ const year = new Date(ts).getFullYear(); // get year from timestamp
1070
+ result[year] = Number(value.toFixed(3)); // round to 3 decimal places
778
1071
  }
779
- await this.updateAllTimeHistory(system);
1072
+ return result;
780
1073
  }
781
1074
 
782
1075
  /**
@@ -835,40 +1128,6 @@ class Senec extends utils.Adapter {
835
1128
  }
836
1129
  }
837
1130
 
838
- /**
839
- * Rebuilds AllTimeHistory from SENEC App API
840
- */
841
- async rebuildAllTimeHistory(system) {
842
- if (!this.config.api_use || !apiConnected) {
843
- this.log.info("Usage of SENEC App API not configured or not connected.");
844
- return;
845
- }
846
-
847
- this.log.info("Rebuilding AllTime History ...");
848
- let year = new Date(new Date().getFullYear() - 1, 1, 1).toISOString().split("T")[0]; // starting last year, because we already got current year covered
849
- let body = "";
850
- try {
851
- while (new Date(year).getFullYear() > 2008) {
852
- // senec was founded in 2009 by Mathias Hammer as Deutsche Energieversorgung GmbH (DEV) - so no way we have older data :)
853
- this.log.info("Rebuilding AllTime History - Year: " + new Date(year).getFullYear());
854
- const baseUrl = apiMonitorUrl + "/" + system;
855
- let url = "";
856
- const tzObj = await this.getStateAsync("_api.Anlagen." + system + ".zeitzone");
857
- const tz = tzObj ? encodeURIComponent(tzObj.val) : encodeURIComponent("Europe/Berlin");
858
- url = baseUrl + "/data?period=YEAR&date=" + year + "&locale=de_DE&timezone=" + tz;
859
- this.log.debug("Polling: " + url);
860
- body = await this.doGet(url, "", this, this.config.pollingTimeout, false);
861
- await this.decodeStatistik(system, JSON.parse(body), api_trans["THIS_YEAR"].dp);
862
- year = new Date(new Date(year).getFullYear() - 1, 1, 1).toISOString().split("T")[0];
863
- if (unloaded) return;
864
- }
865
- } catch (error) {
866
- this.log.info("Rebuild ended: " + error);
867
- }
868
- this.log.info("Restarting ...");
869
- this.extendForeignObject(`system.adapter.${this.namespace}`, { native: { api_alltimeRebuild: false } });
870
- }
871
-
872
1131
  /**
873
1132
  * sets a state's value and creates the state if it doesn't exist yet
874
1133
  */
@@ -1016,24 +1275,24 @@ const ValueTyping = (key, value) => {
1016
1275
  }
1017
1276
  };
1018
1277
 
1019
- const ParseApi2KeyParts = (key) => {
1020
- //const match = key.match(/In([A-Za-z]+)$/);
1021
- //var unit = match ? match[1] : "";
1022
- //if (unit == "Percent") unit = "%";
1023
- //return unit;
1024
- const match = key.match(/^(.*)In([A-Za-z]+)$/);
1025
- if (match) {
1026
- return {
1027
- prefix: match[1], // part before "In"
1028
- unit: match[2] === "Percent" ? "%" : match[2], // replace "Percent" with "%"
1029
- };
1030
- }
1031
- return {
1032
- // default response for error
1033
- prefix: "unknownKey",
1034
- unit: "",
1035
- };
1036
- };
1278
+ // const ParseApi2KeyParts = (key) => {
1279
+ // //const match = key.match(/In([A-Za-z]+)$/);
1280
+ // //var unit = match ? match[1] : "";
1281
+ // //if (unit == "Percent") unit = "%";
1282
+ // //return unit;
1283
+ // const match = key.match(/^(.*)In([A-Za-z]+)$/);
1284
+ // if (match) {
1285
+ // return {
1286
+ // prefix: match[1], // part before "In"
1287
+ // unit: match[2] === "Percent" ? "%" : match[2], // replace "Percent" with "%"
1288
+ // };
1289
+ // }
1290
+ // return {
1291
+ // // default response for error
1292
+ // prefix: "unknownKey",
1293
+ // unit: "",
1294
+ // };
1295
+ // };
1037
1296
 
1038
1297
  /**
1039
1298
  * Converts float value in hex format to js float32.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.senec",
3
- "version": "1.6.17",
3
+ "version": "2.0.0",
4
4
  "description": "Senec Home",
5
5
  "author": {
6
6
  "name": "NoBl",
@@ -28,39 +28,32 @@
28
28
  "url": "https://github.com/nobl/ioBroker.senec.git"
29
29
  },
30
30
  "dependencies": {
31
- "axios": "^1.7.8",
32
- "@iobroker/adapter-core": "^3.2.3"
31
+ "@iobroker/adapter-core": "^3.3.2",
32
+ "axios": "^1.13.4",
33
+ "axios-cookiejar-support": "^6.0.5",
34
+ "cheerio": "^1.2.0",
35
+ "tough-cookie": "^6.0.0"
33
36
  },
34
37
  "devDependencies": {
35
- "@alcalzone/release-script": "^3.8.0",
36
- "@alcalzone/release-script-plugin-iobroker": "^3.7.2",
37
- "@alcalzone/release-script-plugin-license": "^3.7.0",
38
- "@alcalzone/release-script-plugin-manual-review": "^3.7.0",
39
- "@iobroker/adapter-dev": "^1.3.0",
40
- "@iobroker/testing": "^5.0.0",
41
- "@eslint/eslintrc": "^3.2.0",
42
- "@eslint/js": "^9.17.0",
43
- "@tsconfig/node20": "^20.1.4",
44
- "@types/chai": "^4.3.19",
45
- "@types/chai-as-promised": "^8.0.1",
46
- "@types/mocha": "^10.0.10",
47
- "@types/node": "^22.12.0",
38
+ "@alcalzone/release-script": "^5.0.0",
39
+ "@alcalzone/release-script-plugin-iobroker": "^4.0.0",
40
+ "@alcalzone/release-script-plugin-license": "^4.0.0",
41
+ "@alcalzone/release-script-plugin-manual-review": "^4.0.0",
42
+ "@iobroker/adapter-dev": "^1.5.0",
43
+ "@iobroker/testing": "^5.2.2",
44
+ "@eslint/eslintrc": "^3.3.3",
45
+ "@eslint/js": "^9.39.2",
46
+ "@tsconfig/node20": "^20.1.8",
47
+ "@types/node": "^25.2.0",
48
48
  "@types/proxyquire": "^1.3.31",
49
- "@types/sinon": "^17.0.3",
50
- "@types/sinon-chai": "^3.2.12",
51
- "chai": "^5.1.2",
52
- "chai-as-promised": "^8.0.1",
53
- "eslint": "^9.17.0",
54
- "eslint-config-prettier": "^10.0.1",
55
- "eslint-plugin-prettier": "^5.2.1",
56
- "globals": "^15.14.0",
57
- "mocha": "^11.0.1",
49
+ "eslint": "^9.39.2",
50
+ "eslint-config-prettier": "^10.1.8",
51
+ "eslint-plugin-prettier": "^5.5.5",
52
+ "globals": "^17.3.0",
58
53
  "mustache": "^4.2.0",
59
- "prettier": "^3.4.2",
54
+ "prettier": "^3.8.1",
60
55
  "proxyquire": "^2.1.3",
61
- "sinon": "^19.0.2",
62
- "sinon-chai": "^4.0.0",
63
- "typescript": "~5.7.3"
56
+ "typescript": "~5.9.3"
64
57
  },
65
58
  "main": "main.js",
66
59
  "files": [