diff --git a/include/Configuration.h b/include/Configuration.h index ab2ac128..39c46c39 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,7 +4,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011700 // 0.1.23 // make sure to clean all after change +#define CONFIG_VERSION 0x00011800 // 0.1.24 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -29,7 +29,7 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define JSON_BUFFER_SIZE 8192 +#define JSON_BUFFER_SIZE 12288 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -40,6 +40,10 @@ struct CHANNEL_CONFIG_T { struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; + bool Poll_Enable; + bool Poll_Enable_Night; + bool Command_Enable; + bool Command_Enable_Night; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; @@ -60,6 +64,8 @@ struct CONFIG_T { char Ntp_Server[NTP_MAX_SERVER_STRLEN + 1]; char Ntp_Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; + double Ntp_Longitude; + double Ntp_Latitude; bool Mqtt_Enabled; uint Mqtt_Port; diff --git a/include/InverterSettings.h b/include/InverterSettings.h new file mode 100644 index 00000000..188025b1 --- /dev/null +++ b/include/InverterSettings.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class InverterSettingsClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastUpdate = 0; +}; + +extern InverterSettingsClass InverterSettings; \ No newline at end of file diff --git a/include/SunPosition.h b/include/SunPosition.h new file mode 100644 index 00000000..49c5c71f --- /dev/null +++ b/include/SunPosition.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +#define SUNPOS_UPDATE_INTERVAL 60000l + +class SunPositionClass { +public: + SunPositionClass(); + void init(); + void loop(); + + bool isDayPeriod(); + bool sunsetTime(struct tm* info); + bool sunriseTime(struct tm* info); + +private: + void updateSunData(); + + SunSet _sun; + bool _isDayPeriod = true; + uint _sunriseMinutes = 0; + uint _sunsetMinutes = 0; + + uint32_t _lastUpdate = 0; + bool _isValidInfo = false; +}; + +extern SunPositionClass SunPosition; \ No newline at end of file diff --git a/include/Utils.h b/include/Utils.h index dbcea819..33887ff9 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -7,4 +7,5 @@ class Utils { public: static uint32_t getChipId(); static uint64_t generateDtuSerial(); + static int getTimezoneOffset(); }; diff --git a/include/defaults.h b/include/defaults.h index 0faaebfc..2784a8d9 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -23,6 +23,8 @@ #define NTP_SERVER "pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" #define NTP_TIMEZONEDESCR "Europe/Berlin" +#define NTP_LONGITUDE 10.4515f +#define NTP_LATITUDE 51.1657f #define MQTT_ENABLED false #define MQTT_HOST "" diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 63db5998..a07dec93 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -17,6 +17,10 @@ HM_Abstract::HM_Abstract(uint64_t serial) bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -34,6 +38,10 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -62,6 +70,10 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -83,6 +95,10 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -101,6 +117,10 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) { + if (!getEnableCommands()) { + return false; + } + if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) { limit = min(100, limit); } @@ -123,6 +143,10 @@ bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio) bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) { + if (!getEnableCommands()) { + return false; + } + if (turnOn) { _powerState = 1; } else { @@ -139,6 +163,10 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) { + if (!getEnableCommands()) { + return false; + } + _powerState = 2; PowerControlCommand* cmd = radio->enqueCommand(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index c9798473..8d84cac2 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -67,12 +67,32 @@ bool InverterAbstract::isProducing() } } - return totalAc > 0; + return _enablePolling && totalAc > 0; } bool InverterAbstract::isReachable() { - return Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT; + return _enablePolling && Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT; +} + +void InverterAbstract::setEnablePolling(bool enabled) +{ + _enablePolling = enabled; +} + +bool InverterAbstract::getEnablePolling() +{ + return _enablePolling; +} + +void InverterAbstract::setEnableCommands(bool enabled) +{ + _enableCommands = enabled; +} + +bool InverterAbstract::getEnableCommands() +{ + return _enableCommands; } AlarmLogParser* InverterAbstract::EventLog() diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 6334c648..079bf7f4 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -44,6 +44,12 @@ public: bool isProducing(); bool isReachable(); + void setEnablePolling(bool enabled); + bool getEnablePolling(); + + void setEnableCommands(bool enabled); + bool getEnableCommands(); + void clearRxFragmentBuffer(); void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); @@ -73,6 +79,9 @@ private: uint8_t _rxFragmentLastPacketId = 0; uint8_t _rxFragmentRetransmitCnt = 0; + bool _enablePolling = true; + bool _enableCommands = true; + std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; std::unique_ptr _powerCommandParser; diff --git a/platformio.ini b/platformio.ini index 6a39be83..6b5ef34a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,7 +27,7 @@ lib_deps = https://github.com/bertmelis/espMqttClient.git#v1.3.3 nrf24/RF24 @ ^1.4.5 olikraus/U8g2 @ ^2.34.13 - https://github.com/berni2288/arduino-CAN + buelowp/sunset @ ^1.1.7 extra_scripts = pre:auto_firmware_version.py diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 9d81ea8f..f61723e9 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -44,6 +44,8 @@ bool ConfigurationClass::write() ntp["server"] = config.Ntp_Server; ntp["timezone"] = config.Ntp_Timezone; ntp["timezone_descr"] = config.Ntp_TimezoneDescr; + ntp["latitude"] = config.Ntp_Latitude; + ntp["longitude"] = config.Ntp_Longitude; JsonObject mqtt = doc.createNestedObject("mqtt"); mqtt["enabled"] = config.Mqtt_Enabled; @@ -53,7 +55,7 @@ bool ConfigurationClass::write() mqtt["password"] = config.Mqtt_Password; mqtt["topic"] = config.Mqtt_Topic; mqtt["retain"] = config.Mqtt_Retain; - mqtt["publish_invterval"] = config.Mqtt_PublishInterval; + mqtt["publish_interval"] = config.Mqtt_PublishInterval; JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); mqtt_lwt["topic"] = config.Mqtt_LwtTopic; @@ -94,6 +96,10 @@ bool ConfigurationClass::write() JsonObject inv = inverters.createNestedObject(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; + inv["poll_enable"] = config.Inverter[i].Poll_Enable; + inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; + inv["command_enable"] = config.Inverter[i].Command_Enable; + inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -199,6 +205,8 @@ bool ConfigurationClass::read() strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); + config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; + config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; JsonObject mqtt = doc["mqtt"]; config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; @@ -208,7 +216,7 @@ bool ConfigurationClass::read() strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; - config.Mqtt_PublishInterval = mqtt["publish_invterval"] | MQTT_PUBLISH_INTERVAL; + config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; JsonObject mqtt_lwt = mqtt["lwt"]; strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); @@ -250,6 +258,11 @@ bool ConfigurationClass::read() config.Inverter[i].Serial = inv["serial"] | 0ULL; strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + config.Inverter[i].Poll_Enable = inv["poll_enable"] | true; + config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true; + config.Inverter[i].Command_Enable = inv["command_enable"] | true; + config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; + JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0; @@ -288,21 +301,21 @@ bool ConfigurationClass::read() void ConfigurationClass::migrate() { + File f = LittleFS.open(CONFIG_FILENAME, "r", false); + if (!f) { + MessageOutput.println(F("Failed to open file, cancel migration")); + return; + } + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); + return; + } + if (config.Cfg_Version < 0x00011700) { - File f = LittleFS.open(CONFIG_FILENAME, "r", false); - if (!f) { - MessageOutput.println(F("Failed to open file, cancel migration")); - return; - } - - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); - if (error) { - MessageOutput.println(F("Failed to read file, cancel migration")); - return; - } - JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); @@ -314,6 +327,13 @@ void ConfigurationClass::migrate() } } + if (config.Cfg_Version < 0x00011800) { + JsonObject mqtt = doc["mqtt"]; + config.Mqtt_PublishInterval = mqtt["publish_invterval"]; + } + + f.close(); + config.Cfg_Version = CONFIG_VERSION; write(); read(); diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp new file mode 100644 index 00000000..ebb354bb --- /dev/null +++ b/src/InverterSettings.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "InverterSettings.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include "SunPosition.h" +#include + +InverterSettingsClass InverterSettings; + +void InverterSettingsClass::init() +{ + const CONFIG_T& config = Configuration.get(); + const PinMapping_t& pin = PinMapping.get(); + + // Initialize inverter communication + MessageOutput.print(F("Initialize Hoymiles interface... ")); + if (PinMapping.isValidNrf24Config()) { + SPIClass* spiClass = new SPIClass(HSPI); + spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); + Hoymiles.setMessageOutput(&MessageOutput); + Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); + + MessageOutput.println(F(" Setting radio PA level... ")); + Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + + MessageOutput.println(F(" Setting DTU serial... ")); + Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + + MessageOutput.println(F(" Setting poll interval... ")); + Hoymiles.setPollInterval(config.Dtu_PollInterval); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial > 0) { + MessageOutput.print(F(" Adding inverter: ")); + MessageOutput.print(config.Inverter[i].Serial, HEX); + MessageOutput.print(F(" - ")); + MessageOutput.print(config.Inverter[i].Name); + auto inv = Hoymiles.addInverter( + config.Inverter[i].Name, + config.Inverter[i].Serial); + + if (inv != nullptr) { + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); + inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); + } + } + MessageOutput.println(F(" done")); + } + } + MessageOutput.println(F("done")); + } else { + MessageOutput.println(F("Invalid pin config")); + } +} + +void InverterSettingsClass::loop() +{ + if (millis() - _lastUpdate > SUNPOS_UPDATE_INTERVAL) { + const CONFIG_T& config = Configuration.get(); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + auto const& inv_cfg = config.Inverter[i]; + if (inv_cfg.Serial == 0) { + continue; + } + auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); + if (inv == nullptr) { + continue; + } + + inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night)); + inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night)); + } + } + + Hoymiles.loop(); +} \ No newline at end of file diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp new file mode 100644 index 00000000..1485eaa4 --- /dev/null +++ b/src/SunPosition.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "SunPosition.h" +#include "Configuration.h" +#include "Utils.h" + +SunPositionClass SunPosition; + +SunPositionClass::SunPositionClass() +{ +} + +void SunPositionClass::init() +{ +} + +void SunPositionClass::loop() +{ + if (millis() - _lastUpdate > SUNPOS_UPDATE_INTERVAL) { + updateSunData(); + _lastUpdate = millis(); + } +} + +bool SunPositionClass::isDayPeriod() +{ + return _isDayPeriod; +} + +void SunPositionClass::updateSunData() +{ + CONFIG_T const& config = Configuration.get(); + int offset = Utils::getTimezoneOffset() / 3600; + _sun.setPosition(config.Ntp_Latitude, config.Ntp_Longitude, offset); + + struct tm timeinfo; + if (!getLocalTime(&timeinfo, 5)) { + _isDayPeriod = false; + _sunriseMinutes = 0; + _sunsetMinutes = 0; + _isValidInfo = false; + return; + } + + _sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); + _sunriseMinutes = static_cast(_sun.calcCustomSunrise(SunSet::SUNSET_NAUTICAL)); + _sunsetMinutes = static_cast(_sun.calcCustomSunset(SunSet::SUNSET_NAUTICAL)); + uint minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; + + _isDayPeriod = (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); + _isValidInfo = true; +} + +bool SunPositionClass::sunsetTime(struct tm* info) +{ + // Get today's date + time_t aTime = time(NULL); + + // Set the time to midnight + struct tm tm; + localtime_r(&aTime, &tm); + tm.tm_sec = 0; + tm.tm_min = _sunsetMinutes; + tm.tm_hour = 0; + tm.tm_isdst = -1; + time_t midnight = mktime(&tm); + + localtime_r(&midnight, info); + return _isValidInfo; +} + +bool SunPositionClass::sunriseTime(struct tm* info) +{ + // Get today's date + time_t aTime = time(NULL); + + // Set the time to midnight + struct tm tm; + localtime_r(&aTime, &tm); + tm.tm_sec = 0; + tm.tm_min = _sunriseMinutes; + tm.tm_hour = 0; + tm.tm_isdst = -1; + time_t midnight = mktime(&tm); + + localtime_r(&midnight, info); + return _isValidInfo; +} \ No newline at end of file diff --git a/src/Utils.cpp b/src/Utils.cpp index f1975aff..db8363ad 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -35,4 +35,21 @@ uint64_t Utils::generateDtuSerial() } return dtuId; +} + +int Utils::getTimezoneOffset() +{ + // see: https://stackoverflow.com/questions/13804095/get-the-time-zone-gmt-offset-in-c/44063597#44063597 + + time_t gmt, rawtime = time(NULL); + struct tm* ptm; + + struct tm gbuf; + ptm = gmtime_r(&rawtime, &gbuf); + + // Request that mktime() looksup dst in timezone database + ptm->tm_isdst = -1; + gmt = mktime(ptm); + + return static_cast(difftime(rawtime, gmt)); } \ No newline at end of file diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 77a9c887..f133167d 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -51,6 +51,10 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); obj[F("serial")] = buffer; + obj[F("poll_enable")] = config.Inverter[i].Poll_Enable; + obj[F("poll_enable_night")] = config.Inverter[i].Poll_Enable_Night; + obj[F("command_enable")] = config.Inverter[i].Command_Enable; + obj[F("command_enable_night")] = config.Inverter[i].Command_Enable_Night; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -270,6 +274,11 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as(); inverter.channel[arrayCount].YieldTotalOffset = channel[F("yield_total_offset")].as(); strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name)); + inverter.Poll_Enable = root[F("poll_enable")] | true; + inverter.Poll_Enable_Night = root[F("poll_enable_night")] | true; + inverter.Command_Enable = root[F("command_enable")] | true; + inverter.Command_Enable_Night = root[F("command_enable_night")] | true; + arrayCount++; } @@ -297,6 +306,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } if (inv != nullptr) { + inv->setEnablePolling(inverter.Poll_Enable); + inv->setEnableCommands(inverter.Command_Enable); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 53a92e87..836247a0 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -5,6 +5,7 @@ #include "WebApi_ntp.h" #include "Configuration.h" #include "NtpSettings.h" +#include "SunPosition.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -51,6 +52,16 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); root[F("ntp_localtime")] = timeStringBuff; + SunPosition.sunriseTime(&timeinfo); + strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); + root[F("sun_risetime")] = timeStringBuff; + + SunPosition.sunsetTime(&timeinfo); + strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); + root[F("sun_settime")] = timeStringBuff; + + root[F("sun_isDayPeriod")] = SunPosition.isDayPeriod(); + response->setLength(); request->send(response); } @@ -68,6 +79,8 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) root[F("ntp_server")] = config.Ntp_Server; root[F("ntp_timezone")] = config.Ntp_Timezone; root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; + root[F("longitude")] = config.Ntp_Longitude; + root[F("latitude")] = config.Ntp_Latitude; response->setLength(); request->send(response); @@ -112,7 +125,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone"))) { + if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") && root.containsKey("latitude"))) { retMsg[F("message")] = F("Values are missing!"); retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); @@ -151,6 +164,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) strlcpy(config.Ntp_Server, root[F("ntp_server")].as().c_str(), sizeof(config.Ntp_Server)); strlcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as().c_str(), sizeof(config.Ntp_Timezone)); strlcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as().c_str(), sizeof(config.Ntp_TimezoneDescr)); + config.Ntp_Latitude = root[F("latitude")].as(); + config.Ntp_Longitude = root[F("longitude")].as(); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/src/main.cpp b/src/main.cpp index 434416ff..77ff6557 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ */ #include "Configuration.h" #include "Display_Graphic.h" +#include "InverterSettings.h" #include "MessageOutput.h" #include "VeDirectFrameHandler.h" #include "MqttHandleDtu.h" @@ -15,13 +16,13 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "SunPosition.h" #include "Utils.h" #include "WebApi.h" #include "PowerLimiter.h" #include "PylontechCanReceiver.h" #include "defaults.h" #include -#include #include void setup() @@ -85,6 +86,11 @@ void setup() NtpSettings.init(); MessageOutput.println(F("done")); + // Initialize SunPosition + MessageOutput.print(F("Initialize SunPosition... ")); + SunPosition.init(); + MessageOutput.println(F("done")); + // Initialize MqTT MessageOutput.print(F("Initialize MqTT... ")); MqttSettings.init(); @@ -127,46 +133,7 @@ void setup() } MessageOutput.println(F("done")); - // Initialize inverter communication - MessageOutput.print(F("Initialize Hoymiles interface... ")); - if (PinMapping.isValidNrf24Config()) { - SPIClass* spiClass = new SPIClass(HSPI); - spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); - Hoymiles.setMessageOutput(&MessageOutput); - Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); - - MessageOutput.println(F(" Setting radio PA level... ")); - Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); - - MessageOutput.println(F(" Setting DTU serial... ")); - Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); - - MessageOutput.println(F(" Setting poll interval... ")); - Hoymiles.setPollInterval(config.Dtu_PollInterval); - - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial > 0) { - MessageOutput.print(F(" Adding inverter: ")); - MessageOutput.print(config.Inverter[i].Serial, HEX); - MessageOutput.print(F(" - ")); - MessageOutput.print(config.Inverter[i].Name); - auto inv = Hoymiles.addInverter( - config.Inverter[i].Name, - config.Inverter[i].Serial); - - if (inv != nullptr) { - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); - inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); - } - } - MessageOutput.println(F(" done")); - } - } - MessageOutput.println(F("done")); - } else { - MessageOutput.println(F("Invalid pin config")); - } + InverterSettings.init(); // Initialize ve.direct communication MessageOutput.println(F("Initialize ve.direct interface... ")); @@ -191,7 +158,7 @@ void loop() yield(); PowerLimiter.loop(); yield(); - Hoymiles.loop(); + InverterSettings.loop(); yield(); // Vedirect_Enabled is unknown to lib. Therefor check has to be done here if (Configuration.get().Vedirect_Enabled) { @@ -212,6 +179,8 @@ void loop() yield(); Display.loop(); yield(); + SunPosition.loop(); + yield(); MessageOutput.loop(); yield(); PylontechCanReceiver.loop(); diff --git a/webapp/package.json b/webapp/package.json index 74a24ca7..41b2a680 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -21,9 +21,10 @@ "vue-router": "^4.1.6" }, "devDependencies": { + "@intlify/unplugin-vue-i18n": "^0.8.2", "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.6", - "@types/node": "^18.13.0", + "@types/node": "^18.14.0", "@types/spark-md5": "^3.0.2", "@vitejs/plugin-vue": "^4.0.0", "@vue/eslint-config-typescript": "^11.0.2", @@ -31,11 +32,12 @@ "eslint": "^8.34.0", "eslint-plugin-vue": "^9.9.0", "npm-run-all": "^4.1.5", - "sass": "^1.58.1", + "sass": "^1.58.3", + "terser": "^5.16.4", "typescript": "^4.9.5", - "vite": "^4.1.1", + "vite": "^4.1.3", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.0.0", - "vue-tsc": "^1.0.24" + "vite-plugin-css-injected-by-js": "^3.0.1", + "vue-tsc": "^1.1.4" } } diff --git a/webapp/src/components/InputElement.vue b/webapp/src/components/InputElement.vue index c2ab2b43..eff8e9f6 100644 --- a/webapp/src/components/InputElement.vue +++ b/webapp/src/components/InputElement.vue @@ -27,6 +27,7 @@ :maxlength="maxlength" :min="min" :max="max" + :step="step" :disabled="disabled" :aria-describedby="descriptionId" /> @@ -69,6 +70,7 @@ export default defineComponent({ 'maxlength': String, 'min': String, 'max': String, + 'step': String, 'rows': String, 'disabled': Boolean, 'postfix': String, diff --git a/webapp/src/components/RadioInfo.vue b/webapp/src/components/RadioInfo.vue index 2aae902b..8f16a212 100644 --- a/webapp/src/components/RadioInfo.vue +++ b/webapp/src/components/RadioInfo.vue @@ -5,26 +5,24 @@ {{ $t('radioinfo.ChipStatus') }} - - {{ $t('radioinfo.Connected') }} - {{ $t('radioinfo.NotConnected') }} + + {{ $t('radioinfo.ChipType') }} - - nRF24L01+ - nRF24L01 - {{ $t('radioinfo.Unknown') }} + + + + + + @@ -35,12 +33,14 @@ \ No newline at end of file diff --git a/webapp/src/components/WifiApInfo.vue b/webapp/src/components/WifiApInfo.vue index f00858ca..e2647dda 100644 --- a/webapp/src/components/WifiApInfo.vue +++ b/webapp/src/components/WifiApInfo.vue @@ -5,12 +5,8 @@ {{ $t('wifiapinfo.Status') }} - - {{ $t('wifiapinfo.Enabled') }} - {{ $t('wifiapinfo.Disabled') }} + + @@ -29,12 +25,14 @@