diff --git a/.gitignore b/.gitignore index c29e72f47..516579db6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ platformio-device-monitor*.log logs/device-monitor*.log platformio_override.ini +webapp_dist/ .DS_Store diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 94da35d78..32c4af5a8 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -18,7 +18,7 @@ class BatteryStats { uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; } bool updateAvailable(uint32_t since) const; - uint8_t getSoC() const { return _soc; } + float getSoC() const { return _soc; } uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; } uint8_t getSoCPrecision() const { return _socPrecision; } @@ -67,13 +67,15 @@ class BatteryStats { _lastUpdateCurrent = _lastUpdate = timestamp; } - String _manufacturer = "unknown"; + void setManufacturer(const String& m); + String _hwversion = ""; String _fwversion = ""; String _serial = ""; uint32_t _lastUpdate = 0; private: + String _manufacturer = "unknown"; uint32_t _lastMqttPublish = 0; float _soc = 0; uint8_t _socPrecision = 0; // decimal places @@ -98,7 +100,6 @@ class PylontechBatteryStats : public BatteryStats { float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; private: - void setManufacturer(String&& m) { _manufacturer = std::move(m); } void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } float _chargeVoltage; @@ -137,7 +138,6 @@ class PytesBatteryStats : public BatteryStats { float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ; private: - void setManufacturer(String&& m) { _manufacturer = std::move(m); } void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } void updateSerial() { if (!_serialPart1.isEmpty() && !_serialPart2.isEmpty()) { diff --git a/include/HttpGetter.h b/include/HttpGetter.h index 11ece1090..e6103ba99 100644 --- a/include/HttpGetter.h +++ b/include/HttpGetter.h @@ -9,7 +9,17 @@ #include #include -using up_http_client_t = std::unique_ptr; +class HttpGetterClient : public HTTPClient { +public: + void restartTCP() { + // keeps the NetworkClient, and closes the TCP connections (as we + // effectively do not support keep-alive with HTTP 1.0). + HTTPClient::disconnect(true); + HTTPClient::connect(); + } +}; + +using up_http_client_t = std::unique_ptr; using sp_wifi_client_t = std::shared_ptr; class HttpRequestResult { @@ -59,7 +69,7 @@ class HttpGetter { char const* getErrorText() const { return _errBuffer; } private: - String getAuthDigest(String const& authReq, unsigned int counter); + std::pair getAuthDigest(); HttpRequestConfig const& _config; template @@ -71,6 +81,9 @@ class HttpGetter { String _uri; uint16_t _port; + String _wwwAuthenticate = ""; + unsigned _nonceCounter = 0; + sp_wifi_client_t _spWiFiClient; // reused for multiple HTTP requests std::vector> _additionalHeaders; diff --git a/include/MqttHandleHuawei.h b/include/MqttHandleHuawei.h index f7f6f4c20..c83fdd31f 100644 --- a/include/MqttHandleHuawei.h +++ b/include/MqttHandleHuawei.h @@ -8,11 +8,18 @@ #include #include #include +#include +#include class MqttHandleHuaweiClass { public: void init(Scheduler& scheduler); + void forceUpdate(); + + void subscribeTopics(); + void unsubscribeTopics(); + private: void loop(); @@ -24,6 +31,15 @@ class MqttHandleHuaweiClass { Mode }; + static constexpr frozen::string _cmdtopic = "huawei/cmd/"; + static constexpr frozen::map _subscriptions = { + { "limit_online_voltage", Topic::LimitOnlineVoltage }, + { "limit_online_current", Topic::LimitOnlineCurrent }, + { "limit_offline_voltage", Topic::LimitOfflineVoltage }, + { "limit_offline_current", Topic::LimitOfflineCurrent }, + { "mode", Topic::Mode }, + }; + void onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index 12f2a714c..fb449cba0 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -7,11 +7,18 @@ #include #include #include +#include +#include class MqttHandlePowerLimiterClass { public: void init(Scheduler& scheduler); + void forceUpdate(); + + void subscribeTopics(); + void unsubscribeTopics(); + private: void loop(); @@ -28,6 +35,20 @@ class MqttHandlePowerLimiterClass { TargetPowerConsumption }; + static constexpr frozen::string _cmdtopic = "powerlimiter/cmd/"; + static constexpr frozen::map _subscriptions = { + { "threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold }, + { "threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold }, + { "threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC }, + { "threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold }, + { "threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold }, + { "threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage }, + { "threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage }, + { "mode", MqttPowerLimiterCommand::Mode }, + { "upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit }, + { "target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption }, + }; + void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h index a6c97a4c3..2a9e1dd2d 100644 --- a/include/PowerMeterHttpJson.h +++ b/include/PowerMeterHttpJson.h @@ -42,7 +42,7 @@ class PowerMeterHttpJson : public PowerMeterProvider { uint32_t _lastPoll = 0; mutable std::mutex _valueMutex; - power_values_t _powerValues; + power_values_t _powerValues = {}; std::array, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters; diff --git a/include/SPIPortManager.h b/include/SPIPortManager.h new file mode 100644 index 000000000..d3d58896f --- /dev/null +++ b/include/SPIPortManager.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include + +/** + * SPI# to SPI ID and SPI_HOST mapping + * + * ESP32 + * | SPI # | SPI ID | SPI_HOST | + * | 2 | 2 | 1 | + * | 3 | 3 | 2 | + * + * ESP32-S3 + * | SPI # | SPI ID | SPI_HOST | + * | 2 | 0 | 1 | + * | 3 | 1 | 2 | + * + * ESP32-C3 + * | SPI # | SPI ID | SPI_HOST | + * | 2 | 0 | 1 | + * + */ + +class SPIPortManagerClass { +public: + void init(); + + std::optional allocatePort(std::string const& owner); + void freePort(std::string const& owner); + spi_host_device_t SPIhostNum(uint8_t spi_num); + +private: + // the amount of SPIs available on supported ESP32 chips + #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 + static size_t constexpr _num_controllers = 4; + #else + static size_t constexpr _num_controllers = 3; + #endif + + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + static int8_t constexpr _offset_spi_num = -2; // FSPI=0, HSPI=1 + static int8_t constexpr _offset_spi_host = 1; // SPI1_HOST=0 but not usable, SPI2_HOST=1 and SPI3_HOST=2, first usable is SPI2_HOST + #else + static int8_t constexpr _offset_spi_num = 0; // HSPI=2, VSPI=3 + static int8_t constexpr _offset_spi_host = -1; // SPI1_HOST=0 but not usable, SPI2_HOST=1 and SPI3_HOST=2, first usable is SPI2_HOST + #endif + + std::array _ports = { "" }; +}; + +extern SPIPortManagerClass SPIPortManager; diff --git a/include/defaults.h b/include/defaults.h index e348ae63f..d92b8645d 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -144,8 +144,8 @@ #define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001 #define POWERLIMITER_RESTART_HOUR -1 #define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100 -#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0 -#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0 +#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 66.0 +#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 66.0 #define BATTERY_ENABLED false #define BATTERY_PROVIDER 0 // Pylontech CAN receiver diff --git a/lib/CMT2300a/cmt2300a_hal.c b/lib/CMT2300a/cmt2300a_hal.c index 7b07d499f..73f6cab71 100644 --- a/lib/CMT2300a/cmt2300a_hal.c +++ b/lib/CMT2300a/cmt2300a_hal.c @@ -26,9 +26,9 @@ * @name CMT2300A_InitSpi * @desc Initializes the CMT2300A SPI interface. * *********************************************************/ -void CMT2300A_InitSpi(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) +void CMT2300A_InitSpi(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) { - cmt_spi3_init(pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed); + cmt_spi3_init(spi_host, pin_sdio, pin_clk, pin_cs, pin_fcs, spi_speed); } /*! ******************************************************** diff --git a/lib/CMT2300a/cmt2300a_hal.h b/lib/CMT2300a/cmt2300a_hal.h index a465b1490..150e10f09 100644 --- a/lib/CMT2300a/cmt2300a_hal.h +++ b/lib/CMT2300a/cmt2300a_hal.h @@ -23,6 +23,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -36,7 +37,7 @@ extern "C" { #define CMT2300A_GetTickCount() millis() /* ************************************************************************ */ -void CMT2300A_InitSpi(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); +void CMT2300A_InitSpi(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); uint8_t CMT2300A_ReadReg(const uint8_t addr); void CMT2300A_WriteReg(const uint8_t addr, const uint8_t dat); diff --git a/lib/CMT2300a/cmt2300wrapper.cpp b/lib/CMT2300a/cmt2300wrapper.cpp index 016ef56fd..f04bdeea9 100644 --- a/lib/CMT2300a/cmt2300wrapper.cpp +++ b/lib/CMT2300a/cmt2300wrapper.cpp @@ -7,8 +7,9 @@ #include "cmt2300a_params_860.h" #include "cmt2300a_params_900.h" -CMT2300A::CMT2300A(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t spi_speed) +CMT2300A::CMT2300A(const spi_host_device_t spi_host, const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t spi_speed) { + _spi_host = spi_host; _pin_sdio = pin_sdio; _pin_clk = pin_clk; _pin_cs = pin_cs; @@ -266,7 +267,7 @@ void CMT2300A::flush_rx(void) bool CMT2300A::_init_pins() { - CMT2300A_InitSpi(_pin_sdio, _pin_clk, _pin_cs, _pin_fcs, _spi_speed); + CMT2300A_InitSpi(_spi_host, _pin_sdio, _pin_clk, _pin_cs, _pin_fcs, _spi_speed); return true; // assuming pins are connected properly } diff --git a/lib/CMT2300a/cmt2300wrapper.h b/lib/CMT2300a/cmt2300wrapper.h index d1639fe9b..b818d972b 100644 --- a/lib/CMT2300a/cmt2300wrapper.h +++ b/lib/CMT2300a/cmt2300wrapper.h @@ -2,6 +2,7 @@ #pragma once #include +#include #define CMT2300A_ONE_STEP_SIZE 2500 // frequency channel step size for fast frequency hopping operation: One step size is 2.5 kHz. #define FH_OFFSET 100 // value * CMT2300A_ONE_STEP_SIZE = channel frequency offset @@ -18,7 +19,7 @@ enum FrequencyBand_t { class CMT2300A { public: - CMT2300A(const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t _spi_speed = CMT_SPI_SPEED); + CMT2300A(const spi_host_device_t spi_host, const uint8_t pin_sdio, const uint8_t pin_clk, const uint8_t pin_cs, const uint8_t pin_fcs, const uint32_t _spi_speed = CMT_SPI_SPEED); bool begin(void); @@ -128,6 +129,7 @@ class CMT2300A { */ bool _init_radio(); + spi_host_device_t _spi_host; int8_t _pin_sdio; int8_t _pin_clk; int8_t _pin_cs; diff --git a/lib/CMT2300a/cmt_spi3.c b/lib/CMT2300a/cmt_spi3.c index 59aad36f7..7263f4d17 100644 --- a/lib/CMT2300a/cmt_spi3.c +++ b/lib/CMT2300a/cmt_spi3.c @@ -1,6 +1,5 @@ #include "cmt_spi3.h" #include -#include #include // for esp_rom_gpio_connect_out_signal SemaphoreHandle_t paramLock = NULL; @@ -9,14 +8,9 @@ SemaphoreHandle_t paramLock = NULL; } while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS) #define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock) -// for ESP32 this is the so-called HSPI -// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore, -// it is simply the first externally usable hardware SPI master controller -#define SPI_CMT SPI2_HOST - spi_device_handle_t spi_reg, spi_fifo; -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) +void cmt_spi3_init(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed) { paramLock = xSemaphoreCreateMutex(); @@ -43,8 +37,8 @@ void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin .post_cb = NULL, }; - ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED)); - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg)); + ESP_ERROR_CHECK(spi_bus_initialize(spi_host, &buscfg, SPI_DMA_DISABLED)); + ESP_ERROR_CHECK(spi_bus_add_device(spi_host, &devcfg, &spi_reg)); // FiFo spi_device_interface_config_t devcfg2 = { @@ -61,9 +55,9 @@ void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin .pre_cb = NULL, .post_cb = NULL, }; - ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo)); + ESP_ERROR_CHECK(spi_bus_add_device(spi_host, &devcfg2, &spi_fifo)); - esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false); + esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[spi_host].spid_out, true, false); delay(100); } diff --git a/lib/CMT2300a/cmt_spi3.h b/lib/CMT2300a/cmt_spi3.h index 6d3a67b62..5cce47db7 100644 --- a/lib/CMT2300a/cmt_spi3.h +++ b/lib/CMT2300a/cmt_spi3.h @@ -2,8 +2,9 @@ #define __CMT_SPI3_H #include +#include -void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); +void cmt_spi3_init(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed); void cmt_spi3_write(const uint8_t addr, const uint8_t dat); uint8_t cmt_spi3_read(const uint8_t addr); diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 67fe497c4..fe7e3ee28 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -31,9 +31,9 @@ void HoymilesClass::initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, co _radioNrf->init(initialisedSpiBus, pinCE, pinIRQ); } -void HoymilesClass::initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) +void HoymilesClass::initCMT(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { - _radioCmt->init(pin_sdio, pin_clk, pin_cs, pin_fcs, pin_gpio2, pin_gpio3); + _radioCmt->init(spi_host, pin_sdio, pin_clk, pin_cs, pin_fcs, pin_gpio2, pin_gpio3); } void HoymilesClass::loop() diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 86a7d6ca6..9c578f3ac 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -9,6 +9,7 @@ #include #include #include +#include #define HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL (2 * 60 * 1000) // 2 minutes #define HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION (4 * 60 * 1000) // at least 4 minutes between sending limit command and read request. Otherwise eventlog entry @@ -17,7 +18,7 @@ class HoymilesClass { public: void init(); void initNRF(SPIClass* initialisedSpiBus, const uint8_t pinCE, const uint8_t pinIRQ); - void initCMT(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); + void initCMT(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); void setMessageOutput(Print* output); @@ -54,4 +55,4 @@ class HoymilesClass { Print* _messageOutput = &Serial; }; -extern HoymilesClass Hoymiles; \ No newline at end of file +extern HoymilesClass Hoymiles; diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index 035e52f46..1a203cb41 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -83,11 +83,11 @@ bool HoymilesRadio_CMT::cmtSwitchDtuFreq(const uint32_t to_frequency) return true; } -void HoymilesRadio_CMT::init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) +void HoymilesRadio_CMT::init(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3) { _dtuSerial.u64 = 0; - _radio.reset(new CMT2300A(pin_sdio, pin_clk, pin_cs, pin_fcs)); + _radio.reset(new CMT2300A(spi_host, pin_sdio, pin_clk, pin_cs, pin_fcs)); _radio->begin(); diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.h b/lib/Hoymiles/src/HoymilesRadio_CMT.h index 770617fe3..b6e54430e 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.h +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.h @@ -9,6 +9,7 @@ #include #include #include +#include // number of fragments hold in buffer #define FRAGMENT_BUFFER_SIZE 30 @@ -41,7 +42,7 @@ struct CountryFrequencyList_t { class HoymilesRadio_CMT : public HoymilesRadio { public: - void init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); + void init(const spi_host_device_t spi_host, const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int8_t pin_gpio2, const int8_t pin_gpio3); void loop(); void setPALevel(const int8_t paLevel); void setInverterTargetFrequency(const uint32_t frequency); diff --git a/pio-scripts/compile_webapp.py b/pio-scripts/compile_webapp.py new file mode 100644 index 000000000..0ae54e769 --- /dev/null +++ b/pio-scripts/compile_webapp.py @@ -0,0 +1,71 @@ +import os +import hashlib +import pickle +import subprocess + +def check_files(directories, filepaths, hash_file): + old_file_hashes = {} + file_hashes = {} + + for directory in directories: + for root, dirs, filenames in os.walk(directory): + for file in filenames: + filepaths.append(os.path.join(root, file)) + + for file_path in filepaths: + with open(file_path, 'rb') as f: + file_data = f.read() + file_hashes[file_path] = hashlib.md5(file_data).hexdigest() + + if os.path.exists(hash_file): + with open(hash_file, 'rb') as f: + old_file_hashes = pickle.load(f) + + update = False + for file_path, file_hash in file_hashes.items(): + if file_path not in old_file_hashes or old_file_hashes[file_path] != file_hash: + update = True + break + + if not update: + print("INFO: webapp artifacts should be up-to-date") + return + + print("INFO: compiling webapp (hang on, this can take a while and there might be little output)...") + + yarn = "yarn" + try: + subprocess.check_output([yarn, "--version"]) + except FileNotFoundError: + yarn = "yarnpkg" + try: + subprocess.check_output([yarn, "--version"]) + except FileNotFoundError: + raise Exception("it seems neither 'yarn' nor 'yarnpkg' is installed/available on your system") + + # if these commands fail, an exception will prevent us from + # persisting the current hashes => commands will be executed again + subprocess.run([yarn, "--cwd", "webapp", "install", "--frozen-lockfile"], + check=True) + + subprocess.run([yarn, "--cwd", "webapp", "build"], check=True) + + with open(hash_file, 'wb') as f: + pickle.dump(file_hashes, f) + +def main(): + if os.getenv('GITHUB_ACTIONS') == 'true': + print("INFO: not testing for up-to-date webapp artifacts when running as Github action") + return 0 + + print("INFO: testing for up-to-date webapp artifacts") + + directories = ["webapp/src/", "webapp/public/"] + files = ["webapp/index.html", "webapp/tsconfig.config.json", + "webapp/tsconfig.json", "webapp/vite.config.ts", + "webapp/yarn.lock"] + hash_file = "webapp_dist/.hashes.pkl" + + check_files(directories, files, hash_file) + +main() diff --git a/platformio.ini b/platformio.ini index f9435984c..b7917a10e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -50,6 +50,7 @@ lib_deps = plerup/EspSoftwareSerial @ ^8.2.0 extra_scripts = + pre:pio-scripts/compile_webapp.py pre:pio-scripts/auto_firmware_version.py pre:pio-scripts/patch_apply.py post:pio-scripts/create_factory_bin.py diff --git a/src/BatteryCanReceiver.cpp b/src/BatteryCanReceiver.cpp index aca563bbb..90ea7b33d 100644 --- a/src/BatteryCanReceiver.cpp +++ b/src/BatteryCanReceiver.cpp @@ -26,6 +26,15 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName) auto rx = static_cast(pin.battery_rx); twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL); + // interrupts at level 1 are in high demand, at least on ESP32-S3 boards, + // but only a limited amount can be allocated. failing to allocate an + // interrupt in the TWAI driver will cause a bootloop. we therefore + // register the TWAI driver's interrupt at level 2. level 2 interrupts + // should be available -- we don't really know. we would love to have the + // esp_intr_dump() function, but that's not available yet in our version + // of the underlying esp-idf. + g_config.intr_flags = ESP_INTR_FLAG_LEVEL2; + // Initialize configuration structures using macro initializers twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 9f32931b7..e4fe305a5 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -54,6 +54,19 @@ static void addLiveViewAlarm(JsonVariant& root, std::string const& name, root["issues"][name] = 2; } +void BatteryStats::setManufacturer(const String& m) +{ + String sanitized(m); + for (int i = 0; i < sanitized.length(); i++) { + char c = sanitized[i]; + if (c < 0x20 || c >= 0x80) { + sanitized.remove(i); // Truncate string + break; + } + } + _manufacturer = std::move(sanitized); +} + bool BatteryStats::updateAvailable(uint32_t since) const { if (_lastUpdate == 0) { return false; } // no data at all processed yet @@ -475,7 +488,7 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) { using Label = JkBms::DataPointLabel; - _manufacturer = "JKBMS"; + setManufacturer("JKBMS"); auto oProductId = dp.get(); if (oProductId.has_value()) { // the first twelve chars are expected to be the "User Private Data" @@ -483,10 +496,10 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) // name, which can be changed at will using the smartphone app. so // there is not always a "JK" in this string. if there is, we still cut // the string there to avoid possible regressions. - _manufacturer = oProductId->substr(12).c_str(); + setManufacturer(String(oProductId->substr(12).c_str())); auto pos = oProductId->rfind("JK"); if (pos != std::string::npos) { - _manufacturer = oProductId->substr(pos).c_str(); + setManufacturer(String(oProductId->substr(pos).c_str())); } } @@ -558,7 +571,7 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& s _timeToGo = shuntData.TTG / 60; _chargedEnergy = static_cast(shuntData.H18) / 100; _dischargedEnergy = static_cast(shuntData.H17) / 100; - _manufacturer = String("Victron ") + shuntData.getPidAsString().data(); + setManufacturer(String("Victron ") + shuntData.getPidAsString().data()); _temperature = shuntData.T; _tempPresent = shuntData.tempPresent; _midpointVoltage = static_cast(shuntData.VM) / 1000; diff --git a/src/HttpGetter.cpp b/src/HttpGetter.cpp index 8b2e158e9..664c35dd6 100644 --- a/src/HttpGetter.cpp +++ b/src/HttpGetter.cpp @@ -2,6 +2,7 @@ #include "HttpGetter.h" #include #include "mbedtls/sha256.h" +#include "mbedtls/md5.h" #include #include @@ -100,7 +101,7 @@ HttpRequestResult HttpGetter::performGetRequest() } } - auto upTmpHttpClient = std::make_unique(); + auto upTmpHttpClient = std::make_unique(); // use HTTP1.0 to avoid problems with chunked transfer encoding when the // stream is later used to read the server's response. @@ -135,8 +136,23 @@ HttpRequestResult HttpGetter::performGetRequest() break; } case Auth_t::Digest: { - const char *headers[1] = {"WWW-Authenticate"}; - upTmpHttpClient->collectHeaders(headers, 1); + // send "Connection: keep-alive" (despite using HTTP/1.0, where + // "Connection: close" is the default) so there is a chance to + // reuse the TCP connection when performing the second GET request. + upTmpHttpClient->setReuse(true); + + const char *headers[2] = {"WWW-Authenticate", "Connection"}; + upTmpHttpClient->collectHeaders(headers, 2); + + // try with new auth response based on previous WWW-Authenticate + // header, which allows us to retrieve the resource without a + // second GET request. if the server decides that we reused the + // previous challenge too often, it will respond with HTTP401 and + // a new challenge, which we handle as if we had no challenge yet. + auto authorization = getAuthDigest(); + if (authorization.first) { + upTmpHttpClient->addHeader("Authorization", authorization.second); + } break; } } @@ -144,14 +160,36 @@ HttpRequestResult HttpGetter::performGetRequest() int httpCode = upTmpHttpClient->GET(); if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) { + _wwwAuthenticate = ""; + if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) { logError("Cannot perform digest authentication as server did " "not send a WWW-Authenticate header"); return { false }; } - String authReq = upTmpHttpClient->header("WWW-Authenticate"); - String authorization = getAuthDigest(authReq, 1); - upTmpHttpClient->addHeader("Authorization", authorization); + + _wwwAuthenticate = upTmpHttpClient->header("WWW-Authenticate"); + + // using a new WWW-Authenticate challenge means + // we never used the server's nonce in a response + _nonceCounter = 0; + + auto authorization = getAuthDigest(); + if (!authorization.first) { + logError("Digest Error: %s", authorization.second.c_str()); + return { false }; + } + upTmpHttpClient->addHeader("Authorization", authorization.second); + + // use a new TCP connection if the server sent "Connection: close". + bool restart = true; + if (upTmpHttpClient->hasHeader("Connection")) { + String connection = upTmpHttpClient->header("Connection"); + connection.toLowerCase(); + restart = connection.indexOf("keep-alive") == -1; + } + if (restart) { upTmpHttpClient->restartTCP(); } + httpCode = upTmpHttpClient->GET(); } @@ -168,6 +206,29 @@ HttpRequestResult HttpGetter::performGetRequest() return { true, std::move(upTmpHttpClient), _spWiFiClient }; } +template +static String bin2hex(uint8_t* hash) { + size_t constexpr kOutLen = binLen * 2 + 1; + char res[kOutLen]; + for (int i = 0; i < binLen; i++) { + snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); + } + return res; +} + +static String md5(const String& data) { + uint8_t hash[16]; + + mbedtls_md5_context ctx; + mbedtls_md5_init(&ctx); + mbedtls_md5_starts_ret(&ctx); + mbedtls_md5_update_ret(&ctx, reinterpret_cast(data.c_str()), data.length()); + mbedtls_md5_finish_ret(&ctx, hash); + mbedtls_md5_free(&ctx); + + return bin2hex(hash); +} + static String sha256(const String& data) { uint8_t hash[32]; @@ -178,12 +239,7 @@ static String sha256(const String& data) { mbedtls_sha256_finish(&ctx, hash); mbedtls_sha256_free(&ctx); - char res[sizeof(hash) * 2 + 1]; - for (int i = 0; i < sizeof(hash); i++) { - snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); - } - - return res; + return bin2hex(hash); } static String extractParam(String const& authReq, String const& param, char delimiter) { @@ -204,30 +260,45 @@ static String getcNonce(int len) { return s; } -String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) { +static std::pair getAlgo(String const& authReq) { + // the algorithm is NOT enclosed in double quotes, so we can't use extractParam + auto paramBegin = authReq.indexOf("algorithm="); + if (paramBegin == -1) { return { true, "MD5" }; } // default as per RFC2617 + auto valueBegin = paramBegin + 10; + + String algo = authReq.substring(valueBegin, valueBegin + 3); + if (algo == "MD5") { return { true, algo }; } + + algo = authReq.substring(valueBegin, valueBegin + 7); + if (algo == "SHA-256") { return { true, algo }; } + + return { false, "unsupported digest algorithm" }; +} + +std::pair HttpGetter::getAuthDigest() { + if (_wwwAuthenticate.isEmpty()) { return { false, "no digest challenge yet" }; } + // extracting required parameters for RFC 2617 Digest - String realm = extractParam(authReq, "realm=\"", '"'); - String nonce = extractParam(authReq, "nonce=\"", '"'); - String cNonce = getcNonce(8); + String realm = extractParam(_wwwAuthenticate, "realm=\"", '"'); + String nonce = extractParam(_wwwAuthenticate, "nonce=\"", '"'); + String cNonce = getcNonce(8); // client nonce char nc[9]; - snprintf(nc, sizeof(nc), "%08x", counter); - - // sha256 of the user:realm:password - String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password); + snprintf(nc, sizeof(nc), "%08x", ++_nonceCounter); - // sha256 of method:uri - String ha2 = sha256("GET:" + _uri); + auto algo = getAlgo(_wwwAuthenticate); + if (!algo.first) { return { false, algo.second }; } - // sha256 of h1:nonce:nc:cNonce:auth:h2 - String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + + auto hash = (algo.second == "SHA-256") ? &sha256 : &md5; + String ha1 = hash(String(_config.Username) + ":" + realm + ":" + _config.Password); + String ha2 = hash("GET:" + _uri); + String response = hash(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2); - // Final authorization String - return String("Digest username=\"") + _config.Username + + return { true, String("Digest username=\"") + _config.Username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + _uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc + - ", qop=auth, response=\"" + response + "\", algorithm=SHA-256"; + ", qop=auth, response=\"" + response + "\", algorithm=" + algo.second }; } void HttpGetter::addHeader(char const* key, char const* value) diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index aef57a198..adab23f0e 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -9,6 +9,7 @@ #include "PowerLimiter.h" #include "Configuration.h" #include "Battery.h" +#include "SPIPortManager.h" #include #include @@ -35,7 +36,11 @@ void HuaweiCanCommunicationTask(void* parameter) { bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { - SPI = new SPIClass(HSPI); + + auto oSPInum = SPIPortManager.allocatePort("Huawei CAN"); + if (!oSPInum) { return false; } + + SPI = new SPIClass(*oSPInum); SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); pinMode(huawei_cs, OUTPUT); digitalWrite(huawei_cs, HIGH); @@ -231,7 +236,8 @@ void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, ui _mode = HUAWEI_MODE_AUTO_INT; } - xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl); + xTaskCreate(HuaweiCanCommunicationTask, "HUAWEI_CAN_0", 2048/*stack size*/, + NULL/*params*/, 0/*prio*/, &_HuaweiCanCommunicationTaskHdl); MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!"); _initialized = true; diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 3be1927c7..ffe4d8455 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -7,22 +7,9 @@ #include "MessageOutput.h" #include "PinMapping.h" #include "SunPosition.h" +#include "SPIPortManager.h" #include -// the NRF shall use the second externally usable HW SPI controller -// for ESP32 that is the so-called VSPI, for ESP32-S2/S3 it is now called implicitly -// HSPI, as it has shifted places for these chip generations -// for all generations, this is equivalent to SPI3_HOST in the lower level driver -// For ESP32-C2, the only externally usable HW SPI controller is SPI2, its signal names -// being prefixed with FSPI. -#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 -#define SPI_NRF HSPI -#elif CONFIG_IDF_TARGET_ESP32C3 -#define SPI_NRF FSPI -#else -#define SPI_NRF VSPI -#endif - InverterSettingsClass InverterSettings; InverterSettingsClass::InverterSettingsClass() @@ -37,24 +24,32 @@ void InverterSettingsClass::init(Scheduler& scheduler) const PinMapping_t& pin = PinMapping.get(); // Initialize inverter communication - MessageOutput.print("Initialize Hoymiles interface... "); + MessageOutput.println("Initialize Hoymiles interface... "); Hoymiles.setMessageOutput(&MessageOutput); Hoymiles.init(); if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) { if (PinMapping.isValidNrf24Config()) { - SPIClass* spiClass = new SPIClass(SPI_NRF); - spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); - Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); + auto oSPInum = SPIPortManager.allocatePort("NRF24"); + + if (oSPInum) { + SPIClass* spiClass = new SPIClass(*oSPInum); + spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); + Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq); + } } if (PinMapping.isValidCmt2300Config()) { - Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); - MessageOutput.println(" Setting country mode... "); - Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); - MessageOutput.println(" Setting CMT target frequency... "); - Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); + auto oSPInum = SPIPortManager.allocatePort("CMT2300A"); + + if (oSPInum) { + Hoymiles.initCMT(SPIPortManager.SPIhostNum(*oSPInum), pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); + MessageOutput.println(" Setting country mode... "); + Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); + MessageOutput.println(" Setting CMT target frequency... "); + Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); + } } MessageOutput.println(" Setting radio PA level... "); diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 2afab15dc..f376458ab 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -6,7 +6,6 @@ #include "MessageOutput.h" #include "MqttSettings.h" #include "Huawei_can.h" -// #include "Failsafe.h" #include "WebApi_Huawei.h" #include @@ -19,10 +18,22 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler) _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); + subscribeTopics(); + + _lastPublish = millis(); +} + +void MqttHandleHuaweiClass::forceUpdate() +{ + _lastPublish = 0; +} + +void MqttHandleHuaweiClass::subscribeTopics() +{ String const& prefix = MqttSettings.getPrefix(); auto subscribe = [&prefix, this](char const* subTopic, Topic t) { - String fullTopic(prefix + "huawei/cmd/" + subTopic); + String fullTopic(prefix + _cmdtopic.data() + subTopic); MqttSettings.subscribe(fullTopic.c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t, std::placeholders::_1, std::placeholders::_2, @@ -30,16 +41,18 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler) std::placeholders::_5, std::placeholders::_6)); }; - subscribe("limit_online_voltage", Topic::LimitOnlineVoltage); - subscribe("limit_online_current", Topic::LimitOnlineCurrent); - subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage); - subscribe("limit_offline_current", Topic::LimitOfflineCurrent); - subscribe("mode", Topic::Mode); - - _lastPublish = millis(); - + for (auto const& s : _subscriptions) { + subscribe(s.first.data(), s.second); + } } +void MqttHandleHuaweiClass::unsubscribeTopics() +{ + String const prefix = MqttSettings.getPrefix() + _cmdtopic.data(); + for (auto const& s : _subscriptions) { + MqttSettings.unsubscribe(prefix + s.first.data()); + } +} void MqttHandleHuaweiClass::loop() { diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index f01a9b146..744320ed7 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -25,10 +25,22 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; + subscribeTopics(); + + _lastPublish = millis(); +} + +void MqttHandlePowerLimiterClass::forceUpdate() +{ + _lastPublish = 0; +} + +void MqttHandlePowerLimiterClass::subscribeTopics() +{ String const& prefix = MqttSettings.getPrefix(); auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) { - String fullTopic(prefix + "powerlimiter/cmd/" + subTopic); + String fullTopic(prefix + _cmdtopic.data() + subTopic); MqttSettings.subscribe(fullTopic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command, std::placeholders::_1, std::placeholders::_2, @@ -36,20 +48,18 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) std::placeholders::_5, std::placeholders::_6)); }; - subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold); - subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold); - subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC); - subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold); - subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold); - subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage); - subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); - subscribe("mode", MqttPowerLimiterCommand::Mode); - subscribe("upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit); - subscribe("target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption); - - _lastPublish = millis(); + for (auto const& s : _subscriptions) { + subscribe(s.first.data(), s.second); + } } +void MqttHandlePowerLimiterClass::unsubscribeTopics() +{ + String const prefix = MqttSettings.getPrefix() + _cmdtopic.data(); + for (auto const& s : _subscriptions) { + MqttSettings.unsubscribe(prefix + s.first.data()); + } +} void MqttHandlePowerLimiterClass::loop() { diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 1c471a3dc..c4f5382ea 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -251,8 +251,7 @@ void PowerLimiterClass::loop() if (isStartThresholdReached()) { return true; } - if (config.PowerLimiter.SolarPassThroughEnabled && - config.PowerLimiter.BatteryAlwaysUseAtNight && + if (config.PowerLimiter.BatteryAlwaysUseAtNight && !isDayPeriod && !_batteryDischargeEnabled) { _nighttimeDischarging = true; @@ -267,7 +266,7 @@ void PowerLimiterClass::loop() _batteryDischargeEnabled = getBatteryPower(); if (_verboseLogging && !config.PowerLimiter.IsInverterSolarPowered) { - MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n", + MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %f %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n", (config.Battery.Enabled?"enabled":"disabled"), Battery.getStats()->getSoC(), config.PowerLimiter.BatterySocStartThreshold, diff --git a/src/PowerMeterUdpSmaHomeManager.cpp b/src/PowerMeterUdpSmaHomeManager.cpp index 2baa9c43a..d68275c2c 100644 --- a/src/PowerMeterUdpSmaHomeManager.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -18,7 +18,7 @@ void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tar { if (!_verboseLogging) { return; } - MessageOutput.printf("[PowerMeterUdpSmaHomeManager] %s = %.1f (timestamp %d)\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] %s = %.1f (timestamp %u)\r\n", name, value, timestamp); } @@ -139,6 +139,7 @@ uint8_t* PowerMeterUdpSmaHomeManager::decodeGroup(uint8_t* offset, uint16_t grou Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp); Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp); count = 0; + gotUpdate(); } continue; diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index 517a6a230..d1e7d94c7 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -31,7 +31,7 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message) _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] soc: %d soh: %d\r\n", + MessageOutput.printf("[Pylontech] soc: %f soh: %d\r\n", _stats->getSoC(), _stats->_stateOfHealth); } break; @@ -106,7 +106,7 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message) MessageOutput.printf("[Pylontech] Manufacturer: %s\r\n", manufacturer.c_str()); } - _stats->setManufacturer(std::move(manufacturer)); + _stats->setManufacturer(manufacturer); break; } diff --git a/src/PytesCanReceiver.cpp b/src/PytesCanReceiver.cpp index 81c7c85cc..e4b22e51a 100644 --- a/src/PytesCanReceiver.cpp +++ b/src/PytesCanReceiver.cpp @@ -32,7 +32,7 @@ void PytesCanReceiver::onMessage(twai_message_t rx_message) _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); if (_verboseLogging) { - MessageOutput.printf("[Pytes] soc: %d soh: %d\r\n", + MessageOutput.printf("[Pytes] soc: %f soh: %d\r\n", _stats->getSoC(), _stats->_stateOfHealth); } break; @@ -127,7 +127,7 @@ void PytesCanReceiver::onMessage(twai_message_t rx_message) MessageOutput.printf("[Pytes] Manufacturer: %s\r\n", manufacturer.c_str()); } - _stats->setManufacturer(std::move(manufacturer)); + _stats->setManufacturer(manufacturer); break; } diff --git a/src/SPIPortManager.cpp b/src/SPIPortManager.cpp new file mode 100644 index 000000000..8b64c423c --- /dev/null +++ b/src/SPIPortManager.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SPIPortManager.h" +#include "MessageOutput.h" + +SPIPortManagerClass SPIPortManager; +static constexpr char TAG[] = "[SPIPortManager]"; + +void SPIPortManagerClass::init() { + MessageOutput.printf("%s SPI0 and SPI1 reserved by 'Flash and PSRAM'\r\n", TAG); + _ports[0] = "Flash"; + _ports[1] = "PSRAM"; +} + +std::optional SPIPortManagerClass::allocatePort(std::string const& owner) +{ + for (size_t i = 0; i < _ports.size(); ++i) { + if (_ports[i] != "") { + MessageOutput.printf("%s SPI%d already in use by '%s'\r\n", TAG, i, _ports[i].c_str()); + continue; + } + + _ports[i] = owner; + + MessageOutput.printf("%s SPI%d now in use by '%s'\r\n", TAG, i, owner.c_str()); + + return i + _offset_spi_num; + } + + MessageOutput.printf("%s Cannot assign another SPI port to '%s'\r\n", TAG, owner.c_str()); + return std::nullopt; +} + +void SPIPortManagerClass::freePort(std::string const& owner) +{ + for (size_t i = 0; i < _ports.size(); ++i) { + if (_ports[i] != owner) { continue; } + + MessageOutput.printf("%s Freeing SPI%d, owner was '%s'\r\n", TAG, i + _offset_spi_num, owner.c_str()); + _ports[i] = ""; + } +} + +spi_host_device_t SPIPortManagerClass::SPIhostNum(uint8_t spi_num) +{ + return (spi_host_device_t)(spi_num + _offset_spi_host); +} diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index 4e084974c..a994e20d6 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -87,7 +87,7 @@ bool VictronMpptClass::isDataValid() const if (upController->isDataValid()) { return true; } } - return !_controllers.empty(); + return false; } bool VictronMpptClass::isDataValid(size_t idx) const diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 8e587f852..0c038c227 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -91,11 +91,11 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) auto& retMsg = response->getRoot(); - if (!(root.containsKey("serial") + if (!(root.containsKey("serial") && root.containsKey("pollinterval") - && root.containsKey("verbose_logging") - && root.containsKey("nrf_palevel") - && root.containsKey("cmt_palevel") + && root.containsKey("verbose_logging") + && root.containsKey("nrf_palevel") + && root.containsKey("cmt_palevel") && root.containsKey("cmt_frequency") && root.containsKey("cmt_country"))) { retMsg["message"] = "Values are missing!"; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 4f76256df..601cc55c9 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -4,8 +4,12 @@ */ #include "WebApi_mqtt.h" #include "Configuration.h" +#include "MqttHandleBatteryHass.h" #include "MqttHandleHass.h" +#include "MqttHandlePowerLimiterHass.h" #include "MqttHandleInverter.h" +#include "MqttHandleHuawei.h" +#include "MqttHandlePowerLimiter.h" #include "MqttHandleVedirectHass.h" #include "MqttHandleVedirect.h" #include "MqttSettings.h" @@ -307,8 +311,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) // Check if base topic was changed if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) { MqttHandleInverter.unsubscribeTopics(); + MqttHandleHuawei.unsubscribeTopics(); + MqttHandlePowerLimiter.unsubscribeTopics(); + strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); MqttHandleInverter.subscribeTopics(); + MqttHandleHuawei.subscribeTopics(); + MqttHandlePowerLimiter.subscribeTopics(); } WebApi.writeConfig(retMsg); @@ -316,8 +325,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttSettings.performReconnect(); + + MqttHandleBatteryHass.forceUpdate(); MqttHandleHass.forceUpdate(); + MqttHandlePowerLimiterHass.forceUpdate(); MqttHandleVedirectHass.forceUpdate(); + + MqttHandleHuawei.forceUpdate(); + MqttHandlePowerLimiter.forceUpdate(); MqttHandleVedirect.forceUpdate(); } diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index b28380d3c..adea6105b 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -153,13 +153,13 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) if (config.Vedirect.Enabled) { config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as(); config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as(); - config.PowerLimiter.BatteryAlwaysUseAtNight= root["battery_always_use_at_night"].as(); config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast(root["full_solar_passthrough_start_voltage"].as() * 100) / 100.0; config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast(root["full_solar_passthrough_stop_voltage"].as() * 100) / 100.0; } config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as(); config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as(); + config.PowerLimiter.BatteryAlwaysUseAtNight = root["battery_always_use_at_night"].as(); config.PowerLimiter.UseOverscalingToCompensateShading = root["use_overscaling_to_compensate_shading"].as(); config.PowerLimiter.InverterId = root["inverter_serial"].as(); config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as(); diff --git a/src/main.cpp b/src/main.cpp index 7489a27d8..5743f4288 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include "Led_Single.h" #include "MessageOutput.h" #include "SerialPortManager.h" +#include "SPIPortManager.h" #include "VictronMppt.h" #include "Battery.h" #include "Huawei_can.h" @@ -47,7 +48,7 @@ void setup() Serial.begin(SERIAL_BAUDRATE); #if ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(0); - delay(100); + delay(200); #else while (!Serial) yield(); @@ -97,7 +98,9 @@ void setup() const auto& pin = PinMapping.get(); MessageOutput.println("done"); + // Initialize PortManagers SerialPortManager.init(); + SPIPortManager.init(); // Initialize WiFi MessageOutput.print("Initialize Network... "); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 7771a0c22..384959405 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -635,7 +635,7 @@ "TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.", "TargetPowerConsumptionHysteresis": "Hysterese", "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zurückgemeldeten Limit um mindestens diesen Betrag abweicht.", - "LowerPowerLimit": "Minmales Leistungslimit", + "LowerPowerLimit": "Minimales Leistungslimit", "LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.", "BaseLoadLimit": "Grundlast", "BaseLoadLimitHint": "Relevant beim Betrieb ohne oder beim Ausfall des Stromzählers. Solange es die sonstigen Bedinungen zulassen (insb. Batterieladung) wird dieses Limit am Wechselrichter eingestellt.", @@ -778,7 +778,7 @@ "about": { "AboutOpendtu": "Über OpenDTU-OnBattery", "Documentation": "Dokumentation", - "DocumentationBody": "Die Firmware- und Hardware-Dokumentation des Basis-Projektes ist hier zu finden: https://www.opendtu.solar
Zusätzliche Informationen, insbesondere zu OpenDTU-OnBattery-spezifischen Funktionen, gibt es im Wiki auf Github.", + "DocumentationBody": "Die maßgebliche Firmware- und Hardware-Dokumentation ist erreichbar unter https://opendtu-onbattery.net.", "ProjectOrigin": "Projekt Ursprung", "ProjectOriginBody1": "OpenDTU-OnBattery ist eine Erweiterung von OpenDTU. Das Basis-Projekt OpenDTU wurde aus dieser Diskussion (mikrocontroller.net) heraus gestartet.", "ProjectOriginBody2": "Das Hoymiles-Protokoll wurde durch die freiwilligen Bemühungen vieler Teilnehmer entschlüsselt. OpenDTU wurde unter anderem auf der Grundlage dieser Arbeit entwickelt. Das Projekt ist unter einer Open-Source-Lizenz lizenziert (GNU General Public License version 2).", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 0be700f01..b94ad01b9 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -781,7 +781,7 @@ "about": { "AboutOpendtu": "About OpenDTU-OnBattery", "Documentation": "Documentation", - "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", + "DocumentationBody": "The canonical firmware and hardware documentation is accessible at https://opendtu-onbattery.net.", "ProjectOrigin": "Project Origin", "ProjectOriginBody1": "OpenDTU-OnBattery is a fork of OpenDTU. The upstream project OpenDTU was started from this discussion. (Mikrocontroller.net)", "ProjectOriginBody2": "The Hoymiles protocol was decrypted through the voluntary efforts of many participants. OpenDTU, among others, was developed based on this work. The project is licensed under an Open Source License (GNU General Public License version 2).", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 316d19d69..852dc8331 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -754,7 +754,7 @@ "about": { "AboutOpendtu": "À propos d'OpenDTU-OnBattery", "Documentation": "Documentation", - "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", + "DocumentationBody": "La documentation canonique sur les microprogrammes et le matériel est accessible à l'adresse https://opendtu-onbattery.net.", "ProjectOrigin": "Origine du projet", "ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion (Mikrocontroller.net).", "ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (GNU General Public License version 2).", diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index c87d8ffe5..05b7bdd9c 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -105,6 +105,14 @@ wide /> + +
- - { @@ -91,7 +92,7 @@ export default defineComponent({ .then((data) => { if (data.total_commits > 0) { this.systemDataList.update_text = this.$t('systeminfo.VersionNew'); - this.systemDataList.update_status = 'text-bg-danger'; + this.systemDataList.update_status = 'text-bg-warning'; this.systemDataList.update_url = data.html_url; } else { this.systemDataList.update_text = this.$t('systeminfo.VersionOk'); diff --git a/webapp_dist/favicon.ico b/webapp_dist/favicon.ico deleted file mode 100644 index 68e73986d..000000000 Binary files a/webapp_dist/favicon.ico and /dev/null differ diff --git a/webapp_dist/favicon.png b/webapp_dist/favicon.png deleted file mode 100644 index 278aac84f..000000000 Binary files a/webapp_dist/favicon.png and /dev/null differ diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz deleted file mode 100644 index be84fc7db..000000000 Binary files a/webapp_dist/index.html.gz and /dev/null differ diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz deleted file mode 100644 index 42d83ecb0..000000000 Binary files a/webapp_dist/js/app.js.gz and /dev/null differ diff --git a/webapp_dist/site.webmanifest b/webapp_dist/site.webmanifest deleted file mode 100644 index 3be246091..000000000 --- a/webapp_dist/site.webmanifest +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "OpenDTU", - "short_name": "OpenDTU", - "display": "standalone", - "orientation": "portrait", - "icons": [ - { - "src": "/favicon.png", - "sizes": "144x144", - "type": "image/png" - } - ] -} \ No newline at end of file diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz deleted file mode 100644 index 02f82db68..000000000 Binary files a/webapp_dist/zones.json.gz and /dev/null differ