From 30606e23c60b2f15f42134ef18bb3f3653bbed11 Mon Sep 17 00:00:00 2001 From: meMorizE Date: Fri, 28 Nov 2025 10:37:52 +0100 Subject: [PATCH] New Energy Driver: MakeSkyBlue Solar Charger Interface (xnrg_26) (#24151) * Add MakeSkyBlue Solar Charge Controller (as Energy Device ) * only cosmetics, no functional change * index on MakeSkyBlue: ad1b8de93 only cosmetics, no functional change * New serial handler without time relation (long-term error), several cleanup * Optimization and cleanup * Merge * increment xnrg index (25 used meanwhile) * Major rework: Phase usage, Initialization, Mutli-Serial handling and more * Change identifier from _GPIO_ to the more applicable _SENSOR_ * PR candidate 1 * fix UserSelectablePins and kSensorNames (add to end), river disabled as default at my_user_config.h * add missing defines to lt_LT language header file --- tasmota/include/tasmota_template.h | 13 +- tasmota/language/af_AF.h | 2 + tasmota/language/bg_BG.h | 2 + tasmota/language/ca_AD.h | 2 + tasmota/language/cs_CZ.h | 2 + tasmota/language/de_DE.h | 2 + tasmota/language/el_GR.h | 2 + tasmota/language/en_GB.h | 2 + tasmota/language/es_ES.h | 2 + tasmota/language/fr_FR.h | 2 + tasmota/language/fy_NL.h | 2 + tasmota/language/he_HE.h | 2 + tasmota/language/hu_HU.h | 2 + tasmota/language/it_IT.h | 2 + tasmota/language/ko_KO.h | 2 + tasmota/language/lt_LT.h | 2 + tasmota/language/nl_NL.h | 2 + tasmota/language/pl_PL.h | 2 + tasmota/language/pt_BR.h | 2 + tasmota/language/pt_PT.h | 2 + tasmota/language/ro_RO.h | 2 + tasmota/language/ru_RU.h | 2 + tasmota/language/sk_SK.h | 2 + tasmota/language/sv_SE.h | 2 + tasmota/language/tr_TR.h | 2 + tasmota/language/uk_UA.h | 2 + tasmota/language/vi_VN.h | 2 + tasmota/language/zh_CN.h | 2 + tasmota/language/zh_TW.h | 2 + tasmota/my_user_config.h | 2 + .../xnrg_26_mk_sky_blu.ino | 858 ++++++++++++++++++ 31 files changed, 928 insertions(+), 1 deletion(-) create mode 100644 tasmota/tasmota_xnrg_energy/xnrg_26_mk_sky_blu.ino diff --git a/tasmota/include/tasmota_template.h b/tasmota/include/tasmota_template.h index 301e8dfab..ed26fbd46 100644 --- a/tasmota/include/tasmota_template.h +++ b/tasmota/include/tasmota_template.h @@ -235,6 +235,7 @@ enum UserSelectablePins { #ifdef ESP32 GPIO_HSDIO_CMD, GPIO_HSDIO_CLK, GPIO_HSDIO_RST, GPIO_HSDIO_D0, GPIO_HSDIO_D1, GPIO_HSDIO_D2, GPIO_HSDIO_D3, // Hosted MCU SDIO interface, including 1-bit and 4-bit modes #endif + GPIO_MKSKYBLU_TX, GPIO_MKSKYBLU_RX, // MakeSkyBlue solar charge controller GPIO_VID6608_F, GPIO_VID6608_CW, // VID6608 GPIO_SENSOR_END }; @@ -513,8 +514,9 @@ const char kSensorNames[] PROGMEM = #ifdef ESP32 D_SENSOR_HSDIO_CMD "|" D_SENSOR_HSDIO_CLK "|" D_SENSOR_HSDIO_RST "|" D_SENSOR_HSDIO_D0 "|" D_SENSOR_HSDIO_D1 "|" D_SENSOR_HSDIO_D2 "|" D_SENSOR_HSDIO_D3 "|" #endif + D_SENSOR_MKSKYBLU_TX "|" D_SENSOR_MKSKYBLU_RX "|" D_VID6608_F "|" D_VID6608_CW "|" - ; +; const char kSensorNamesFixed[] PROGMEM = D_SENSOR_USER; @@ -543,6 +545,11 @@ const char kSensorNamesFixed[] PROGMEM = #define MAX_CSE7761 2 // Model 1/2 (DUALR3), 2/2 (POWCT) #define MAX_TWAI SOC_TWAI_CONTROLLER_NUM #define MAX_GPS_RX 3 // Baudrates 1 (9600), 2 (19200), 3 (38400) +#ifdef ESP32 +#define MAX_MKSKYBLU_IF 8 // MakeSkyBlue solar charger: ESP32-NRG supports up to 8 phases +#else +#define MAX_MKSKYBLU_IF 3 // MakeSkyBlue solar charger: ESP82xx-NRG supports up to 3 phases +#endif const uint16_t kGpioNiceList[] PROGMEM = { GPIO_NONE, // Not used @@ -1018,6 +1025,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_V9240_TX), // Serial V9240 interface AGPIO(GPIO_V9240_RX), // Serial V9240 interface #endif +#ifdef USE_MAKE_SKY_BLUE + AGPIO(GPIO_MKSKYBLU_TX) + AGMAX(MAX_MKSKYBLU_IF), + AGPIO(GPIO_MKSKYBLU_RX) + AGMAX(MAX_MKSKYBLU_IF), +#endif #endif // USE_ENERGY_SENSOR /*-------------------------------------------------------------------------------------------*\ diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index fe6660870..3b563e783 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index 962437478..e9dc26b0c 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/ca_AD.h b/tasmota/language/ca_AD.h index 2a0ba1ec5..ad13d1070 100644 --- a/tasmota/language/ca_AD.h +++ b/tasmota/language/ca_AD.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index c505b51d9..05e4d01fe 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index e39510350..6298226c2 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index b8d3eeb2b..255197660 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index f7b38b595..57ccd444d 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index 272d22de9..0274afa46 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index a1d590164..1332a4030 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units diff --git a/tasmota/language/fy_NL.h b/tasmota/language/fy_NL.h index 3c4d8d70d..45f3637e9 100644 --- a/tasmota/language/fy_NL.h +++ b/tasmota/language/fy_NL.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index 67e5cc68f..af78d061d 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index 19f239367..f942eff08 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -1049,6 +1049,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index d70fe9ea4..8ec1c2d66 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis - RX" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K - RX" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K - TX" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index 39f312b16..58ecf293c 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/lt_LT.h b/tasmota/language/lt_LT.h index c50fcdf26..17232a880 100644 --- a/tasmota/language/lt_LT.h +++ b/tasmota/language/lt_LT.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index bffb0f589..6315065db 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 82db9882d..3dd07ab19 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index 5176e3d0d..1dbbf27e7 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index ed3ca9cf3..3cc8e622f 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index bb2ac9310..1ec0fb631 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index d8a85b4f4..1211454f5 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -1043,6 +1043,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index 4fb132312..526fa21b8 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index e8fe01528..025bfea85 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index f8a1adfbe..ab0676f76 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index f552bdc3d..8688d2289 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h index 456b525f5..0deda35d5 100644 --- a/tasmota/language/vi_VN.h +++ b/tasmota/language/vi_VN.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index 52cc0bd0c..8e6117863 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index b96f65727..9c3e199d8 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -1042,6 +1042,8 @@ #define D_SENSOR_WOOLIIS_RX "Wooliis Rx" #define D_SENSOR_C8_CO2_5K_RX "C8-CO2-5K Rx" #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" +#define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" +#define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" // Units #define D_UNIT_AMPERE "安培" diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 681fcade2..2e2f1078b 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -986,6 +986,8 @@ //#define USE_WE517 // Add support for Orno WE517-Modbus energy monitor (+1k code) //#define USE_MODBUS_ENERGY // Add support for generic modbus energy monitor using a user file in rule space (+5k) //#define USE_V9240 // Add support for Vango Technologies V924x ultralow power, single-phase, power measurement (+12k) +//#define USE_MAKE_SKY_BLUE // Add support for MakeSkyBlue - Solar Charge Controller interface + #define MAKE_SKY_BLUE_OPTION 0x7 // MakeSkyBlue option: 0=minimal, 0x1=with serial debug, 0x2=with EnergyConfig On/Off, 0x4=with EnergyConfig register R/W // -- Low level interface devices ----------------- #define USE_DHT // Add support for DHT11, AM2301 (DHT21, DHT22, AM2302, AM2321) and SI7021 Temperature and Humidity sensor (1k6 code) diff --git a/tasmota/tasmota_xnrg_energy/xnrg_26_mk_sky_blu.ino b/tasmota/tasmota_xnrg_energy/xnrg_26_mk_sky_blu.ino new file mode 100644 index 000000000..a554a8424 --- /dev/null +++ b/tasmota/tasmota_xnrg_energy/xnrg_26_mk_sky_blu.ino @@ -0,0 +1,858 @@ +/* + xnrg_26_mk_sky_blu.ino - MakeSkyBlue Solar charger support for Tasmota + + Copyright (C) 2025 meMorizE + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ENERGY_SENSOR +#ifdef USE_MAKE_SKY_BLUE +/*********************************************************************************************\ + * This implementation communicates with solar charge controller of MakeSkyBlue(Shenzen) Co.LTD + * http://www.makeskyblue.com + * Model: S3-30A/40A/50A/60A MPPT 12V/24V/36V/48V with firmwware V118 (or V119 including a Wifi Box) + * Tested with Model S3-60A + * https://makeskyblue.com/en-de/products/60a-mppt-solar-charge-controller-w-wifi + * + * Precondition to get communication working: + * The charge controller should have a Mini-USB socket at bottom right beside the screw terminals. + * It has a Vcc supply output of +5V and GND but the D+ and D- signals are NOT according USB standard ! + * They are TTL serial signals with 9600bps 8N1, D+=Tx_Out D-=Rx_In + * When using a DIY ESP hardware be aware of the 5V TTL levels by using e.g. level shifter or isolator + * + * This implementation supports multiple charge controllers at one tasmota-ESP (by 2025-11): + * ESP82xx up to 3 + * ESP32xx up to 8 (theoretically, not tested if so many serial interfaces will work) + * Every serial receiver channel is related to one energy phase. + * It allows to have only one transmitter and multiple receivers, but this does + * not allow individual addressing for on/off and register read/write. + * + * The orignal V119 Wifi Box hardware includes an ESP8285 with 1MB Flash (as module 2AL3B ESP-M) + * This specific hardware uses + * GPIO1: TX0, ESP => Charge controller + * GPIO3: RXD0, ESP <= Charge controller + * GPIO5 red LED, active low, for e.g. LedLink_i + * free (or bootstrap): GPIO4, GPIO12, GPIO13, GPIO14, GPIO15, GPIO16, GPIO17(ADC) + * {"NAME":"MakeSkyBlue Wifi-Adapter (ESP8285)","GPIO":[1,10528,1,10560,1,1,0,0,1,1,1,1,1,1],"FLAG":0,"BASE":18} + * + * Useful runtime commands and options: + * VoltRes 1 - select voltage resolution to 0.1 V (native resolution of solar charger) + * AmpRes 1 - select current resolution to 0.1 A (native resolution of solar charger) + * SetOption72 - read and use total energy from the memory within the charge controller + * SetOption129 1 - Display energy for each phase instead of single sum (only if multiple channels configured) + * SetOption150 1 - Display no common voltage/frequency + * + * This implementation is based on information from + * https://github.com/lasitha-sparrow/Makeskyblue_wifi_Controller + * https://www.genetrysolar.com/community/forum-134/topic-1219/ + * + * and logic analyzer recording of the original Wifi-Box firmware together with the Android app: + * Status: - request periodical every 1s + * - response typical within 30ms + * Measurements: - request periodical every 1s, about 380ms after Status request + * - response varies within 30...370ms + * ReadRegister: - requested on demand with an interval of min. 170ms, valid registers 1...9 + * - response typical within 30ms +\*********************************************************************************************/ + +#include + +#define XNRG_26 26 + + +/* available compile options, bit encoded */ +#if MAKE_SKY_BLUE_OPTION & 0x1 +#define MKSB_WITH_SERIAL_DEBUGGING // provide counters for error detection and statistics +// >Practical use: a real device shows frequent timeouts and / or CRC errors, typically if the solar power is more than 350W. +#endif +// +#if MAKE_SKY_BLUE_OPTION & 0x2 +#define MKSB_WITH_ON_OFF_SUPPORT // allow to switch charging OFF and ON +// >Use Console command 'EnergyConfig 0 -' to switch OFF +// >Use Console command 'EnergyConfig 0 +' to switch ON +#endif +// +#if MAKE_SKY_BLUE_OPTION & 0x4 +#define MKSB_WITH_REGISTER_SUPPORT // read and write access to configuration registers +// >Use Console command 'EnergyConfig' to read and write configuration registers R1...R9 +// no parameters: show help +// 1st parameter: i = transmit interface, 0 for all +// 2nd parameter: n = address value of specific register number n +// 3rd parameters: v = value to write to specified register number n, none=read +#endif + + +#define MKSB_BAUDRATE 9600 +// TxRx first byte of every valid frame +#define MKSB_START_FRAME 0xAA +// Tx second byte: Request to the charge controller +#define MKSB_CMD_READ_MEASUREMENTS 0x55 // response: 0xBB +#define MKSB_CMD_READ_REGISTER 0xCB // response: 0xDA +#define MKSB_CMD_WRITE_REGISTER 0xCA // response: 0xDA +#define MKSB_CMD_AUXILARY 0xCC // response: 0xDC or 0xDD +// Rx second byte: Response from the charge controller +#define MKSB_RSP_READ_MEASURES 0xBB // request: 0xAA +#define MKSB_RSP_RW_CONFIG 0xDA // request: 0xCB or 0xCA +#define MKSB_RSP_CLR_WIFI_PASSWORD 0xDC // D05 Clear Wifi-Password (not implemented) +#define MKSB_RSP_POWER 0xDD // request 0xCC +// +#define MKSB_RSP_SZ_READ_MEASURES 20 // bytes // minimum size of response frame +#define MKSB_RSP_SZ_RW_CONFIG 9 // bytes // size of response frame + + +// TxRx third byte: config registers (request 0xCB or 0xCA, response 0xDA) +#define MKSB_REG_FIRST 1 // vvv = related local parameter at display UI +#define MKSB_REG_VOLTAGE_BULK 1 // D02 MPPT Voltage limit BULK, >= stops charging [mV], e.g. 55000mV +#define MKSB_REG_VOLTAGE_FLOAT 2 // D01 MPPT voltage limit FLOAT, <= restarts charging [mV], SLA battery only +#define MKSB_REG_OUT_TIMER 3 // D00 Time duration the load gets connected [mh], default: 24h = 24000mh +#define MKSB_REG_CURRENT 4 // - MPPT current limit [mA] e.g. 1000...60000mA, CAUTION: respect limit of hardware +#define MKSB_REG_BATT_UVP_CUT 5 // D03 Battery UnderVoltageProtection, <=limit cuts load [mV], e.g. 42400mV +#define MKSB_REG_BATT_UVP_CONN 6 // ~ Battery UnderVoltageProtection, >=limit reconnects load [mV], typical: D03 + 200mV, e.g. 44400mV +#define MKSB_REG_COMM_ADDR 7 // - Communication Address [mAddress] +#define MKSB_REG_BATT_TYPE 8 // D04 Battery Type: value 0=SLA, 1=LiPo (2=LiLo, 3=LiFE, 4=LiTo) [mSelect], e.g. 1000=LiPo +#define MKSB_REG_BATT_CELLS 9 // - Battery System: 1...4 * 12V (Read-Only, set at Batt.connection) [m12V], e.g. 4000m12V +#define MKSB_REG_LAST 9 // ^^^ = related local parameter at display UI +#define MKSB_REG_TOTAL (1+(MKSB_REG_LAST-MKSB_REG_FIRST)) + +#define MKSB_RX_BUFFER_SIZE 24 // bytes, 20 minimum +#define MKSB_TX_BUFFER_SIZE 8 // bytes, 7 minimum + +#define MKSB_STATUS_MPPT_IDLE 0 // Idle, HMI=3.0 Night Mode (PV < XYZ V) +#define MKSB_STATUS_MPPT_OCP_OUTPUT 2 // Overcurrent Protection, ??? E73 +#define MKSB_STATUS_MPPT_OVP_OUTPUT 3 // MPPT Bulk Voltage Limit reached +#define MKSB_STATUS_MPPT_CHARGING 4 // Charging, HMI=4.0 MPPT Mode +#define MKSB_STATUS_MPPT_FULL 6 // Battery full +// +#define MKSB_STATUS_BATT_UVP 0x100 // Battery Undervoltage Protection, load cut, Fault E65 +#define MKSB_STATUS_BATT_OVP 0x200 // Battery Overvoltage, load still connected, Fault E63 + +// module type definition +typedef struct MKSB_MODULE_T_ +{ + uint8_t idx; // index of receiver interface + uint8_t phase_id; // phase id + uint8_t idx_tx; // index of transmitter interface + TasmotaSerial *Serial = nullptr; + char *pRxBuffer = nullptr; + uint8_t txBuffer[MKSB_TX_BUFFER_SIZE]; + uint16_t energy_total; // totalizer at charge controller (non-volatile there) +// uint16_t status; // combines mode (LSB) and status / error (MSB) + uint8_t rxIdx; // bytecounter at the Rx data buffer + uint8_t rxChecksum; // for checksum calculation at reception + uint8_t ev250ms_state; // for scheduled serial requesting, snyced with everysecond +#ifdef MKSB_WITH_SERIAL_DEBUGGING + uint32_t cntTx; // count requests transmitted to the charge controller + uint32_t cntRxGood; // count valid responses received from the charge controller + uint32_t cntRxBadCRC; // count CRC-invalid responses + uint32_t tsTx; // timestamp in ms of last byte transmitted + uint32_t tsRx; // timestamp in ms of last byte received +#endif +#ifdef MKSB_WITH_REGISTER_SUPPORT + uint16_t regs_to_read; // bit_n = flags register n to read + uint16_t regs_to_write; // bit_n = flags register n to write + uint16_t regs_to_report; // bit_n = flags register n to report (after valid response received) + uint16_t regs_value[MKSB_REG_TOTAL]; // value storage of all known configuration registers +#endif +#ifdef MKSB_WITH_ON_OFF_SUPPORT + bool actual_state; // true = active, false = stop + bool target_state; // true = active, false = stop +#endif +} MKSB_MODULE_T; + + +MKSB_MODULE_T *pMskbInstance = nullptr; +float *pMksbInstance_FloatArrays = nullptr; // single allocation reference for all channel float arrays +enum _E_MKSB_FLOATS +{ + MKSB_INSTANCE_FLOATARRAY_TEMPERATURE = 0, + MKSB_INSTANCE_FLOATARRAY_BATTVOLTAGE, + MKSB_INSTANCE_FLOATARRAY_BATTCURRENT, + MKSB_INSTANCE_FLOATARRAY_SIZE // number of different float arrays +} E_MKSB_FLOATS; +// Note: each float array has Energy->phase_count elements + +// module const data +const char mksb_HTTP_SNS_TEMPERATURE[] PROGMEM = "{s}" D_TEMPERATURE "{m}%s °%c{e}"; +const char mksb_HTTP_SNS_BATT_VOLTAGE[] PROGMEM = "{s}" D_BATTERY " " D_VOLTAGE "{m}%s " D_UNIT_VOLT "{e}"; +const char mksb_HTTP_SNS_BATT_CURRENT[] PROGMEM = "{s}" D_BATTERY " " D_CURRENT "{m}%s " D_UNIT_AMPERE "{e}"; +//const char mksb_HTTP_SNS_STATUS_INFO[] PROGMEM = "{s}" D_STATUS "{m}%s {e}"; +// +#ifdef MKSB_WITH_REGISTER_SUPPORT +// config register format strings: requires one float as register value +static const char mksb_fsrreg1[] PROGMEM = "NRG: CH%u R1 = %1_fV [D02] MPPT Bulk Charging Voltage"; +static const char mksb_fsrreg2[] PROGMEM = "NRG: CH%u R2 = %1_fV [D01] MPPT Floating Charging Voltage (SLA only)"; +static const char mksb_fsrreg3[] PROGMEM = "NRG: CH%u R3 = %0_fh [D00] Load-Output ON duration"; +static const char mksb_fsrreg4[] PROGMEM = "NRG: CH%u R4 = %1_fA MPPT Current Limit (CAUTION: change on your own risk!)"; +static const char mksb_fsrreg5[] PROGMEM = "NRG: CH%u R5 = %1_fV [D03] Battery Undervoltage Protection (Load-Output OFF)"; +static const char mksb_fsrreg6[] PROGMEM = "NRG: CH%u R6 = %1_fV Battery Undervoltage Recovery (Load-Output ON)"; +static const char mksb_fsrreg7[] PROGMEM = "NRG: CH%u R7 = %0_f Com Address (not used)"; +static const char mksb_fsrreg8[] PROGMEM = "NRG: CH%u R8 = %0_f [D04] Battery Type [0=SLA, 1=Li]"; +static const char mksb_fsrreg9[] PROGMEM = "NRG: CH%u R9 = %0_f Battery System [1...4 * 12V] (read-only)"; +static const char * mksb_register_fstrings[] PROGMEM = { mksb_fsrreg1, mksb_fsrreg2, mksb_fsrreg3, + mksb_fsrreg4, mksb_fsrreg5, mksb_fsrreg6, + mksb_fsrreg7, mksb_fsrreg8, mksb_fsrreg9 }; +#endif + + +/********************************************************************************************/ +/* EXTRACT UNSIGNED INT FROM SERIAL RECEIVED DATA */ +uint32_t mExtractUint32(const char *data, uint8_t offset_lsb, uint8_t offset_msb) +{ + uint32_t result = 0; + + for ( ; offset_msb >= offset_lsb; offset_msb-- ) { + result = (result << 8) | (uint8_t)data[offset_msb]; + } + return result; +} + + +/********************************************************************************************/ +/* FINALIZE SERIAL RECEIVE */ +static void mFinishReceive(void * me) +{ + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + #ifdef MKSB_WITH_SERIAL_DEBUGGING + if ( mksb->rxIdx ) { // bytes received with timediff since last request + if ( mksb->idx_tx ) { // channel is tranceiver, show time since last send + mksb->tsRx = millis(); + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Tx+ %ums] %*_H"), + mksb->idx, (mksb->tsRx - mksb->tsTx), mksb->rxIdx, mksb->pRxBuffer); + } else { // channel is receiver only, show time since last receive-finish + mksb->tsTx = millis(); // use it for now + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Rx+ %ums] %*_H"), + mksb->idx, (mksb->tsTx - mksb->tsRx), mksb->rxIdx, mksb->pRxBuffer); + mksb->tsRx = mksb->tsTx; + } + } + #endif + // finalize reception + mksb->Serial->flush(); // ensure receive buffer is empty + mksb->rxIdx = 0; // reset receiver state +} + + +/********************************************************************************************/ +/* SEND SERIAL DATA */ +static void mSendSerial(void * me, uint8_t len) +{ + uint32_t i; + uint8_t crc; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + if (mksb->idx_tx == 0) { + return; // the channel has no transmitter, receive only + } + + mksb->Serial->flush(); // ensure receive buffer is empty + mksb->rxIdx = 0; // reset receiver state + + // request frame + mksb->Serial->write( MKSB_START_FRAME ); + crc = 0; + for ( i = 0; i < len; i++ ) { // variable data + mksb->Serial->write(mksb->txBuffer[i]); + crc += mksb->txBuffer[i]; + } + mksb->Serial->write(crc); // checksum + mksb->Serial->flush(); // ensure transmission complete + +#ifdef MKSB_WITH_SERIAL_DEBUGGING + mksb->tsTx = millis(); + mksb->cntTx++; + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: CH%u Tx: %02X%*_H%02X"), + mksb->idx, MKSB_START_FRAME, len, mksb->txBuffer, crc ); +#endif +} + + +/********************************************************************************************/ +/* PARSE SERIAL DATA RECEIVED */ +static void mParseMeasurements(void * me) +{ + uint8_t phase; + uint32_t voltage, current, power; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + phase = mksb->phase_id; + // solar + voltage = mExtractUint32(mksb->pRxBuffer, 6, 7); // voltage [0.1V] + power = mExtractUint32(mksb->pRxBuffer, 8, 9); // power [1.0W] + Energy->voltage[phase] = (float)voltage / 10.0f; + Energy->active_power[phase] = (float)power; + if ( voltage ) { // prevent division by 0 + // calculate: solar current I = P / U + Energy->current[phase] = Energy->active_power[phase] / Energy->voltage[phase]; + } else { + Energy->current[phase] = 0.0f; + } + Energy->data_valid[phase] = 0; + + // battery + current = mExtractUint32(mksb->pRxBuffer, 4, 5); // charge current [0.1A] + voltage = mExtractUint32(mksb->pRxBuffer, 2, 3); // voltage [0.1V] + + // temperature of the charge controller electronics, integer resolution is 0.1degC + pMksbInstance_FloatArrays[phase] = ConvertTempToFahrenheit( (float)mExtractUint32(mksb->pRxBuffer, 10, 11) / 10.0f ); + phase += Energy->phase_count; + // battery voltage, integer resolution is 0.1V + pMksbInstance_FloatArrays[phase] = (float)voltage / 10.0f; + phase += Energy->phase_count; + // battery charge current, integer resolution is 0.1A + pMksbInstance_FloatArrays[phase] = (float)current / 10.0f; + mksb->energy_total = mExtractUint32(mksb->pRxBuffer, 12, 13); // solar energy total [1.0kWh] + + // mksb->status = mExtractUint32(mksb->pRxBuffer, 16, 17); // mode and status + + // unused response data: unknown encoding + // mExtractUint32(mksb->pRxBuffer, 14, 15); // ?dummy [14:15] + // mExtractUint32(mksb->pRxBuffer, 18, 18); // ?dummy [18] +} + + +/********************************************************************************************/ +/* RECEIVE SERIAL DATA */ +static void MkSkyBluSerialReceive(void * me) +{ + int i; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + if (mksb->Serial == nullptr) { + return; // serial interface not available + } + + while ( mksb->Serial->available() ) { + yield(); + if (mksb->rxIdx < MKSB_RX_BUFFER_SIZE) { // buffer available + mksb->pRxBuffer[mksb->rxIdx] = mksb->Serial->read(); + if (MKSB_START_FRAME != mksb->pRxBuffer[0] ) { // no start of frame yet + mksb->rxIdx = 0; // reset receiver + } else { // [0] start valid + if (0 == mksb->rxIdx) { // start of frame present + mksb->rxChecksum = 0; // reset checksum calc + } else { // [1+] cmd or later + if ((MKSB_RSP_READ_MEASURES == mksb->pRxBuffer[1]) && (19 == mksb->rxIdx)) { + if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) { + mParseMeasurements(mksb); +#ifdef MKSB_WITH_SERIAL_DEBUGGING + mksb->cntRxGood++; + } else { + mksb->cntRxBadCRC++; +#endif + } + mFinishReceive(me); + } else +#ifdef MKSB_WITH_REGISTER_SUPPORT + if ((MKSB_RSP_RW_CONFIG == mksb->pRxBuffer[1]) && (8 == mksb->rxIdx)) + { + uint8_t reg; + if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) { + reg = mksb->pRxBuffer[2] - MKSB_REG_FIRST; + if ( reg < MKSB_REG_TOTAL ) { + mksb->regs_value[reg] = mExtractUint32(mksb->pRxBuffer, 3, 4); + mksb->regs_to_report |= 1 << reg; + } +#ifdef MKSB_WITH_SERIAL_DEBUGGING + mksb->cntRxGood++; + } else { + mksb->cntRxBadCRC++; +#endif + } + mFinishReceive(me); + } else +#endif +#ifdef MKSB_WITH_ON_OFF_SUPPORT + if ((MKSB_RSP_POWER == mksb->pRxBuffer[1]) && (5 == mksb->rxIdx)) { + if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) { + if ( mksb->pRxBuffer[2] == 0 ) { /* ON */ + if ( mksb->actual_state != true ) { + AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging ON"), mksb->idx); + mksb->actual_state = true; + } + } else + if ( mksb->pRxBuffer[2] == 1 ) { /* OFF */ + if ( mksb->actual_state != false ) { + AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging OFF"), mksb->idx); + mksb->actual_state = false; + } + } else { /* unknown content */ + } +#ifdef MKSB_WITH_SERIAL_DEBUGGING + mksb->cntRxGood++; + } else { + mksb->cntRxBadCRC++; +#endif + } + mFinishReceive(me); + } else +#endif + { // calc checksum + mksb->rxChecksum += mksb->pRxBuffer[mksb->rxIdx]; + } + } + } + mksb->rxIdx++; // more to receive + } else { // buffer full + mFinishReceive(me); + } + } +} + + +/********************************************************************************************/ +/* EVERY SECOND */ +void MkSkyBluEverySecond(void * me) +{ + int i; + float fValue; + uint8_t phase; + bool updateTotal = false; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + phase = mksb->phase_id; + + if (Energy->data_valid[phase] > ENERGY_WATCHDOG) { + Energy->voltage[phase] = Energy->current[phase] = Energy->active_power[phase] = 0.0f; + Energy->voltage[phase] = NAN; // mark as invalid + Energy->current[phase] = NAN; // mark as invalid + pMksbInstance_FloatArrays[phase] = NAN; // temperature + phase += Energy->phase_count; + pMksbInstance_FloatArrays[phase] = NAN; // battery voltage + phase += Energy->phase_count; + pMksbInstance_FloatArrays[phase] = NAN; // battery current + } else { + Energy->kWhtoday_delta[phase] += Energy->active_power[phase] * 1000 / 36; // solar energy only + // import the non-resetable solar energy full kWh counter from the charge controller, but with 2 requirements: + // 1. SetOption72 is active (bug@EnergyUpdateTotal?: a call of it impacts kWhtoday, even if option is off) + // 2. the tasmota total is smaller than the total of the charge controller + if ( Settings->flag3.hardware_energy_total ) { + fValue = (float)mksb->energy_total; + if ( Energy->total[phase] < fValue ) { + Energy->import_active[phase] = fValue; + updateTotal = true; + } + } + } + mksb->ev250ms_state = 0; // sync the 250ms state machine + +#ifdef MKSB_WITH_SERIAL_DEBUGGING + { + static uint32_t lastDebugLog = 0; + if ( (millis() - lastDebugLog) > (5u * 60000u) ) { // every 5 minutes + if ( mksb->idx_tx ) { // transceiver + AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Tx:%u, Rx+:%u, Rx-total:%u (Rx-CRC:%u)"), + mksb->idx, mksb->cntTx, mksb->cntRxGood, mksb->cntTx - mksb->cntRxGood, mksb->cntRxBadCRC ); + } else { // receiver only + AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Rx+:%u, Rx-CRC:%u"), + mksb->idx, mksb->cntRxGood, mksb->cntRxBadCRC ); + } + if (mksb->phase_id >= (Energy->phase_count - 1) ) { // last channel logged + lastDebugLog = millis(); + } + } + } +#endif + if ( updateTotal == true ) { + EnergyUpdateTotal(); // this also calls EnergyUpdateToday() at the end + } else { + EnergyUpdateToday(); + } +} + + +/********************************************************************************************/ +/* EVERY 250 MS */ +void MkSkyBluEvery250ms(void * me) +{ + static const uint8_t mksb_ser_req_measures[] PROGMEM = { MKSB_CMD_READ_MEASUREMENTS, 0,0,0 }; + static const uint8_t mksb_ser_req_write_reg[] PROGMEM = { MKSB_CMD_WRITE_REGISTER, 0 ,0,0, 0,0,0 }; + static const uint8_t mksb_ser_req_read_reg[] PROGMEM = { MKSB_CMD_READ_REGISTER, 0 ,0,0, 0,0,0 }; + static const uint8_t mksb_ser_req_chrg_off[] PROGMEM = { MKSB_CMD_AUXILARY, 1, 2, 0 }; + static const uint8_t mksb_ser_req_chrg_on[] PROGMEM = { MKSB_CMD_AUXILARY, 2, 0, 0 }; + int i; + float fVal; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + if (mksb->Serial == nullptr) { + return; // serial interface not available + } + if (mksb->idx_tx == 0) { + return; // no transmitter at this channel + } + + // if ( mksb->ev250ms_state == 0 ) { + // MkSkyBluRequestStatus(); + // } else + if ( mksb->ev250ms_state == 0 ) { + memcpy_P(mksb->txBuffer, mksb_ser_req_measures, sizeof (mksb_ser_req_measures)); + mSendSerial(mksb, sizeof (mksb_ser_req_measures)); + } +#ifdef MKSB_WITH_REGISTER_SUPPORT + else if ( mksb->ev250ms_state == 3 ) { + if ( mksb->regs_to_read || mksb->regs_to_write ) { + for ( i = 0; i < MKSB_REG_TOTAL; i++ ) { + if ( mksb->regs_to_write & (1 << i) ) { // prio write + memcpy_P(mksb->txBuffer, mksb_ser_req_write_reg, sizeof (mksb_ser_req_write_reg)); + // variable register and value + mksb->txBuffer[1] = i + MKSB_REG_FIRST; + mksb->txBuffer[2] = (uint8_t)(mksb->regs_value[i] & 0xFF); + mksb->txBuffer[3] = (uint8_t)(mksb->regs_value[i] >> 8); + mSendSerial(mksb, sizeof (mksb_ser_req_write_reg)); + mksb->regs_to_write &= ~(1 << i); + break; // only one per call + } else + if ( mksb->regs_to_read & (1 << i) ) { + memcpy_P(mksb->txBuffer, mksb_ser_req_read_reg, sizeof (mksb_ser_req_read_reg)); + // variable register + mksb->txBuffer[1] = i + MKSB_REG_FIRST; + mSendSerial(mksb, sizeof (mksb_ser_req_read_reg)); + mksb->regs_to_read &= ~(1 << i); + break; // only one per call + } else {} + } + } + } +#endif +#ifdef MKSB_WITH_ON_OFF_SUPPORT + else if ( mksb->actual_state != mksb->target_state ) { + if ( mksb->target_state == false ) { + memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_off, sizeof (mksb_ser_req_chrg_off)); + mSendSerial(mksb, sizeof (mksb_ser_req_chrg_off)); + } else { + memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_on, sizeof (mksb_ser_req_chrg_on)); + mSendSerial(mksb, sizeof (mksb_ser_req_chrg_on)); + } + mksb->actual_state = mksb->target_state; + } +#endif +#ifdef MKSB_WITH_REGISTER_SUPPORT + if ( mksb->regs_to_report ) + { + for ( i = 0; i < MKSB_REG_TOTAL; i++ ) { + if ( mksb->regs_to_report & (1 << i) ) { + fVal = ((float)(mksb->regs_value[i])) / 1000.0f; + AddLog( LOG_LEVEL_ERROR, mksb_register_fstrings[i], mksb->idx, &fVal); // log always + mksb->regs_to_report &= ~(1 << i); + break; // once per call + } + } + } +#endif + mksb->ev250ms_state++; + if ( mksb->ev250ms_state >= 4 ) + { + mksb->ev250ms_state = 0; + } +} + + +/********************************************************************************************/ +/* ENERGY COMMAND */ +bool MkSkyBluEnergyCommand(void * me) +{ + bool serviced = true; + uint8_t reg; + char *str; + int32_t value; + int i; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + if ((CMND_POWERCAL == Energy->command_code) || (CMND_VOLTAGECAL == Energy->command_code) || (CMND_CURRENTCAL == Energy->command_code)) { + // Service in xdrv_03_energy.ino + } else + if (CMND_ENERGYCONFIG == Energy->command_code) { + AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: EnergyConfig index %d, payload %d, data '%s'"), + XdrvMailbox.index, XdrvMailbox.payload, XdrvMailbox.data ? XdrvMailbox.data : "null" ); + if ( XdrvMailbox.data_len == 0 ) { // no arguments: show help + AddLog(LOG_LEVEL_INFO, PSTR("NRG: Usage: EnergyConfig [+,-,Register-Num:1...9] [Register-Value]")); + } else { + str = XdrvMailbox.data; + // 1st argument: transmit interface number 1...8, 0=all + i = strtoul( str, &str, 10 ); + if( i != mksb->idx_tx && i != 0 ) { + return serviced; // this instance: not addressed or no transmitter + } + while ((*str != '\0') && isspace(*str)) { str++; }; // Trim spaces + // 2nd argument: +,- or register number +#ifdef MKSB_WITH_ON_OFF_SUPPORT + if ('-' == str[0] ) { // to set controller charging off + mksb->target_state = false; + return serviced; + } + if ('+' == str[0] ) { // to set controller charging active + mksb->target_state = true; + return serviced; + } +#endif +#ifdef MKSB_WITH_REGISTER_SUPPORT + reg = (uint8_t)strtoul( str, &str, 10 ) - MKSB_REG_FIRST; + if ( MKSB_REG_FIRST <= reg && MKSB_REG_LAST >= reg ) + { // valid register number 1...9 + while ((*str != '\0') && isspace(*str)) { str++; } // Trim spaces + if ( *str ) + { // 3nd argument: there is a value = write registers value + value = (int32_t)(CharToFloat(str) * 1000.0f); + // write Register: no range check here ! + mksb->regs_value[reg] = value; // store value to prepared for write + mksb->regs_to_write |= 1 << reg; // trigger write + } else + { // 3rd argument: there is no value = read registers value + mksb->regs_to_read |= 1 << reg; + } + return serviced; + } else + { // invalid register number + mksb->regs_to_read = (1 << MKSB_REG_TOTAL) - 1; // flag all registers to be read + return serviced; + } +#endif + serviced = false; + } + } else { + serviced = false; // Unknown command + } + return serviced; +} + + +/********************************************************************************************/ +/* PUBLISH SENSORS (beyond Energy) */ +static void MkSkyBluShow(uint32_t function) +{ + uint8_t phase; + bool voltage_common = (Settings->flag6.no_voltage_common) ? false : Energy->voltage_common; + + if ( FUNC_JSON_APPEND == function ) { + phase = 0; + // Efficiency: not used for JSON + phase += Energy->phase_count; + // Temperature + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%s"), + EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution)); + phase += Energy->phase_count; + // Battery Voltage + ResponseAppend_P(PSTR(",\"" D_JSON_VOLTAGE " battery\":%s"), + EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution, voltage_common)); + phase += Energy->phase_count; + // Battery Current + ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT " battery\":%s"), + EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution)); + } + +#ifdef USE_WEBSERVER + if ( FUNC_WEB_COL_SENSOR == function ) { + phase = 0; + WSContentSend_PD(mksb_HTTP_SNS_TEMPERATURE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution), TempUnit()); + phase += Energy->phase_count; + WSContentSend_PD(mksb_HTTP_SNS_BATT_VOLTAGE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution)); + phase += Energy->phase_count; + WSContentSend_PD(mksb_HTTP_SNS_BATT_CURRENT, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution)); + } + else if ( FUNC_WEB_SENSOR == function ) + { + WSContentSend_P( PSTR("MakeSkyBlue " D_SOLAR_POWER " " D_CHARGE) ); // headline after values + } else {} +#endif // USE_WEBSERVER +} + + +/********************************************************************************************/ +/* Reset ENERGY */ +void MkSkyBluReset(void * me) +{ + int i; + uint8_t phase; + MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; + + // mksb->temperature = NAN; + mksb->energy_total = 0; + + phase = mksb->phase_id; + Energy->total[phase] = 0.0f; +// Energy->data_valid[phase] = 0; +} + + +/********************************************************************************************/ +/* SENSORS INIT */ +void MkSkyBluSnsInit(void * me) +{ + int i; + MKSB_MODULE_T * mksb; + + mksb = (MKSB_MODULE_T *)me; + i = mksb->idx - 1; // inteface receiver index + // Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions + if ( mksb->idx_tx ) { // transceiver + mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), Pin(GPIO_MKSKYBLU_TX, i), 1); + } else { // receiver only + mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), -1, 1); + } + if (mksb->Serial == nullptr) { + AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial alloc failed"), mksb->idx); + return; + } + if (mksb->Serial->begin(MKSB_BAUDRATE)) { + // mksb->pTxBuffer = (char*)(malloc(MKSB_TX_BUFFER_SIZE)); + if (mksb->Serial->hardwareSerial()) { + ClaimSerial(); + // Use idle serial buffer to save RAM + mksb->pRxBuffer = TasmotaGlobal.serial_in_buffer + + sizeof(TasmotaGlobal.serial_in_buffer) - mksb->idx * MKSB_RX_BUFFER_SIZE; + // from the end, this allows to use other sensors from start simultaneously + } else { + mksb->pRxBuffer = (char*)(malloc(MKSB_RX_BUFFER_SIZE)); + } +#ifdef ESP32 + AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: CH%d ESP32 Serial UART%d"), mksb->idx, mksb->Serial->getUart()); +#endif + } else { + mksb->Serial = nullptr; + AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial init failed"), mksb->idx); + } +} + + +/********************************************************************************************/ +/* DRIVER INIT */ +void MkSkyBluDrvInit(void) +{ + int i; + MKSB_MODULE_T * mksb = nullptr; + uint8_t phase = 0, u8 = 0; + + for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) { + if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver + phase++; // count configured receiver + if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // check for configured transmitter + u8++; // count configured transceiver + } + } + } + if ( !u8 ) { // at least one transceiver needed + return; // mkskyblu not configured, driver not active + } else + if (Energy == nullptr ) { // something is wrong with the tasmota energy support + return; + } else + if( phase > sizeof( Energy->data_valid ) ) { // limit to max supported phases, 2025-11-02: 3 at ESP82xx, 8 at ESP32 + phase = sizeof( Energy->data_valid ); + AddLog(LOG_LEVEL_INFO, PSTR("NRG: Channel count limited to %d"), phase); + } + // one instance per configured receiver + pMskbInstance = (MKSB_MODULE_T *)(malloc(phase * sizeof(MKSB_MODULE_T))); + pMksbInstance_FloatArrays = (float *)(malloc(phase * (MKSB_INSTANCE_FLOATARRAY_SIZE * sizeof(float)))); + if ( pMskbInstance == nullptr || pMksbInstance_FloatArrays == nullptr ) { + AddLog(LOG_LEVEL_ERROR, PSTR("NRG: Memory allocation failed")); + return; + } + // at this point we have at least one transceiver configured + mksb = pMskbInstance; // first instance + phase = 0; + for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) { + if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver + mksb->idx = i + 1; // interface receiver index + if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // transceiver + mksb->idx_tx = mksb->idx; // interface transmitter index + } else { + mksb->idx_tx = 0; // unknown related transmitter + } + mksb->Serial = nullptr; + mksb->pRxBuffer = nullptr; // allocated at SnsInit + mksb->phase_id = phase++; + // preset / fixed module values + mksb->energy_total = 0; +#ifdef MKSB_WITH_REGISTER_SUPPORT + mksb->regs_to_read = 0; + mksb->regs_to_write = 0; + mksb->regs_to_report = 0; +#endif +#ifdef MKSB_WITH_ON_OFF_SUPPORT + mksb->actual_state = true; // default: charging enabled + mksb->target_state = true; // default: charging enabled +#endif + mksb++; // next instance + } + } + for( i = 0; i < (phase * MKSB_INSTANCE_FLOATARRAY_SIZE); i++ ) { + pMksbInstance_FloatArrays[i] = NAN; + } + // preset / fixed energy values + Energy->phase_count = phase; + Energy->voltage_common = false; // every charge controller has an individual solar voltage + Energy->frequency_common = true; + Energy->type_dc = true; // solar dc charger + Energy->use_overtemp = false; // ESP device acts as separated gateway, charge controller has its own temperature management + Energy->voltage_available = true; // solar power and voltage is provided by serial communication + Energy->current_available = true; // solar current is calculated from power and voltage + AddLog(LOG_LEVEL_INFO, PSTR("NRG: MakeSkyBlue driver initialized with %d channel(s)"), Energy->phase_count); + TasmotaGlobal.energy_driver = XNRG_26; +} + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xnrg26(uint32_t function) +{ + bool result = false; + int i; + MKSB_MODULE_T * mksb; + + if ( function == FUNC_PRE_INIT ) { + MkSkyBluDrvInit(); // create all instances + return result; + } else + + for( i = 0; i < Energy->phase_count; i++ ) + { + mksb = &pMskbInstance[i]; + if ( (void *)mksb == nullptr ) { + continue; // instance not configured + } + switch (function) { + case FUNC_LOOP: + MkSkyBluSerialReceive((void *)mksb); + break; + case FUNC_EVERY_250_MSECOND: + MkSkyBluEvery250ms((void *)mksb); + break; + case FUNC_ENERGY_EVERY_SECOND: + MkSkyBluEverySecond((void *)mksb); + break; + case FUNC_JSON_APPEND: + case FUNC_WEB_SENSOR: + case FUNC_WEB_COL_SENSOR: + MkSkyBluShow(function); + return result; // one call for all instances + break; + case FUNC_ENERGY_RESET: +// MkSkyBluReset((void *)mksb); // not used + break; + case FUNC_COMMAND: + result = MkSkyBluEnergyCommand((void *)mksb); + break; + case FUNC_INIT: + MkSkyBluSnsInit((void *)mksb); + break; + } + } + return result; +} + +#endif // USE_MAKE_SKY_BLUE +#endif // USE_ENERGY_SENSOR