Compare commits

...

46 Commits

Author SHA1 Message Date
Thomas Basler
653efb41a2 Fix: Syntax error in defines 2025-01-14 23:07:46 +01:00
Thomas Basler
571ba2f350 Fix: Hint regarding required device profile is shown for profiles which don't need a device profile
This patch introduces a define which allows to specifiy for each environment if a device profile is absolutly required.

Fixes #2500
2025-01-14 22:13:48 +01:00
Thomas Basler
220cfbf7ae Update nrf24/RF24 from 1.4.10 to 1.4.11 2025-01-14 19:17:14 +01:00
Thomas Basler
db130f646e Upgrade ESPAsyncWebServer from 3.4.2 to 3.6.0 2025-01-14 19:14:17 +01:00
Thomas Basler
5510c9ff57 webapp: add app.js.gz 2025-01-14 18:43:41 +01:00
Thomas Basler
ebf4e921ee Merge branch 'pr2420' into dev 2025-01-14 18:38:23 +01:00
Thomas Basler
d068542c94 Merge branch 'pr2421' into dev 2025-01-14 18:37:40 +01:00
Thomas Basler
19fa310f43 webapp: Update dependencies 2025-01-14 18:35:45 +01:00
Thomas Basler
87772cb76b Feature: Add support for HERF-600 inverters
Fixes #2492
2025-01-08 17:52:33 +01:00
Thomas Basler
50207a42bf webapp: Update dependencies 2024-12-31 16:08:56 +01:00
Thomas Basler
a0e6942537 Feature: Detect if inverter supports 'Power Distribution Logic'
The detection of 'Power Distribution Logic' is based on the firmware version for specific models and is needed to disable any means of overscaling, as it simply does not work when 'Power Distrbution Logic' is available.

Based on the code from @AndreasBoehm
2024-12-31 16:08:37 +01:00
Thomas Basler
c37397acca Fix lint errors 2024-12-30 00:16:14 +01:00
Thomas Basler
498afe377b Remove extra semikolon 2024-12-30 00:06:08 +01:00
Thomas Basler
d43ac7fb92 Update bblanchon/ArduinoJson from 7.2.1 to 7.3.0 2024-12-29 22:32:43 +01:00
Thomas Basler
11105944be Fix: Uptime overflow after ~50 days
Fixes #2473
2024-12-29 20:53:20 +01:00
Thomas Basler
c7fa4ff212 Disable queue debugging 2024-12-21 11:59:51 +01:00
Thomas Basler
96ba58af8c Fix: Wifi.begin was called with wrong parameters
The third parameter should be a optional channel name and not a scan method. There exists a separate method for the scan method.
2024-12-18 23:00:27 +01:00
Thomas Basler
d485d1b820 Show totals in blue and producing inverters in green 2024-12-18 22:16:23 +01:00
Thomas Basler
8f60a3a12a Feature: Show inverter status and current power in overview (if multiple inverters are available) 2024-12-18 21:21:10 +01:00
Thomas Basler
940027ab19 Upgrade ESPAsyncWebServer from 3.4.1 to 3.4.2 2024-12-18 19:31:56 +01:00
Thomas Basler
24b3f27364 webapp: Update dependencies 2024-12-16 20:50:09 +01:00
Thomas Basler
5265c6281f Feature: Set Limit transfer only to "OK" if the queue does not contain any more commands 2024-12-15 20:45:32 +01:00
Thomas Basler
8acae28c59 Feature: New handling of command queue
Goal of this change is to  prevent a overflow in the command queue by flooding it with MQTT commands and therefor also prevent  the reading of the inverter data.

To achieve this it is now possible to specify a insert type for each  queue element.
2024-12-15 20:45:32 +01:00
Thomas Basler
0061d5e159 Upgrade ESPAsyncWebServer from 3.4.0 to 3.4.1 2024-12-15 20:45:15 +01:00
Thomas Basler
5d14454185 Fix: Auto reboot was not triggered on pin mapping change 2024-12-14 13:33:44 +01:00
Thomas Basler
58382be16c Feature: show hint if device profile missing or not selected 2024-12-14 13:07:31 +01:00
Thomas Basler
2edec642fb Fix: Remove temperature readings for ESP32-S2 modules
For some reasons this leads to WDT resets on this kind of module.
This is just a workaround until another solution is found.
2024-12-14 12:36:50 +01:00
Thomas Basler
726a08ec2c Upgrade ESPAsyncWebServer from 3.3.23 to 3.4.0 2024-12-13 21:07:50 +01:00
Thomas Basler
d775ee9e89 Update bblanchon/ArduinoJson from 7.2.0 to 7.2.1 2024-12-05 22:23:18 +01:00
Thomas Basler
1c1fcbea51 Upgrade ESPAsyncWebServer from 3.3.22 to 3.3.23 2024-12-05 22:22:45 +01:00
Thomas Basler
bf89fd7558 webapp: Update dependencies 2024-12-05 22:21:48 +01:00
Bernhard Kirchen
8247070aae webapp: improve styling of hints on home view
* increase the spacing between the icon and the text.
* put the text into its own box so it does not flow around the icon.
* vertically center the icon to account for multiple lines of text.
* vertically center the text to account for a single line of text.

closes #1441.
2024-12-03 22:59:25 +01:00
Thomas Basler
241ee1e99d webapp: Update dependencies 2024-12-02 23:07:55 +01:00
Thomas Basler
a75543c309 Feature:: Added support for HMS-450 inverters which begin with 1400 2024-12-02 22:45:50 +01:00
Bernhard Kirchen
1c5a3cf6fe webapp: avoid undefined serial for InputSerial
if variables are set with 'const foo = {} as Inverter', then
'foo.serial' will be undefined, causing warnings and errors
when using InputSerial.
2024-12-02 22:20:52 +01:00
Bernhard Kirchen
b2dcac549c Fix: need to skip BOM also when migrating config 2024-12-02 22:20:12 +01:00
Bernhard Kirchen
37b173071e webapp: fix line break for reload button
if a page uses the reload button, it had only 1 column of space, and
only if the viewport was at least "sm". this is not the case for typical
smartphones, in which case the reload button would appear on its own row
instead of to the right.

we now limit the heading to 10 columns if and only if the reload button
is to be used, otherwise the heading uses all 12 columns, regardless of
the viewport size. the reload buton uses two columns -- if it is
displayed at all.

the font size of the icon is increased slightly.

as the font size of h1 headings changes with the viewport size, we need
to center both the heading and the button vertically.
2024-12-02 22:19:03 +01:00
Florian
041ae7bae7 add Sum of DC Powrr
add Sum of DC power of all enabled inverters to Homeassistant MQTT autodiscovery
2024-11-22 08:21:13 +01:00
stefan123t
680863fb00
Update README.md 2024-11-20 08:02:07 +01:00
stefan123t
8297591853
change markdown table to github style 2024-11-20 08:00:26 +01:00
Thomas Basler
cc3290be8e Use correct variable 2024-11-13 19:21:41 +01:00
Thomas Basler
9bfded055a Remove not required string generation 2024-11-13 19:20:56 +01:00
Thomas Basler
2b07e3c2c8 Organize includes 2024-11-12 20:23:28 +01:00
Thomas Basler
eac2e2fb39 Fix: Really always execute the generate of the factory.bin file 2024-11-11 23:52:59 +01:00
Thomas Basler
d843ac6422 Fix comment 2024-11-11 22:11:57 +01:00
Thomas Basler
33a9b7454c Make function getClientId const 2024-11-10 02:45:42 +01:00
65 changed files with 1277 additions and 964 deletions

View File

@ -20,7 +20,7 @@ public:
void unsubscribe(const String& topic); void unsubscribe(const String& topic);
String getPrefix() const; String getPrefix() const;
String getClientId(); String getClientId() const;
private: private:
void NetworkEvent(network_event event); void NetworkEvent(network_event event);

View File

@ -59,6 +59,8 @@ public:
bool init(const String& deviceMapping); bool init(const String& deviceMapping);
PinMapping_t& get(); PinMapping_t& get();
bool isMappingSelected() const { return _mappingSelected; }
bool isValidNrf24Config() const; bool isValidNrf24Config() const;
bool isValidCmt2300Config() const; bool isValidCmt2300Config() const;
bool isValidW5500Config() const; bool isValidW5500Config() const;
@ -68,6 +70,8 @@ public:
private: private:
PinMapping_t _pinMapping; PinMapping_t _pinMapping;
bool _mappingSelected = false;
}; };
extern PinMappingClass PinMapping; extern PinMappingClass PinMapping;

View File

@ -12,7 +12,7 @@ public:
private: private:
AsyncWebSocket _ws; AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth; AsyncAuthenticationMiddleware _simpleDigestAuth;
Task _wsCleanupTask; Task _wsCleanupTask;
void wsCleanupTaskCb(); void wsCleanupTaskCb();

View File

@ -25,7 +25,7 @@ private:
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebSocket _ws; AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth; AsyncAuthenticationMiddleware _simpleDigestAuth;
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };

View File

@ -45,7 +45,9 @@
"Refreshing": "Refrescando", "Refreshing": "Refrescando",
"Pull": "Tira hacia abajo para refrescar", "Pull": "Tira hacia abajo para refrescar",
"Release": "Soltar para refrescar", "Release": "Soltar para refrescar",
"Close": "Cerrar" "Close": "Cerrar",
"Yes": "Yes",
"No": "No"
}, },
"wait": { "wait": {
"NotReady": "OpenDTU is not yet ready", "NotReady": "OpenDTU is not yet ready",
@ -193,7 +195,10 @@
"FirmwareVersion": "Versión del firmware", "FirmwareVersion": "Versión del firmware",
"FirmwareBuildDate": "Fecha de construcción del firmware", "FirmwareBuildDate": "Fecha de construcción del firmware",
"HardwarePartNumber": "Número de parte de hardware", "HardwarePartNumber": "Número de parte de hardware",
"HardwareVersion": "Versión de hardware" "HardwareVersion": "Versión de hardware",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
}, },
"gridprofile": { "gridprofile": {
"NoInfo": "@:devinfo.NoInfo", "NoInfo": "@:devinfo.NoInfo",
@ -637,7 +642,8 @@
"TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.", "TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.",
"TimeSyncLink": "Por favor, verifica la configuración de tu hora.", "TimeSyncLink": "Por favor, verifica la configuración de tu hora.",
"DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.", "DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.",
"DefaultPasswordLink": "Por favor, cambia la contraseña." "DefaultPasswordLink": "Por favor, cambia la contraseña.",
"PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (<code>pin_mapping.json</code>) or have not selected a profile defined there. Please refer to the <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> for details."
}, },
"deviceadmin": { "deviceadmin": {
"DeviceManager": "Administrador de Dispositivos", "DeviceManager": "Administrador de Dispositivos",

View File

@ -45,7 +45,9 @@
"Refreshing": "Aggiorna", "Refreshing": "Aggiorna",
"Pull": "Trascina in basso per aggiornare", "Pull": "Trascina in basso per aggiornare",
"Release": "Rilascia per aggiornare", "Release": "Rilascia per aggiornare",
"Close": "Chiudi" "Close": "Chiudi",
"Yes": "Yes",
"No": "No"
}, },
"wait": { "wait": {
"NotReady": "OpenDTU is not yet ready", "NotReady": "OpenDTU is not yet ready",
@ -193,7 +195,10 @@
"FirmwareVersion": "Versione Firmware", "FirmwareVersion": "Versione Firmware",
"FirmwareBuildDate": "Data Firmware", "FirmwareBuildDate": "Data Firmware",
"HardwarePartNumber": "Hardware Part Number", "HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version" "HardwareVersion": "Hardware Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
}, },
"gridprofile": { "gridprofile": {
"NoInfo": "@:devinfo.NoInfo", "NoInfo": "@:devinfo.NoInfo",
@ -637,7 +642,8 @@
"TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.", "TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.",
"TimeSyncLink": "Controlla le impostazioni Data/Ora.", "TimeSyncLink": "Controlla le impostazioni Data/Ora.",
"DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.", "DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.",
"DefaultPasswordLink": "Per favore cambia la password." "DefaultPasswordLink": "Per favore cambia la password.",
"PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (<code>pin_mapping.json</code>) or have not selected a profile defined there. Please refer to the <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> for details."
}, },
"deviceadmin": { "deviceadmin": {
"DeviceManager": "Device-Manager", "DeviceManager": "Device-Manager",

View File

@ -19,6 +19,12 @@ CpuTemperatureClass CpuTemperature;
float CpuTemperatureClass::read() float CpuTemperatureClass::read()
{ {
#ifdef CONFIG_IDF_TARGET_ESP32S2
// Disabling temperature reading for ESP32-S2 models as it might lead to WDT resets.
// See: https://github.com/espressif/esp-idf/issues/8088
return NAN;
#endif
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
float temperature = NAN; float temperature = NAN;

View File

@ -57,7 +57,7 @@ void HoymilesClass::loop()
} }
} }
if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { if (iv != nullptr && iv->getRadio()->isInitialized()) {
if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) {
iv->Statistics()->zeroRuntimeData(); iv->Statistics()->zeroRuntimeData();
@ -119,6 +119,7 @@ void HoymilesClass::loop()
iv->sendGridOnProFileParaRequest(); iv->sendGridOnProFileParaRequest();
} }
_messageOutput->printf("Queue size - NRF: %" PRId32 " CMT: %" PRId32 "\r\n", _radioNrf->getQueueSize(), _radioCmt->getQueueSize());
_lastPoll = millis(); _lastPoll = millis();
} }
@ -229,6 +230,7 @@ void HoymilesClass::removeInverterBySerial(const uint64_t serial)
for (uint8_t i = 0; i < _inverters.size(); i++) { for (uint8_t i = 0; i < _inverters.size(); i++) {
if (_inverters[i]->serial() == serial) { if (_inverters[i]->serial() == serial) {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
_inverters[i]->getRadio()->removeCommands(_inverters[i].get());
_inverters.erase(_inverters.begin() + i); _inverters.erase(_inverters.begin() + i);
return; return;
} }

View File

@ -156,6 +156,16 @@ bool HoymilesRadio::isInitialized() const
return _isInitialized; return _isInitialized;
} }
void HoymilesRadio::removeCommands(InverterAbstract* inv)
{
_commandQueue.removeAllEntriesForInverter(inv);
}
uint8_t HoymilesRadio::countSimilarCommands(std::shared_ptr<CommandAbstract> cmd)
{
return _commandQueue.countSimilarCommands(cmd);
}
bool HoymilesRadio::isIdle() const bool HoymilesRadio::isIdle() const
{ {
return !_busyFlag; return !_busyFlag;
@ -165,3 +175,8 @@ bool HoymilesRadio::isQueueEmpty() const
{ {
return _commandQueue.size() == 0; return _commandQueue.size() == 0;
} }
uint32_t HoymilesRadio::getQueueSize() const
{
return _commandQueue.size();
}

View File

@ -1,11 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "Arduino.h"
#include "commands/CommandAbstract.h" #include "commands/CommandAbstract.h"
#include "queue/CommandQueue.h"
#include "types.h" #include "types.h"
#include <ThreadSafeQueue.h>
#include <TimeoutHelper.h> #include <TimeoutHelper.h>
#include <memory>
#ifdef HOY_DEBUG_QUEUE
#define DEBUG_PRINT(fmt, args...) Serial.printf(fmt, ##args)
#else
#define DEBUG_PRINT(fmt, args...) /* Don't do anything in release builds */
#endif
class HoymilesRadio { class HoymilesRadio {
public: public:
@ -14,11 +20,48 @@ public:
bool isIdle() const; bool isIdle() const;
bool isQueueEmpty() const; bool isQueueEmpty() const;
uint32_t getQueueSize() const;
bool isInitialized() const; bool isInitialized() const;
void removeCommands(InverterAbstract* inv);
uint8_t countSimilarCommands(std::shared_ptr<CommandAbstract> cmd);
void enqueCommand(std::shared_ptr<CommandAbstract> cmd) void enqueCommand(std::shared_ptr<CommandAbstract> cmd)
{ {
DEBUG_PRINT("Queue size before: %ld\r\n", _commandQueue.size());
DEBUG_PRINT("Handling command %s with type %d\r\n", cmd.get()->getCommandName().c_str(), static_cast<uint8_t>(cmd.get()->getQueueInsertType()));
switch (cmd.get()->getQueueInsertType()) {
case QueueInsertType::RemoveOldest:
_commandQueue.removeDuplicatedEntries(cmd);
break;
case QueueInsertType::ReplaceExistent:
// Checks if the queue already contains a command like the new one
// and replaces the existing one with the new one.
// (The new one will not be pushed at the end of the queue)
if (_commandQueue.countSimilarCommands(cmd) > 0) {
DEBUG_PRINT(" ... existing entry will be replaced\r\n");
_commandQueue.replaceEntries(cmd);
return;
}
break;
case QueueInsertType::RemoveNewest:
// Checks if the queue already contains a command like the new one
// and drops the new one. The new one will not be inserted.
if (_commandQueue.countSimilarCommands(cmd) > 0) {
DEBUG_PRINT(" ... new entry will be dropped\r\n");
return;
}
break;
case QueueInsertType::AllowMultiple:
// Dont do anything, just fall through and insert the command.
break;
}
// Push the command into the queue if we reach this position of the code
DEBUG_PRINT(" ... new entry will be appended\r\n");
_commandQueue.push(cmd); _commandQueue.push(cmd);
DEBUG_PRINT("Queue size after: %ld\r\n", _commandQueue.size());
} }
template <typename T> template <typename T>
@ -38,7 +81,7 @@ protected:
void handleReceivedPackage(); void handleReceivedPackage();
serial_u _dtuSerial; serial_u _dtuSerial;
ThreadSafeQueue<std::shared_ptr<CommandAbstract>> _commandQueue; CommandQueue _commandQueue;
bool _isInitialized = false; bool _isInitialized = false;
bool _busyFlag = false; bool _busyFlag = false;

View File

@ -44,7 +44,15 @@ ActivePowerControlCommand::ActivePowerControlCommand(InverterAbstract* inv, cons
String ActivePowerControlCommand::getCommandName() const String ActivePowerControlCommand::getCommandName() const
{ {
return "ActivePowerControl"; char buffer[30];
snprintf(buffer, sizeof(buffer), "ActivePowerControl (%02X)", getType());
return buffer;
}
bool ActivePowerControlCommand::areSameParameter(CommandAbstract* other)
{
return CommandAbstract::areSameParameter(other)
&& this->getType() == static_cast<ActivePowerControlCommand*>(other)->getType();
} }
void ActivePowerControlCommand::setActivePowerLimit(const float limit, const PowerLimitControlType type) void ActivePowerControlCommand::setActivePowerLimit(const float limit, const PowerLimitControlType type)
@ -79,7 +87,10 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons
} }
} }
_inv->SystemConfigPara()->setLastUpdateCommand(millis()); _inv->SystemConfigPara()->setLastUpdateCommand(millis());
_inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); std::shared_ptr<ActivePowerControlCommand> cmd(std::shared_ptr<ActivePowerControlCommand>(), this);
if (_inv->getRadio()->countSimilarCommands(cmd) == 1) {
_inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK);
}
return true; return true;
} }
@ -89,7 +100,7 @@ float ActivePowerControlCommand::getLimit() const
return l / 10; return l / 10;
} }
PowerLimitControlType ActivePowerControlCommand::getType() PowerLimitControlType ActivePowerControlCommand::getType() const
{ {
return (PowerLimitControlType)((static_cast<uint16_t>(_payload[14]) << 8) | _payload[15]); return (PowerLimitControlType)((static_cast<uint16_t>(_payload[14]) << 8) | _payload[15]);
} }

View File

@ -15,11 +15,13 @@ public:
explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0);
virtual String getCommandName() const; virtual String getCommandName() const;
virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::RemoveOldest; }
virtual bool areSameParameter(CommandAbstract* other);
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(); virtual void gotTimeout();
void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent); void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent);
float getLimit() const; float getLimit() const;
PowerLimitControlType getType(); PowerLimitControlType getType() const;
}; };

View File

@ -138,3 +138,9 @@ uint8_t CommandAbstract::getMaxRetransmitCount() const
{ {
return MAX_RETRANSMIT_COUNT; return MAX_RETRANSMIT_COUNT;
} }
bool CommandAbstract::areSameParameter(CommandAbstract* other)
{
return this->getCommandName() == other->getCommandName()
&& this->_targetAddress == other->getTargetAddress();
}

View File

@ -11,6 +11,18 @@
class InverterAbstract; class InverterAbstract;
enum class QueueInsertType {
AllowMultiple,
// Remove from beginning of the queue
RemoveOldest,
// Don't insert command if it already exist
RemoveNewest,
// Replace the existing entry in the queue by the one to be added
ReplaceExistent,
};
class CommandAbstract { class CommandAbstract {
public: public:
explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0); explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0);
@ -46,6 +58,10 @@ public:
// Sets the amount how often a missing fragment is re-requested if it was not available // Sets the amount how often a missing fragment is re-requested if it was not available
virtual uint8_t getMaxRetransmitCount() const; virtual uint8_t getMaxRetransmitCount() const;
// Returns whether multiple instances of this command are allowed in the command queue.
virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::RemoveNewest; }
virtual bool areSameParameter(CommandAbstract* other);
protected: protected:
uint8_t _payload[RF_LEN]; uint8_t _payload[RF_LEN];
uint8_t _payload_size; uint8_t _payload_size;

View File

@ -8,6 +8,7 @@ public:
explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0);
virtual String getCommandName() const; virtual String getCommandName() const;
virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::AllowMultiple; }
virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id);
virtual void gotTimeout(); virtual void gotTimeout();

View File

@ -30,7 +30,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial) HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HERF_1CH::isValidSerial(const uint64_t serial) bool HERF_1CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -37,7 +37,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial) HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HERF_2CH::isValidSerial(const uint64_t serial) bool HERF_2CH::isValidSerial(const uint64_t serial)
{ {
@ -48,7 +50,7 @@ bool HERF_2CH::isValidSerial(const uint64_t serial)
String HERF_2CH::typeName() const String HERF_2CH::typeName() const
{ {
return "HERF-800-2T"; return "HERF-600/800-2T";
} }
const byteAssign_t* HERF_2CH::getByteAssignment() const const byteAssign_t* HERF_2CH::getByteAssignment() const

View File

@ -5,7 +5,9 @@
#include "HERF_4CH.h" #include "HERF_4CH.h"
HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial) HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_4CH(radio, serial) {}; : HM_4CH(radio, serial)
{
}
bool HERF_4CH::isValidSerial(const uint64_t serial) bool HERF_4CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -29,7 +29,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {}; : HMS_Abstract(radio, serial)
{
}
bool HMS_1CH::isValidSerial(const uint64_t serial) bool HMS_1CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -29,18 +29,20 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {}; : HMS_Abstract(radio, serial)
{
}
bool HMS_1CHv2::isValidSerial(const uint64_t serial) bool HMS_1CHv2::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x112500000000 && serial <= 0x1125ffffffff // serial >= 0x112500000000 && serial <= 0x1125ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1125; return preSerial == 0x1125 || preSerial == 0x1400;
} }
String HMS_1CHv2::typeName() const String HMS_1CHv2::typeName() const
{ {
return "HMS-500-1T v2"; return "HMS-450/500-1T v2";
} }
const byteAssign_t* HMS_1CHv2::getByteAssignment() const const byteAssign_t* HMS_1CHv2::getByteAssignment() const

View File

@ -36,7 +36,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {}; : HMS_Abstract(radio, serial)
{
}
bool HMS_2CH::isValidSerial(const uint64_t serial) bool HMS_2CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -50,7 +50,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial)
: HMS_Abstract(radio, serial) {}; : HMS_Abstract(radio, serial)
{
}
bool HMS_4CH::isValidSerial(const uint64_t serial) bool HMS_4CH::isValidSerial(const uint64_t serial)
{ {
@ -73,3 +75,10 @@ uint8_t HMS_4CH::getByteAssignmentSize() const
{ {
return sizeof(byteAssignment) / sizeof(byteAssignment[0]); return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
} }
bool HMS_4CH::supportsPowerDistributionLogic()
{
// This feature was added in inverter firmware version 01.01.12 and
// will limit the AC output instead of limiting the DC inputs.
return DevInfo()->getFwBuildVersion() >= 10112U;
}

View File

@ -10,4 +10,5 @@ public:
String typeName() const; String typeName() const;
const byteAssign_t* getByteAssignment() const; const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const; uint8_t getByteAssignmentSize() const;
bool supportsPowerDistributionLogic() final;
}; };

View File

@ -59,7 +59,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial)
: HMT_Abstract(radio, serial) {}; : HMT_Abstract(radio, serial)
{
}
bool HMT_4CH::isValidSerial(const uint64_t serial) bool HMT_4CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -73,7 +73,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial)
: HMT_Abstract(radio, serial) {}; : HMT_Abstract(radio, serial)
{
}
bool HMT_6CH::isValidSerial(const uint64_t serial) bool HMT_6CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -12,7 +12,7 @@ HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) : HM_Abstract(radio, serial)
{ {
EventLog()->setMessageType(AlarmMessageType_t::HMT); EventLog()->setMessageType(AlarmMessageType_t::HMT);
}; }
bool HMT_Abstract::sendChangeChannelRequest() bool HMT_Abstract::sendChangeChannelRequest()
{ {
@ -26,4 +26,4 @@ bool HMT_Abstract::sendChangeChannelRequest()
_radio->enqueCommand(cmdChannel); _radio->enqueCommand(cmdChannel);
return true; return true;
}; }

View File

@ -29,7 +29,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_1CH::isValidSerial(const uint64_t serial) bool HM_1CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -37,7 +37,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_2CH::isValidSerial(const uint64_t serial) bool HM_2CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -50,7 +50,9 @@ static const byteAssign_t byteAssignment[] = {
}; };
HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {}; : HM_Abstract(radio, serial)
{
}
bool HM_4CH::isValidSerial(const uint64_t serial) bool HM_4CH::isValidSerial(const uint64_t serial)
{ {

View File

@ -14,7 +14,9 @@
#include "commands/SystemConfigParaCommand.h" #include "commands/SystemConfigParaCommand.h"
HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial) HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial)
: InverterAbstract(radio, serial) {}; : InverterAbstract(radio, serial)
{
}
bool HM_Abstract::sendStatsRequest() bool HM_Abstract::sendStatsRequest()
{ {
@ -217,3 +219,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest()
return true; return true;
} }
bool HM_Abstract::supportsPowerDistributionLogic()
{
return false;
}

View File

@ -16,6 +16,7 @@ public:
bool sendRestartControlRequest(); bool sendRestartControlRequest();
bool resendPowerControlRequest(); bool resendPowerControlRequest();
bool sendGridOnProFileParaRequest(); bool sendGridOnProFileParaRequest();
bool supportsPowerDistributionLogic() override;
private: private:
uint8_t _lastAlarmLogCnt = 0; uint8_t _lastAlarmLogCnt = 0;

View File

@ -103,6 +103,9 @@ public:
virtual bool sendChangeChannelRequest(); virtual bool sendChangeChannelRequest();
virtual bool sendGridOnProFileParaRequest() = 0; virtual bool sendGridOnProFileParaRequest() = 0;
// This feature will limit the AC output instead of limiting the DC inputs.
virtual bool supportsPowerDistributionLogic() = 0;
HoymilesRadio* getRadio(); HoymilesRadio* getRadio();
AlarmLogParser* EventLog(); AlarmLogParser* EventLog();

View File

@ -1,16 +1,16 @@
# Class overview # Class overview
| Class | Models | Serial range | | Class | Models | Serial range |
| --------------| --------------------------- | ------------- -- | | ------------- | --------------------------- | ---------------- |
| HM_1CH | HM-300/350/400-1T | 1121 | | HM_1CH | HM-300/350/400-1T | 1121 |
| HM_2CH | HM-600/700/800-2T | 1141 | | HM_2CH | HM-600/700/800-2T | 1141 |
| HM_4CH | HM-1000/1200/1500-4T | 1161 | | HM_4CH | HM-1000/1200/1500-4T | 1161 |
| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | | HMS_1CH | HMS-300/350/400/450/500-1T | 1124 |
| HMS_1CHv2 | HMS-500-1T v2 | 1125 | | HMS_1CHv2 | HMS-450/500-1T v2 | 1125, 1400 |
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 | | HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 |
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 | | HMT_6CH | HMT-1800/2250-6T | 1382 |
| HERF_1CH | HERF 300 | 2841 | | HERF_1CH | HERF 300 | 2841 |
| HERF_2CH | HERF 800 | 2821 | | HERF_2CH | HERF 600/800 | 2821 |
| HERF_4CH | HERF 1800 | 2801 | | HERF_4CH | HERF 1800 | 2801 |

View File

@ -81,6 +81,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01
{ { 0xF1, 0x01, 0x10, ALL }, 600, "HERF-600" }, // 00
{ { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00
{ { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00
{ { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "CommandQueue.h"
#include "../inverters/InverterAbstract.h"
#include <algorithm>
void CommandQueue::removeAllEntriesForInverter(InverterAbstract* inv)
{
std::lock_guard<std::mutex> lock(_mutex);
auto it = std::remove_if(_queue.begin(), _queue.end(),
[&inv](std::shared_ptr<CommandAbstract> v) -> bool { return v.get()->getTargetAddress() == inv->serial(); });
_queue.erase(it, _queue.end());
}
void CommandQueue::removeDuplicatedEntries(std::shared_ptr<CommandAbstract> cmd)
{
std::lock_guard<std::mutex> lock(_mutex);
auto it = std::remove_if(_queue.begin() + 1, _queue.end(),
[&cmd](std::shared_ptr<CommandAbstract> v) -> bool {
return cmd->areSameParameter(v.get())
&& cmd.get()->getQueueInsertType() == QueueInsertType::RemoveOldest;
});
_queue.erase(it, _queue.end());
}
void CommandQueue::replaceEntries(std::shared_ptr<CommandAbstract> cmd)
{
std::lock_guard<std::mutex> lock(_mutex);
std::replace_if(_queue.begin() + 1, _queue.end(),
[&cmd](std::shared_ptr<CommandAbstract> v)-> bool {
return cmd.get()->getQueueInsertType() == QueueInsertType::ReplaceExistent
&& cmd->areSameParameter(v.get());
},
cmd
);
}
uint8_t CommandQueue::countSimilarCommands(std::shared_ptr<CommandAbstract> cmd)
{
std::lock_guard<std::mutex> lock(_mutex);
return std::count_if(_queue.begin(), _queue.end(),
[&cmd](std::shared_ptr<CommandAbstract> v) -> bool {
return cmd->areSameParameter(v.get());
});
}

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "../commands/CommandAbstract.h"
#include <ThreadSafeQueue.h>
#include <memory>
class InverterAbstract;
class CommandQueue : public ThreadSafeQueue<std::shared_ptr<CommandAbstract>> {
public:
void removeAllEntriesForInverter(InverterAbstract* inv);
void removeDuplicatedEntries(std::shared_ptr<CommandAbstract> cmd);
void replaceEntries(std::shared_ptr<CommandAbstract> cmd);
uint8_t countSimilarCommands(std::shared_ptr<CommandAbstract> cmd);
};

View File

@ -3,7 +3,7 @@
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <queue> #include <deque>
template <typename T> template <typename T>
class ThreadSafeQueue { class ThreadSafeQueue {
@ -33,14 +33,14 @@ public:
return {}; return {};
} }
T tmp = _queue.front(); T tmp = _queue.front();
_queue.pop(); _queue.pop_front();
return tmp; return tmp;
} }
void push(const T& item) void push(const T& item)
{ {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
_queue.push(item); _queue.push_back(item);
} }
T front() T front()
@ -49,6 +49,10 @@ public:
return _queue.front(); return _queue.front();
} }
protected:
std::deque<T> _queue;
mutable std::mutex _mutex;
private: private:
// Moved out of public interface to prevent races between this // Moved out of public interface to prevent races between this
// and pop(). // and pop().
@ -56,7 +60,4 @@ private:
{ {
return _queue.empty(); return _queue.empty();
} }
std::queue<T> _queue;
mutable std::mutex _mutex;
}; };

View File

@ -49,7 +49,7 @@ def get_firmware_specifier_build_flag():
build_version = get_build_version() build_version = get_build_version()
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
build_branch = get_build_branch() build_branch = get_build_branch()
build_flag += " -D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\"" build_flag += " -D AUTO_GIT_BRANCH=\\\"" + build_branch + "\\\""
return (build_flag) return (build_flag)

View File

@ -130,4 +130,5 @@ def esp32_create_combined_bin(source, target, env):
esptool.main(cmd) esptool.main(cmd)
env.AddPostAction("buildprog", esp32_create_combined_bin) from SCons.Script import AlwaysBuild
AlwaysBuild(env.AddPostAction("buildprog", esp32_create_combined_bin))

View File

@ -30,6 +30,7 @@ build_flags =
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
-DEMC_TASK_STACK_SIZE=6400 -DEMC_TASK_STACK_SIZE=6400
; -DHOY_DEBUG_QUEUE
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of ; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and ; https://github.com/espressif/arduino-esp32/issues/9044 and
@ -41,10 +42,10 @@ build_unflags =
-std=gnu++11 -std=gnu++11
lib_deps = lib_deps =
mathieucarbou/ESPAsyncWebServer @ 3.3.22 mathieucarbou/ESPAsyncWebServer @ 3.6.0
bblanchon/ArduinoJson @ 7.2.0 bblanchon/ArduinoJson @ 7.3.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.10 nrf24/RF24 @ 1.4.11
olikraus/U8g2 @ 2.36.2 olikraus/U8g2 @ 2.36.2
buelowp/sunset @ 1.1.7 buelowp/sunset @ 1.1.7
arkhipenko/TaskScheduler @ 3.8.5 arkhipenko/TaskScheduler @ 3.8.5
@ -78,6 +79,7 @@ upload_protocol = esptool
[env:generic_esp32] [env:generic_esp32]
board = esp32dev board = esp32dev
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32_16mb_psram] [env:generic_esp32_16mb_psram]
@ -86,6 +88,7 @@ board_build.flash_mode = qio
board_build.partitions = partitions_custom_16mb.csv board_build.partitions = partitions_custom_16mb.csv
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
-DBOARD_HAS_PSRAM -DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue -mfix-esp32-psram-cache-issue
@ -94,6 +97,7 @@ build_flags = ${env.build_flags}
board = esp32-c3-devkitc-02 board = esp32-c3-devkitc-02
custom_patches = ${env.custom_patches} custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32c3_usb] [env:generic_esp32c3_usb]
@ -102,11 +106,13 @@ custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32s3] [env:generic_esp32s3]
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DPIN_MAPPING_REQUIRED=1
[env:generic_esp32s3_usb] [env:generic_esp32s3_usb]
@ -115,6 +121,7 @@ upload_protocol = esp-builtin
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DPIN_MAPPING_REQUIRED=1
[env:generic] [env:generic]

View File

@ -353,6 +353,8 @@ void ConfigurationClass::migrate()
return; return;
} }
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
// Deserialize the JSON document // Deserialize the JSON document

View File

@ -32,7 +32,7 @@ void MqttHandleDtuClass::loop()
return; return;
} }
MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/uptime", String(esp_timer_get_time() / 1000000));
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname());
MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize())); MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize()));

View File

@ -70,6 +70,7 @@ void MqttHandleHassClass::publishConfig()
publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE); publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuSensor("DC Power", "dc/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);

View File

@ -179,7 +179,7 @@ String MqttSettingsClass::getPrefix() const
return Configuration.get().Mqtt.Topic; return Configuration.get().Mqtt.Topic;
} }
String MqttSettingsClass::getClientId() String MqttSettingsClass::getClientId() const
{ {
String clientId = Configuration.get().Mqtt.ClientId; String clientId = Configuration.get().Mqtt.ClientId;
if (clientId == "") { if (clientId == "") {

View File

@ -287,8 +287,7 @@ void NetworkSettingsClass::applyConfig()
MessageOutput.print("new credentials... "); MessageOutput.print("new credentials... ");
WiFi.begin( WiFi.begin(
Configuration.get().WiFi.Ssid, Configuration.get().WiFi.Ssid,
Configuration.get().WiFi.Password, Configuration.get().WiFi.Password);
WIFI_ALL_CHANNEL_SCAN);
} else { } else {
MessageOutput.print("existing credentials... "); MessageOutput.print("existing credentials... ");
WiFi.begin(); WiFi.begin();

View File

@ -212,6 +212,8 @@ bool PinMappingClass::init(const String& deviceMapping)
for (uint8_t i = 0; i < doc.size(); i++) { for (uint8_t i = 0; i < doc.size(); i++) {
String devName = doc[i]["name"] | ""; String devName = doc[i]["name"] | "";
if (devName == deviceMapping) { if (devName == deviceMapping) {
_mappingSelected = true;
strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name)); strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name));
_pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK; _pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK;
_pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS; _pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS;

View File

@ -129,10 +129,14 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
return; return;
} }
bool performRestart = false;
{ {
auto guard = Configuration.getWriteGuard(); auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig(); auto& config = guard.getConfig();
performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping)); strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping));
config.Display.Rotation = root["display"]["rotation"].as<uint8_t>(); config.Display.Rotation = root["display"]["rotation"].as<uint8_t>();
config.Display.PowerSafe = root["display"]["power_safe"].as<bool>(); config.Display.PowerSafe = root["display"]["power_safe"].as<bool>();
@ -149,7 +153,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
} }
auto const& config = Configuration.get(); auto const& config = Configuration.get();
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode)); Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
Display.setOrientation(config.Display.Rotation); Display.setOrientation(config.Display.Rotation);

View File

@ -35,6 +35,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
root["hw_model_name"] = inv->DevInfo()->getHwModelName(); root["hw_model_name"] = inv->DevInfo()->getHwModelName();
root["max_power"] = inv->DevInfo()->getMaxPower(); root["max_power"] = inv->DevInfo()->getMaxPower();
root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr(); root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr();
root["pdl_supported"] = inv->supportsPowerDistributionLogic();
} }
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -5,11 +5,10 @@
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "Configuration.h" #include "Configuration.h"
#include "RestartHelper.h" #include "RestartHelper.h"
#include "Update.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "helper.h" #include "helper.h"
#include <AsyncJson.h> #include <AsyncJson.h>
#include <Update.h>
void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler)
{ {

View File

@ -10,6 +10,10 @@
#include "defaults.h" #include "defaults.h"
#include <AsyncJson.h> #include <AsyncJson.h>
#ifndef PIN_MAPPING_REQUIRED
#define PIN_MAPPING_REQUIRED 0
#endif
WebApiWsLiveClass::WebApiWsLiveClass() WebApiWsLiveClass::WebApiWsLiveClass()
: _ws("/livedata") : _ws("/livedata")
, _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::wsCleanupTaskCb, this)) , _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::wsCleanupTaskCb, this))
@ -125,6 +129,8 @@ void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected()));
hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0;
hintObj["pin_mapping_issue"] = PIN_MAPPING_REQUIRED && !PinMapping.isMappingSelected();
} }
void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv) void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)

View File

@ -88,7 +88,7 @@ void setup()
// Load PinMapping // Load PinMapping
MessageOutput.print("Reading PinMapping... "); MessageOutput.print("Reading PinMapping... ");
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { if (PinMapping.init(Configuration.get().Dev_PinMapping)) {
MessageOutput.print("found valid mapping "); MessageOutput.print("found valid mapping ");
} else { } else {
MessageOutput.print("using default config "); MessageOutput.print("using default config ");
@ -96,7 +96,7 @@ void setup()
const auto& pin = PinMapping.get(); const auto& pin = PinMapping.get();
MessageOutput.println("done"); MessageOutput.println("done");
// Initialize WiFi // Initialize Network
MessageOutput.print("Initialize Network... "); MessageOutput.print("Initialize Network... ");
NetworkSettings.init(scheduler); NetworkSettings.init(scheduler);
MessageOutput.println("done"); MessageOutput.println("done");

View File

@ -17,35 +17,35 @@
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"bootstrap-icons-vue": "^1.11.3", "bootstrap-icons-vue": "^1.11.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"sortablejs": "^1.15.3", "sortablejs": "^1.15.6",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.5.12", "vue": "^3.5.13",
"vue-i18n": "10.0.4", "vue-i18n": "11.0.1",
"vue-router": "^4.4.5" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^5.2.0", "@intlify/unplugin-vue-i18n": "^6.0.3",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/node": "^22.9.0", "@types/node": "^22.10.6",
"@types/pulltorefreshjs": "^0.1.7", "@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@types/spark-md5": "^3.0.5", "@types/spark-md5": "^3.0.5",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-typescript": "^14.1.3", "@vue/eslint-config-typescript": "^14.3.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.7.0",
"eslint": "^9.14.0", "eslint": "^9.18.0",
"eslint-plugin-vue": "^9.30.0", "eslint-plugin-vue": "^9.32.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"pulltorefreshjs": "^0.1.22", "pulltorefreshjs": "^0.1.22",
"sass": "=1.77.6", "sass": "=1.77.6",
"terser": "^5.36.0", "terser": "^5.37.0",
"typescript": "^5.6.3", "typescript": "~5.6.3",
"vite": "^5.4.10", "vite": "^6.0.7",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^3.5.2",
"vue-tsc": "^2.1.10" "vue-tsc": "^2.2.0"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -1,9 +1,9 @@
<template> <template>
<div :class="{ 'container-xxl': !isWideScreen, 'container-fluid': isWideScreen }" role="main"> <div :class="{ 'container-xxl': !isWideScreen, 'container-fluid': isWideScreen }" role="main">
<div class="page-header"> <div class="page-header">
<div class="row"> <div class="row mb-3">
<div class="col-sm-11"> <div :class="'align-content-center ' + (showReload ? 'col-10' : 'col-12')">
<h1> <h1 class="mb-0">
{{ title }} {{ title }}
<span <span
v-if="showWebSocket" v-if="showWebSocket"
@ -14,10 +14,10 @@
></span> ></span>
</h1> </h1>
</div> </div>
<div class="col-sm-1" v-if="showReload"> <div class="col-2 align-content-center" v-if="showReload">
<button <button
type="button" type="button"
class="float-end btn btn-outline-primary" class="float-end btn btn-outline-primary fs-5"
@click="$emit('reload')" @click="$emit('reload')"
v-tooltip v-tooltip
:title="$t('base.Reload')" :title="$t('base.Reload')"

View File

@ -46,6 +46,12 @@
<td>{{ $t('devinfo.HardwareVersion') }}</td> <td>{{ $t('devinfo.HardwareVersion') }}</td>
<td>{{ devInfoList.hw_version }}</td> <td>{{ devInfoList.hw_version }}</td>
</tr> </tr>
<tr>
<td>{{ $t('devinfo.SupportsPowerDistributionLogic') }}</td>
<td>
<StatusBadge :status="devInfoList.pdl_supported" true_text="devinfo.Yes" false_text="devinfo.No" />
</td>
</tr>
</tbody> </tbody>
</table> </table>
</template> </template>
@ -53,6 +59,7 @@
<script lang="ts"> <script lang="ts">
import BootstrapAlert from '@/components/BootstrapAlert.vue'; import BootstrapAlert from '@/components/BootstrapAlert.vue';
import type { DevInfoStatus } from '@/types/DevInfoStatus'; import type { DevInfoStatus } from '@/types/DevInfoStatus';
import StatusBadge from '@/components/StatusBadge.vue';
import { BIconInfoSquare } from 'bootstrap-icons-vue'; import { BIconInfoSquare } from 'bootstrap-icons-vue';
import { defineComponent, type PropType } from 'vue'; import { defineComponent, type PropType } from 'vue';
@ -60,6 +67,7 @@ export default defineComponent({
components: { components: {
BootstrapAlert, BootstrapAlert,
BIconInfoSquare, BIconInfoSquare,
StatusBadge,
}, },
props: { props: {
devInfoList: { type: Object as PropType<DevInfoStatus>, required: true }, devInfoList: { type: Object as PropType<DevInfoStatus>, required: true },

View File

@ -1,16 +1,36 @@
<template> <template>
<BootstrapAlert :show="hints.radio_problem" variant="danger"> <BootstrapAlert :show="hints.radio_problem" variant="danger">
<BIconBroadcast class="fs-4" /> {{ $t('hints.RadioProblem') }} <div class="d-flex">
<div class="align-content-center"><BIconBroadcast class="fs-4" /></div>
<div class="align-content-center ms-3">{{ $t('hints.RadioProblem') }}</div>
</div>
</BootstrapAlert> </BootstrapAlert>
<BootstrapAlert :show="hints.time_sync" variant="danger"> <BootstrapAlert :show="hints.time_sync" variant="danger">
<BIconClock class="fs-4" /> {{ $t('hints.TimeSync') }} <div class="d-flex">
<a @click="gotoTimeSettings" href="#" class="alert-link">{{ $t('hints.TimeSyncLink') }}</a> <div class="align-content-center"><BIconClock class="fs-4" /></div>
<div class="align-content-center ms-3">
{{ $t('hints.TimeSync') }}
<a @click="gotoTimeSettings" href="#" class="alert-link">{{ $t('hints.TimeSyncLink') }}</a>
</div>
</div>
</BootstrapAlert> </BootstrapAlert>
<BootstrapAlert :show="hints.default_password" variant="danger"> <BootstrapAlert :show="hints.default_password" variant="danger">
<BIconExclamationCircle class="fs-4" /> {{ $t('hints.DefaultPassword') }} <div class="d-flex">
<a @click="gotoPasswordSettings" href="#" class="alert-link">{{ $t('hints.DefaultPasswordLink') }}</a> <div class="align-content-center"><BIconExclamationCircle class="fs-4" /></div>
<div class="align-content-center ms-3">
{{ $t('hints.DefaultPassword') }}
<a @click="gotoPasswordSettings" href="#" class="alert-link">{{ $t('hints.DefaultPasswordLink') }}</a>
</div>
</div>
</BootstrapAlert>
<BootstrapAlert :show="hints.pin_mapping_issue" variant="warning">
<div class="d-flex">
<div class="align-content-center"><BIconExclamationCircle class="fs-4" /></div>
<div class="align-content-center ms-3" v-html="$t('hints.PinMappingIssue')"></div>
</div>
</BootstrapAlert> </BootstrapAlert>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="row row-cols-1 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-md-3 g-3">
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldTotal')"> <CardElement centerContent textVariant="text-bg-primary" :text="$t('invertertotalinfo.TotalYieldTotal')">
<h2> <h2>
{{ {{
$n(totalData.YieldTotal.v, 'decimal', { $n(totalData.YieldTotal.v, 'decimal', {
@ -14,7 +14,7 @@
</CardElement> </CardElement>
</div> </div>
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldDay')"> <CardElement centerContent textVariant="text-bg-primary" :text="$t('invertertotalinfo.TotalYieldDay')">
<h2> <h2>
{{ {{
$n(totalData.YieldDay.v, 'decimal', { $n(totalData.YieldDay.v, 'decimal', {
@ -27,7 +27,7 @@
</CardElement> </CardElement>
</div> </div>
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalPower')"> <CardElement centerContent textVariant="text-bg-primary" :text="$t('invertertotalinfo.TotalPower')">
<h2> <h2>
{{ {{
$n(totalData.Power.v, 'decimal', { $n(totalData.Power.v, 'decimal', {

View File

@ -30,7 +30,9 @@
"Refreshing": "Aktualisieren", "Refreshing": "Aktualisieren",
"Pull": "Zum Aktualisieren nach unten ziehen", "Pull": "Zum Aktualisieren nach unten ziehen",
"Release": "Loslassen zum Aktualisieren", "Release": "Loslassen zum Aktualisieren",
"Close": "Schließen" "Close": "Schließen",
"Yes": "Ja",
"No": "Nein"
}, },
"wait": { "wait": {
"NotReady": "OpenDTU ist noch nicht bereit", "NotReady": "OpenDTU ist noch nicht bereit",
@ -179,7 +181,10 @@
"FirmwareVersion": "Firmware-Version", "FirmwareVersion": "Firmware-Version",
"FirmwareBuildDate": "Firmware-Erstellungsdatum", "FirmwareBuildDate": "Firmware-Erstellungsdatum",
"HardwarePartNumber": "Hardware-Teilenummer", "HardwarePartNumber": "Hardware-Teilenummer",
"HardwareVersion": "Hardware-Version" "HardwareVersion": "Hardware-Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' unterstützt",
"Yes": "@:base.Yes",
"No": "@:base.No"
}, },
"gridprofile": { "gridprofile": {
"NoInfo": "@:devinfo.NoInfo", "NoInfo": "@:devinfo.NoInfo",
@ -627,7 +632,8 @@
"TimeSync": "Die Uhr wurde noch nicht synchronisiert. Ohne eine korrekt eingestellte Uhr werden keine Anfragen an den Wechselrichter gesendet. Dies ist kurz nach dem Start normal. Nach einer längeren Laufzeit (>1 Minute) bedeutet es jedoch, dass der NTP-Server nicht erreichbar ist.", "TimeSync": "Die Uhr wurde noch nicht synchronisiert. Ohne eine korrekt eingestellte Uhr werden keine Anfragen an den Wechselrichter gesendet. Dies ist kurz nach dem Start normal. Nach einer längeren Laufzeit (>1 Minute) bedeutet es jedoch, dass der NTP-Server nicht erreichbar ist.",
"TimeSyncLink": "Bitte überprüfen Sie Ihre Zeiteinstellungen.", "TimeSyncLink": "Bitte überprüfen Sie Ihre Zeiteinstellungen.",
"DefaultPassword": "Sie verwenden das Standardpasswort für die Weboberfläche und den Notfall Access Point. Dies ist potenziell unsicher.", "DefaultPassword": "Sie verwenden das Standardpasswort für die Weboberfläche und den Notfall Access Point. Dies ist potenziell unsicher.",
"DefaultPasswordLink": "Bitte ändern Sie das Passwort." "DefaultPasswordLink": "Bitte ändern Sie das Passwort.",
"PinMappingIssue": "Sie verwenden eine generische Firmware, haben jedoch noch keine Datei mit Verdrahtungsprofilen (<code>pin_mapping.json</code>) hochgeladen oder kein dort definiertes Profil ausgewählt. Details hierzu entnehmen Sie bitte der <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">Dokumentation</a>."
}, },
"deviceadmin": { "deviceadmin": {
"DeviceManager": "Hardware-Einstellungen", "DeviceManager": "Hardware-Einstellungen",

View File

@ -30,7 +30,9 @@
"Refreshing": "Refreshing", "Refreshing": "Refreshing",
"Pull": "Pull down to refresh", "Pull": "Pull down to refresh",
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Close" "Close": "Close",
"Yes": "Yes",
"No": "No"
}, },
"wait": { "wait": {
"NotReady": "OpenDTU is not yet ready", "NotReady": "OpenDTU is not yet ready",
@ -179,7 +181,10 @@
"FirmwareVersion": "Firmware Version", "FirmwareVersion": "Firmware Version",
"FirmwareBuildDate": "Firmware Build Date", "FirmwareBuildDate": "Firmware Build Date",
"HardwarePartNumber": "Hardware Part Number", "HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version" "HardwareVersion": "Hardware Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
}, },
"gridprofile": { "gridprofile": {
"NoInfo": "@:devinfo.NoInfo", "NoInfo": "@:devinfo.NoInfo",
@ -627,7 +632,8 @@
"TimeSync": "The clock has not yet been synchronised. Without a correctly set clock, no requests are made to the inverter. This is normal shortly after the start. However, after a longer runtime (>1 minute), it indicates that the NTP server is not accessible.", "TimeSync": "The clock has not yet been synchronised. Without a correctly set clock, no requests are made to the inverter. This is normal shortly after the start. However, after a longer runtime (>1 minute), it indicates that the NTP server is not accessible.",
"TimeSyncLink": "Please check your time settings.", "TimeSyncLink": "Please check your time settings.",
"DefaultPassword": "You are using the default password for the web interface and the emergency access point. This is potentially insecure.", "DefaultPassword": "You are using the default password for the web interface and the emergency access point. This is potentially insecure.",
"DefaultPasswordLink": "Please change the password." "DefaultPasswordLink": "Please change the password.",
"PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (<code>pin_mapping.json</code>) or have not selected a profile defined there. Please refer to the <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> for details."
}, },
"deviceadmin": { "deviceadmin": {
"DeviceManager": "Device-Manager", "DeviceManager": "Device-Manager",

View File

@ -30,7 +30,9 @@
"Refreshing": "Refreshing", "Refreshing": "Refreshing",
"Pull": "Pull down to refresh", "Pull": "Pull down to refresh",
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Fermer" "Close": "Fermer",
"Yes": "Yes",
"No": "No"
}, },
"wait": { "wait": {
"NotReady": "OpenDTU is not yet ready", "NotReady": "OpenDTU is not yet ready",
@ -179,7 +181,10 @@
"FirmwareVersion": "Version du firmware", "FirmwareVersion": "Version du firmware",
"FirmwareBuildDate": "Date de création du firmware", "FirmwareBuildDate": "Date de création du firmware",
"HardwarePartNumber": "Numéro d'article matériel", "HardwarePartNumber": "Numéro d'article matériel",
"HardwareVersion": "Version du matériel" "HardwareVersion": "Version du matériel",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"Yes": "@:base.Yes",
"No": "@:base.No"
}, },
"gridprofile": { "gridprofile": {
"NoInfo": "@:devinfo.NoInfo", "NoInfo": "@:devinfo.NoInfo",
@ -609,7 +614,8 @@
"TimeSync": "L'horloge n'a pas encore été synchronisée. Sans une horloge correctement réglée, aucune demande n'est adressée à l'onduleur. Ceci est normal peu de temps après le démarrage. Cependant, après un temps de fonctionnement plus long (>1 minute), cela indique que le serveur NTP n'est pas accessible.", "TimeSync": "L'horloge n'a pas encore été synchronisée. Sans une horloge correctement réglée, aucune demande n'est adressée à l'onduleur. Ceci est normal peu de temps après le démarrage. Cependant, après un temps de fonctionnement plus long (>1 minute), cela indique que le serveur NTP n'est pas accessible.",
"TimeSyncLink": "Veuillez vérifier vos paramètres horaires.", "TimeSyncLink": "Veuillez vérifier vos paramètres horaires.",
"DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.", "DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.",
"DefaultPasswordLink": "Merci de changer le mot de passe." "DefaultPasswordLink": "Merci de changer le mot de passe.",
"PinMappingIssue": "Vous utilisez une image générique du micrologiciel, mais vous n'avez pas encore téléchargé un fichier contenant les profils de l'appareil (<code>pin_mapping.json</code>) ou vous n'avez pas sélectionné un profil défini dans ce fichier. Veuillez vous référer à la <a href=\"https://opendtu.solar/firmware/device_profiles/\" target=\"_blank\" class=\"alert-link\">documentation</a> pour plus de détails."
}, },
"deviceadmin": { "deviceadmin": {
"DeviceManager": "Gestionnaire de périphériques", "DeviceManager": "Gestionnaire de périphériques",

View File

@ -8,4 +8,5 @@ export interface DevInfoStatus {
hw_version: number; hw_version: number;
hw_model_name: string; hw_model_name: string;
max_power: number; max_power: number;
pdl_supported: boolean;
} }

View File

@ -58,6 +58,7 @@ export interface Hints {
time_sync: boolean; time_sync: boolean;
default_password: boolean; default_password: boolean;
radio_problem: boolean; radio_problem: boolean;
pin_mapping_issue: boolean;
} }
export interface LiveData { export interface LiveData {

View File

@ -31,12 +31,22 @@
> >
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="me-2"> <div class="me-2">
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" /> <span
<BIconExclamationCircleFill v-if="inverter.AC"
class="fs-4" class="badge"
v-if="inverter.reachable && !inverter.producing" :class="{
/> 'text-bg-secondary': !inverter.poll_enabled,
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" /> 'text-bg-danger': inverter.poll_enabled && !inverter.reachable,
'text-bg-warning':
inverter.poll_enabled && inverter.reachable && !inverter.producing,
'text-bg-success':
inverter.poll_enabled && inverter.reachable && inverter.producing,
}"
>
{{ $n(inverter.AC[0]?.Power?.v || 0, 'decimalNoDigits') }}
{{ inverter.AC[0].Power?.u }}
</span>
<span v-else class="badge text-bg-light">-</span>
</div> </div>
<div class="ms-auto me-auto"> <div class="ms-auto me-auto">
{{ inverter.name }} {{ inverter.name }}
@ -70,7 +80,7 @@
'text-bg-tertiary': !inverter.poll_enabled, 'text-bg-tertiary': !inverter.poll_enabled,
'text-bg-danger': inverter.poll_enabled && !inverter.reachable, 'text-bg-danger': inverter.poll_enabled && !inverter.reachable,
'text-bg-warning': inverter.poll_enabled && inverter.reachable && !inverter.producing, 'text-bg-warning': inverter.poll_enabled && inverter.reachable && !inverter.producing,
'text-bg-primary': inverter.poll_enabled && inverter.reachable && inverter.producing, 'text-bg-success': inverter.poll_enabled && inverter.reachable && inverter.producing,
}" }"
> >
<div class="p-1 flex-grow-1"> <div class="p-1 flex-grow-1">
@ -513,9 +523,7 @@ import * as bootstrap from 'bootstrap';
import { import {
BIconArrowCounterclockwise, BIconArrowCounterclockwise,
BIconBroadcast, BIconBroadcast,
BIconCheckCircleFill,
BIconCpu, BIconCpu,
BIconExclamationCircleFill,
BIconInfoCircle, BIconInfoCircle,
BIconJournalText, BIconJournalText,
BIconOutlet, BIconOutlet,
@ -523,7 +531,6 @@ import {
BIconSpeedometer, BIconSpeedometer,
BIconToggleOff, BIconToggleOff,
BIconToggleOn, BIconToggleOn,
BIconXCircleFill,
} from 'bootstrap-icons-vue'; } from 'bootstrap-icons-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -540,9 +547,7 @@ export default defineComponent({
ModalDialog, ModalDialog,
BIconArrowCounterclockwise, BIconArrowCounterclockwise,
BIconBroadcast, BIconBroadcast,
BIconCheckCircleFill,
BIconCpu, BIconCpu,
BIconExclamationCircleFill,
BIconInfoCircle, BIconInfoCircle,
BIconJournalText, BIconJournalText,
BIconOutlet, BIconOutlet,
@ -550,7 +555,6 @@ export default defineComponent({
BIconSpeedometer, BIconSpeedometer,
BIconToggleOff, BIconToggleOff,
BIconToggleOn, BIconToggleOn,
BIconXCircleFill,
}, },
data() { data() {
return { return {

View File

@ -383,8 +383,8 @@ export default defineComponent({
return { return {
modal: {} as bootstrap.Modal, modal: {} as bootstrap.Modal,
modalDelete: {} as bootstrap.Modal, modalDelete: {} as bootstrap.Modal,
newInverterData: {} as Inverter, newInverterData: { serial: '' } as Inverter,
selectedInverterData: {} as Inverter, selectedInverterData: { serial: '' } as Inverter,
inverters: [] as Inverter[], inverters: [] as Inverter[],
dataLoading: true, dataLoading: true,
alert: {} as AlertResponse, alert: {} as AlertResponse,
@ -440,7 +440,7 @@ export default defineComponent({
}, },
onSubmit() { onSubmit() {
this.callInverterApiEndpoint('add', JSON.stringify(this.newInverterData)); this.callInverterApiEndpoint('add', JSON.stringify(this.newInverterData));
this.newInverterData = {} as Inverter; this.newInverterData = { serial: '' } as Inverter;
}, },
onDelete() { onDelete() {
this.callInverterApiEndpoint('del', JSON.stringify({ id: this.selectedInverterData.id })); this.callInverterApiEndpoint('del', JSON.stringify({ id: this.selectedInverterData.id }));

File diff suppressed because it is too large Load Diff

Binary file not shown.