Merge branch 'development'

This commit is contained in:
helgeerbe 2023-03-27 10:59:51 +02:00
commit 7ba2058625
38 changed files with 1363 additions and 198 deletions

View File

@ -37,7 +37,7 @@ Like to show your own build? Just send me a Pull Request.
#### ####
Power Limiter States Power Limiter States
![PowerLimiterInverterStates](https://user-images.githubusercontent.com/59169507/224017473-9b3706f4-7c38-44f0-8890-7936fee67c7c.png) ![PowerLimiterInverterStates](https://github.com/helgeerbe/OpenDTU-OnBattery/blob/development/docs/PowerLimiterInverterStates.png)
### Web-Live-Interface: ### Web-Live-Interface:
![image](https://user-images.githubusercontent.com/59169507/187224107-4e0d0cab-2e1b-4e47-9410-a49f80aa6789.png) ![image](https://user-images.githubusercontent.com/59169507/187224107-4e0d0cab-2e1b-4e47-9410-a49f80aa6789.png)

View File

@ -1,6 +1,6 @@
<mxfile host="65bd71144e"> <mxfile host="65bd71144e">
<diagram name="Page-1" id="b5b7bab2-c9e2-2cf4-8b2a-24fd1a2a6d21"> <diagram name="Page-1" id="b5b7bab2-c9e2-2cf4-8b2a-24fd1a2a6d21">
<mxGraphModel dx="1539" dy="843" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="none" math="0" shadow="0"> <mxGraphModel dx="1370" dy="985" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="none" math="0" shadow="0">
<root> <root>
<mxCell id="0"/> <mxCell id="0"/>
<mxCell id="1" parent="0"/> <mxCell id="1" parent="0"/>
@ -18,7 +18,7 @@
<mxPoint x="205" y="370" as="targetPoint"/> <mxPoint x="205" y="370" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-6" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_OFF&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry / stop inverter, limit lower limit&lt;br&gt;do / nothing&lt;br&gt;exit / start inverter&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"> <mxCell id="sqCMRMHiXPc9LqBIY9SA-6" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_OFF&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry / stop inverter, limit lower limit&lt;br&gt;do / nothing&lt;br&gt;exit / nothing&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
<mxGeometry x="170" y="585" width="200" height="100" as="geometry"/> <mxGeometry x="170" y="585" width="200" height="100" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="sqCMRMHiXPc9LqBIY9SA-7" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_CONSUME_SOLAR_POWER_ONLY&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry /&lt;br&gt;do / setNewLimit&lt;br&gt;exit /&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1"> <mxCell id="sqCMRMHiXPc9LqBIY9SA-7" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;STATE_CONSUME_SOLAR_POWER_ONLY&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;/p&gt;&lt;p style=&quot;margin:0px;margin-left:8px;text-align:left;&quot;&gt;entry /&lt;br&gt;do / setNewLimit&lt;br&gt;exit /&lt;/p&gt;" style="shape=mxgraph.sysml.simpleState;html=1;overflow=fill;whiteSpace=wrap;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;" parent="1" vertex="1">
@ -119,32 +119,19 @@
<mxPoint x="530" y="760" as="targetPoint"/> <mxPoint x="530" y="760" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="24" value="isStartThresholdReached &amp;amp;&amp;amp; newPowerLimit &amp;gt; lowerPowerLimit" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1"> <mxCell id="41" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="24" target="25">
<mxGeometry x="595" y="930" width="130" height="130" as="geometry"/> <mxGeometry relative="1" as="geometry"/>
</mxCell> </mxCell>
<mxCell id="25" value="cnUseDirectSolarPower &amp;amp;&amp;amp; newPowerLimit &amp;gt; lowerPowerLimit" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1"> <mxCell id="42" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="41">
<mxGeometry x="595" y="745" width="130" height="130" as="geometry"/> <mxGeometry x="0.36" relative="1" as="geometry">
</mxCell>
<mxCell id="26" value="" style="endArrow=classic;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" parent="1" source="24" target="25" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="860" y="950" as="sourcePoint"/>
<mxPoint x="750" y="840" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="32" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="26" vertex="1" connectable="0">
<mxGeometry x="0.0933" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/> <mxPoint as="offset"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="28" value="yes" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.113;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="24" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1"> <mxCell id="24" value="canUseDirectSolarPower" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-0.2308" y="10" width="50" height="50" relative="1" as="geometry"> <mxGeometry x="600" y="930" width="130" height="130" as="geometry"/>
<mxPoint x="620" y="510" as="sourcePoint"/> </mxCell>
<mxPoint x="670" y="460" as="targetPoint"/> <mxCell id="25" value="isStartThresholdReached" style="rhombus;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<Array as="points"> <mxGeometry x="830" y="930" width="130" height="130" as="geometry"/>
<mxPoint x="993" y="1000"/>
</Array>
<mxPoint x="1" as="offset"/>
</mxGeometry>
</mxCell> </mxCell>
<mxCell id="29" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="5" edge="1"> <mxCell id="29" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21" target="5" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry"> <mxGeometry width="50" height="50" relative="1" as="geometry">
@ -157,43 +144,18 @@
<mxPoint x="3" y="-15" as="offset"/> <mxPoint x="3" y="-15" as="offset"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="33" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="25" target="sqCMRMHiXPc9LqBIY9SA-7" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="620" y="480" as="sourcePoint"/>
<mxPoint x="670" y="430" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="34" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="33" vertex="1" connectable="0">
<mxGeometry x="0.424" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="35" value="" style="endArrow=classic;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="25" target="38" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="390" y="910" as="sourcePoint"/>
<mxPoint x="440" y="860" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="36" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="35" vertex="1" connectable="0">
<mxGeometry x="0.4667" relative="1" as="geometry">
<mxPoint x="-19" y="-5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="38" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" parent="1" vertex="1">
<mxGeometry x="795" y="795" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-38" value="" style="endArrow=classic;html=1;rounded=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1"> <mxCell id="JKiNQljIdbqBsyxyxwz1-38" value="" style="endArrow=classic;html=1;rounded=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-6" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry"> <mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="820" as="sourcePoint"/> <mxPoint x="570" y="820" as="sourcePoint"/>
<mxPoint x="620" y="770" as="targetPoint"/> <mxPoint x="620" y="770" as="targetPoint"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-39" value="!Inverter-&amp;gt;isProducing ||&lt;br&gt;isStopThresholdReached ||&lt;br&gt;newLimit &amp;lt; lowerLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-38" vertex="1" connectable="0"> <mxCell id="JKiNQljIdbqBsyxyxwz1-39" value="isStopThresholdReached ||&lt;br&gt;(!canUseDirectSolarPower &lt;br&gt;&amp;amp;&amp;amp; EMPTY_WHEN_FULL)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-38" vertex="1" connectable="0">
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry"> <mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
<mxPoint x="-14" y="-33" as="offset"/> <mxPoint x="-14" y="-33" as="offset"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-40" value="!canUseDirectSolarPower ||&lt;br&gt;isStartThresholdReached" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1"> <mxCell id="JKiNQljIdbqBsyxyxwz1-40" value="isStartThresholdReached ||&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;(!canUseDirectSolarPower&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;&amp;amp;&amp;amp; EMPTY_AT_NIGHT)" style="endArrow=classic;html=1;rounded=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="sqCMRMHiXPc9LqBIY9SA-7" target="sqCMRMHiXPc9LqBIY9SA-8" edge="1">
<mxGeometry x="-0.0286" y="25" width="50" height="50" relative="1" as="geometry"> <mxGeometry x="-0.0286" y="25" width="50" height="50" relative="1" as="geometry">
<mxPoint x="570" y="560" as="sourcePoint"/> <mxPoint x="570" y="560" as="sourcePoint"/>
<mxPoint x="620" y="510" as="targetPoint"/> <mxPoint x="620" y="510" as="targetPoint"/>
@ -210,7 +172,7 @@
</Array> </Array>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="JKiNQljIdbqBsyxyxwz1-42" value="!Inverter-&amp;gt;isProducing ||&lt;br&gt;isStopThresholdReached ||&lt;br&gt;newLimit &amp;lt; lowerLimit" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-41" vertex="1" connectable="0"> <mxCell id="JKiNQljIdbqBsyxyxwz1-42" value="isStopThresholdReached ||" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="JKiNQljIdbqBsyxyxwz1-41" vertex="1" connectable="0">
<mxGeometry x="-0.2129" y="-2" relative="1" as="geometry"> <mxGeometry x="-0.2129" y="-2" relative="1" as="geometry">
<mxPoint x="-151" y="32" as="offset"/> <mxPoint x="-151" y="32" as="offset"/>
</mxGeometry> </mxGeometry>
@ -257,6 +219,56 @@
<mxPoint x="16" y="33" as="offset"/> <mxPoint x="16" y="33" as="offset"/>
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="38" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="24" target="sqCMRMHiXPc9LqBIY9SA-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="710" as="sourcePoint"/>
<mxPoint x="630" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="39" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="38">
<mxGeometry x="0.104" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="40" value="" style="endArrow=classic;html=1;entryX=1;entryY=0.75;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="sqCMRMHiXPc9LqBIY9SA-7">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="970" y="658" as="sourcePoint"/>
<mxPoint x="800" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="48" value="canUseDirectSolarPower&lt;br style=&quot;border-color: var(--border-color);&quot;&gt;&amp;amp;&amp;amp; EMPTY_AT_NIGHT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="40">
<mxGeometry x="-0.04" y="1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="43" value="" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="25" target="sqCMRMHiXPc9LqBIY9SA-8">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="960" y="980" as="sourcePoint"/>
<mxPoint x="1010" y="930" as="targetPoint"/>
<Array as="points">
<mxPoint x="1020" y="995"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="44" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="43">
<mxGeometry x="0.1135" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="45" value="" style="shape=ellipse;html=1;dashed=0;whitespace=wrap;aspect=fixed;strokeWidth=5;perimeter=ellipsePerimeter;" vertex="1" parent="1">
<mxGeometry x="880" y="850" width="30" height="30" as="geometry"/>
</mxCell>
<mxCell id="46" value="" style="endArrow=classic;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="25" target="45">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="710" as="sourcePoint"/>
<mxPoint x="630" y="660" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="47" value="no" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="46">
<mxGeometry x="-0.08" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
</root> </root>
</mxGraphModel> </mxGraphModel>
</diagram> </diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View File

@ -99,13 +99,20 @@ struct CONFIG_T {
bool Mqtt_Hass_Expire; bool Mqtt_Hass_Expire;
bool PowerMeter_Enabled;
uint32_t PowerMeter_Interval;
uint32_t PowerMeter_Source;
char PowerMeter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1];
char PowerMeter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
char PowerMeter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
uint32_t PowerMeter_SdmBaudrate;
uint32_t PowerMeter_SdmAddress;
bool PowerLimiter_Enabled; bool PowerLimiter_Enabled;
bool PowerLimiter_SolarPassTroughEnabled; bool PowerLimiter_SolarPassTroughEnabled;
uint8_t PowerLimiter_BatteryDrainStategy; uint8_t PowerLimiter_BatteryDrainStategy;
uint32_t PowerLimiter_Interval; uint32_t PowerLimiter_Interval;
char PowerLimiter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1];
char PowerLimiter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
char PowerLimiter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
bool PowerLimiter_IsInverterBehindPowerMeter; bool PowerLimiter_IsInverterBehindPowerMeter;
uint8_t PowerLimiter_InverterId; uint8_t PowerLimiter_InverterId;
uint8_t PowerLimiter_InverterChannelId; uint8_t PowerLimiter_InverterChannelId;

View File

@ -26,12 +26,10 @@ public:
void loop(); void loop();
plStates getPowerLimiterState(); plStates getPowerLimiterState();
int32_t getLastRequestedPowewrLimit(); int32_t getLastRequestedPowewrLimit();
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
private: private:
uint32_t _lastCommandSent; uint32_t _lastCommandSent;
uint32_t _lastLoop; uint32_t _lastLoop;
uint32_t _lastPowerMeterUpdate;
int32_t _lastRequestedPowerLimit; int32_t _lastRequestedPowerLimit;
plStates _plState = STATE_DISCOVER; plStates _plState = STATE_DISCOVER;

45
include/PowerMeter.h Normal file
View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>
#include "SDM.h"
#ifndef SDM_RX_PIN
#define SDM_RX_PIN 13
#endif
#ifndef SDM_TX_PIN
#define SDM_TX_PIN 32
#endif
class PowerMeterClass {
public:
void init();
void mqtt();
void loop();
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
float getPowerTotal();
uint32_t getLastPowerMeterUpdate();
private:
uint32_t _interval;
uint32_t _lastPowerMeterUpdate;
float _powerMeter1Power = 0.0;
float _powerMeter2Power = 0.0;
float _powerMeter3Power = 0.0;
float _powerMeterTotalPower = 0.0;
float _powerMeter1Voltage = 0.0;
float _powerMeter2Voltage = 0.0;
float _powerMeter3Voltage = 0.0;
float _PowerMeterImport = 0.0;
float _PowerMeterExport = 0.0;
bool mqttInitDone = false;
};
extern PowerMeterClass PowerMeter;

View File

@ -15,6 +15,7 @@
#include "WebApi_network.h" #include "WebApi_network.h"
#include "WebApi_ntp.h" #include "WebApi_ntp.h"
#include "WebApi_power.h" #include "WebApi_power.h"
#include "WebApi_powermeter.h"
#include "WebApi_powerlimiter.h" #include "WebApi_powerlimiter.h"
#include "WebApi_prometheus.h" #include "WebApi_prometheus.h"
#include "WebApi_security.h" #include "WebApi_security.h"
@ -55,6 +56,7 @@ private:
WebApiNetworkClass _webApiNetwork; WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp; WebApiNtpClass _webApiNtp;
WebApiPowerClass _webApiPower; WebApiPowerClass _webApiPower;
WebApiPowerMeterClass _webApiPowerMeter;
WebApiPowerLimiterClass _webApiPowerLimiter; WebApiPowerLimiterClass _webApiPowerLimiter;
WebApiPrometheusClass _webApiPrometheus; WebApiPrometheusClass _webApiPrometheus;
WebApiSecurityClass _webApiSecurity; WebApiSecurityClass _webApiSecurity;

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiPowerMeterClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -92,6 +92,13 @@
#define VEDIRECT_UPDATESONLY true #define VEDIRECT_UPDATESONLY true
#define VEDIRECT_POLL_INTERVAL 5 #define VEDIRECT_POLL_INTERVAL 5
#define POWERMETER_ENABLED false
#define POWERMETER_INTERVAL 10
#define POWERMETER_SOURCE 2
#define POWERMETER_SDMBAUDRATE 9600
#define POWERMETER_SDMADDRESS 1
#define POWERLIMITER_ENABLED false #define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTROUGH_ENABLED true
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 #define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0

View File

@ -170,8 +170,8 @@ uint8_t DevInfoParser::getDevIdx()
/* struct tm to seconds since Unix epoch */ /* struct tm to seconds since Unix epoch */
time_t DevInfoParser::timegm(struct tm* t) time_t DevInfoParser::timegm(struct tm* t)
{ {
register uint32_t year; uint32_t year;
register time_t result; time_t result;
#define MONTHSPERYEAR 12 /* months per calendar year */ #define MONTHSPERYEAR 12 /* months per calendar year */
static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };

254
lib/SdmEnergyMeter/SDM.cpp Normal file
View File

@ -0,0 +1,254 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
//------------------------------------------------------------------------------
#include "SDM.h"
//------------------------------------------------------------------------------
#if defined ( USE_HARDWARESERIAL )
#if defined ( ESP8266 )
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, bool swapuart) : sdmSer(serial) {
this->_baud = baud;
this->_dere_pin = dere_pin;
this->_config = config;
this->_swapuart = swapuart;
}
#elif defined ( ESP32 )
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) {
this->_baud = baud;
this->_dere_pin = dere_pin;
this->_config = config;
this->_rx_pin = rx_pin;
this->_tx_pin = tx_pin;
}
#else
SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config) : sdmSer(serial) {
this->_baud = baud;
this->_dere_pin = dere_pin;
this->_config = config;
}
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) {
this->_baud = baud;
this->_dere_pin = dere_pin;
this->_config = config;
this->_rx_pin = rx_pin;
this->_tx_pin = tx_pin;
}
#else
SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin) : sdmSer(serial) {
this->_baud = baud;
this->_dere_pin = dere_pin;
}
#endif
#endif
SDM::~SDM() {
}
void SDM::begin(void) {
#if defined ( USE_HARDWARESERIAL )
#if defined ( ESP8266 )
sdmSer.begin(_baud, (SerialConfig)_config);
#elif defined ( ESP32 )
sdmSer.begin(_baud, _config, _rx_pin, _tx_pin);
#else
sdmSer.begin(_baud, _config);
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
sdmSer.begin(_baud, (SoftwareSerialConfig)_config, _rx_pin, _tx_pin);
#else
sdmSer.begin(_baud);
#endif
#endif
#if defined ( USE_HARDWARESERIAL ) && defined ( ESP8266 )
if (_swapuart)
sdmSer.swap();
#endif
if (_dere_pin != NOT_A_PIN) {
pinMode(_dere_pin, OUTPUT); //set output pin mode for DE/RE pin when used (for control MAX485)
}
dereSet(LOW); //set init state to receive from SDM -> DE Disable, /RE Enable (for control MAX485)
}
float SDM::readVal(uint16_t reg, uint8_t node) {
uint16_t temp;
unsigned long resptime;
uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0};
float res = NAN;
uint16_t readErr = SDM_ERR_NO_ERROR;
sdmarr[2] = highByte(reg);
sdmarr[3] = lowByte(reg);
temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes
sdmarr[6] = lowByte(temp);
sdmarr[7] = highByte(temp);
#if !defined ( USE_HARDWARESERIAL )
sdmSer.listen(); //enable softserial rx interrupt
#endif
flush(); //read serial if any old data is available
dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485)
delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524
sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes
sdmSer.flush(); //clear out tx buffer
dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
resptime = millis();
while (sdmSer.available() < FRAMESIZE) {
if (millis() - resptime > msturnaround) {
readErr = SDM_ERR_TIMEOUT; //err debug (4)
break;
}
yield();
}
if (readErr == SDM_ERR_NO_ERROR) { //if no timeout...
if (sdmSer.available() >= FRAMESIZE) {
for(int n=0; n<FRAMESIZE; n++) {
sdmarr[n] = sdmSer.read();
}
if (sdmarr[0] == node && sdmarr[1] == SDM_B_02 && sdmarr[2] == SDM_REPLY_BYTE_COUNT) {
if ((calculateCRC(sdmarr, FRAMESIZE - 2)) == ((sdmarr[8] << 8) | sdmarr[7])) { //calculate crc from first 7 bytes and compare with received crc (bytes 7 & 8)
((uint8_t*)&res)[3]= sdmarr[3];
((uint8_t*)&res)[2]= sdmarr[4];
((uint8_t*)&res)[1]= sdmarr[5];
((uint8_t*)&res)[0]= sdmarr[6];
} else {
readErr = SDM_ERR_CRC_ERROR; //err debug (1)
}
} else {
readErr = SDM_ERR_WRONG_BYTES; //err debug (2)
}
} else {
readErr = SDM_ERR_NOT_ENOUGHT_BYTES; //err debug (3)
}
}
flush(mstimeout); //read serial if any old data is available and wait for RESPONSE_TIMEOUT (in ms)
if (sdmSer.available()) //if serial rx buffer (after RESPONSE_TIMEOUT) still contains data then something spam rs485, check node(s) or increase RESPONSE_TIMEOUT
readErr = SDM_ERR_TIMEOUT; //err debug (4) but returned value may be correct
if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter
readingerrcode = readErr;
readingerrcount++;
} else {
++readingsuccesscount;
}
#if !defined ( USE_HARDWARESERIAL )
sdmSer.stopListening(); //disable softserial rx interrupt
#endif
return (res);
}
uint16_t SDM::getErrCode(bool _clear) {
uint16_t _tmp = readingerrcode;
if (_clear == true)
clearErrCode();
return (_tmp);
}
uint32_t SDM::getErrCount(bool _clear) {
uint32_t _tmp = readingerrcount;
if (_clear == true)
clearErrCount();
return (_tmp);
}
uint32_t SDM::getSuccCount(bool _clear) {
uint32_t _tmp = readingsuccesscount;
if (_clear == true)
clearSuccCount();
return (_tmp);
}
void SDM::clearErrCode() {
readingerrcode = SDM_ERR_NO_ERROR;
}
void SDM::clearErrCount() {
readingerrcount = 0;
}
void SDM::clearSuccCount() {
readingsuccesscount = 0;
}
void SDM::setMsTurnaround(uint16_t _msturnaround) {
if (_msturnaround < SDM_MIN_DELAY)
msturnaround = SDM_MIN_DELAY;
else if (_msturnaround > SDM_MAX_DELAY)
msturnaround = SDM_MAX_DELAY;
else
msturnaround = _msturnaround;
}
void SDM::setMsTimeout(uint16_t _mstimeout) {
if (_mstimeout < SDM_MIN_DELAY)
mstimeout = SDM_MIN_DELAY;
else if (_mstimeout > SDM_MAX_DELAY)
mstimeout = SDM_MAX_DELAY;
else
mstimeout = _mstimeout;
}
uint16_t SDM::getMsTurnaround() {
return (msturnaround);
}
uint16_t SDM::getMsTimeout() {
return (mstimeout);
}
uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) {
uint16_t _crc, _flag;
_crc = 0xFFFF;
for (uint8_t i = 0; i < len; i++) {
_crc ^= (uint16_t)array[i];
for (uint8_t j = 8; j; j--) {
_flag = _crc & 0x0001;
_crc >>= 1;
if (_flag)
_crc ^= 0xA001;
}
}
return _crc;
}
void SDM::flush(unsigned long _flushtime) {
unsigned long flushstart = millis();
while (sdmSer.available() || (millis() - flushstart < _flushtime)) {
if (sdmSer.available()) //read serial if any old data is available
sdmSer.read();
delay(1);
}
}
void SDM::dereSet(bool _state) {
if (_dere_pin != NOT_A_PIN)
digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
}

299
lib/SdmEnergyMeter/SDM.h Normal file
View File

@ -0,0 +1,299 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
//------------------------------------------------------------------------------
#ifndef SDM_h
#define SDM_h
//------------------------------------------------------------------------------
#include <Arduino.h>
#include <SDM_Config_User.h>
#if defined ( USE_HARDWARESERIAL )
#include <HardwareSerial.h>
#else
#include <SoftwareSerial.h>
#endif
//------------------------------------------------------------------------------
//DEFAULT CONFIG (DO NOT CHANGE ANYTHING!!! for changes use SDM_Config_User.h):
//------------------------------------------------------------------------------
#if !defined ( SDM_UART_BAUD )
#define SDM_UART_BAUD 4800 // default baudrate
#endif
#if !defined ( DERE_PIN )
#define DERE_PIN NOT_A_PIN // default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin)
#endif
#if defined ( USE_HARDWARESERIAL )
#if !defined ( SDM_UART_CONFIG )
#define SDM_UART_CONFIG SERIAL_8N1 // default hardware uart config
#endif
#if defined ( ESP8266 ) && !defined ( SWAPHWSERIAL )
#define SWAPHWSERIAL 0 // (only esp8266) when hwserial used, then swap uart pins from 3/1 to 13/15 (default not swap)
#endif
#if defined ( ESP32 )
#if !defined ( SDM_RX_PIN )
#define SDM_RX_PIN -1 // use default rx pin for selected port
#endif
#if !defined ( SDM_TX_PIN )
#define SDM_TX_PIN -1 // use default tx pin for selected port
#endif
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
#if !defined ( SDM_UART_CONFIG )
#define SDM_UART_CONFIG SWSERIAL_8N1 // default softwareware uart config for esp8266/esp32
#endif
#endif
// #if !defined ( SDM_RX_PIN ) || !defined ( SDM_TX_PIN )
// #error "SDM_RX_PIN and SDM_TX_PIN must be defined in SDM_Config_User.h for Software Serial option)"
// #endif
#if !defined ( SDM_RX_PIN )
#define SDM_RX_PIN -1
#endif
#if !defined ( SDM_TX_PIN )
#define SDM_TX_PIN -1
#endif
#endif
#if !defined ( WAITING_TURNAROUND_DELAY )
#define WAITING_TURNAROUND_DELAY 200 // time in ms to wait for process current request
#endif
#if !defined ( RESPONSE_TIMEOUT )
#define RESPONSE_TIMEOUT 500 // time in ms to wait for return response from all devices before next request
#endif
#if !defined ( SDM_MIN_DELAY )
#define SDM_MIN_DELAY 20 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#endif
#if !defined ( SDM_MAX_DELAY )
#define SDM_MAX_DELAY 5000 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT
#endif
//------------------------------------------------------------------------------
#define SDM_ERR_NO_ERROR 0 // no error
#define SDM_ERR_CRC_ERROR 1 // crc error
#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong
#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm
#define SDM_ERR_TIMEOUT 4 // timeout
//------------------------------------------------------------------------------
#define FRAMESIZE 9 // size of out/in array
#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data
#define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1)
#define SDM_B_02 0x04 // BYTE 2 -> function code (default value 0x04 read from 3X input registers)
#define SDM_B_05 0x00 // BYTE 5
#define SDM_B_06 0x02 // BYTE 6
// BYTES 3 & 4 (BELOW)
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
// REGISTERS LIST FOR SDM DEVICES |
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
// REGISTER NAME REGISTER ADDRESS UNIT | SDM630 | SDM230 | SDM220 | SDM120CT| SDM120 | SDM72D | SDM72 V2|
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
#define SDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_VOLTAGE 0x0002 // V | 1 | | | | | | 1 |
#define SDM_PHASE_3_VOLTAGE 0x0004 // V | 1 | | | | | | 1 |
#define SDM_PHASE_1_CURRENT 0x0006 // A | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_CURRENT 0x0008 // A | 1 | | | | | | 1 |
#define SDM_PHASE_3_CURRENT 0x000A // A | 1 | | | | | | 1 |
#define SDM_PHASE_1_POWER 0x000C // W | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_POWER 0x000E // W | 1 | | | | | | 1 |
#define SDM_PHASE_3_POWER 0x0010 // W | 1 | | | | | | 1 |
#define SDM_PHASE_1_APPARENT_POWER 0x0012 // VA | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_APPARENT_POWER 0x0014 // VA | 1 | | | | | | 1 |
#define SDM_PHASE_3_APPARENT_POWER 0x0016 // VA | 1 | | | | | | 1 |
#define SDM_PHASE_1_REACTIVE_POWER 0x0018 // VAr | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_REACTIVE_POWER 0x001A // VAr | 1 | | | | | | 1 |
#define SDM_PHASE_3_REACTIVE_POWER 0x001C // VAr | 1 | | | | | | 1 |
#define SDM_PHASE_1_POWER_FACTOR 0x001E // | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_PHASE_2_POWER_FACTOR 0x0020 // | 1 | | | | | | 1 |
#define SDM_PHASE_3_POWER_FACTOR 0x0022 // | 1 | | | | | | 1 |
#define SDM_PHASE_1_ANGLE 0x0024 // Degrees | 1 | 1 | 1 | 1 | | | |
#define SDM_PHASE_2_ANGLE 0x0026 // Degrees | 1 | | | | | | |
#define SDM_PHASE_3_ANGLE 0x0028 // Degrees | 1 | | | | | | |
#define SDM_AVERAGE_L_TO_N_VOLTS 0x002A // V | 1 | | | | | | 1 |
#define SDM_AVERAGE_LINE_CURRENT 0x002E // A | 1 | | | | | | 1 |
#define SDM_SUM_LINE_CURRENT 0x0030 // A | 1 | | | | | | 1 |
#define SDM_TOTAL_SYSTEM_POWER 0x0034 // W | 1 | | | | | 1 | 1 |
#define SDM_TOTAL_SYSTEM_APPARENT_POWER 0x0038 // VA | 1 | | | | | | 1 |
#define SDM_TOTAL_SYSTEM_REACTIVE_POWER 0x003C // VAr | 1 | | | | | | 1 |
#define SDM_TOTAL_SYSTEM_POWER_FACTOR 0x003E // | 1 | | | | | | 1 |
#define SDM_TOTAL_SYSTEM_PHASE_ANGLE 0x0042 // Degrees | 1 | | | | | | |
#define SDM_FREQUENCY 0x0046 // Hz | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_IMPORT_ACTIVE_ENERGY 0x0048 // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
#define SDM_EXPORT_ACTIVE_ENERGY 0x004A // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
#define SDM_IMPORT_REACTIVE_ENERGY 0x004C // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | |
#define SDM_EXPORT_REACTIVE_ENERGY 0x004E // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | |
#define SDM_VAH_SINCE_LAST_RESET 0x0050 // kVAh/MVAh | 1 | | | | | | |
#define SDM_AH_SINCE_LAST_RESET 0x0052 // Ah/kAh | 1 | | | | | | |
#define SDM_TOTAL_SYSTEM_POWER_DEMAND 0x0054 // W | 1 | 1 | | | | | |
#define SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND 0x0056 // W | 1 | 1 | | | | | |
#define SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND 0x0058 // W | | 1 | | | | | |
#define SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND 0x005A // W | | 1 | | | | | |
#define SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND 0x005C // W | | 1 | | | | | |
#define SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND 0x005E // W | | 1 | | | | | |
#define SDM_TOTAL_SYSTEM_VA_DEMAND 0x0064 // VA | 1 | | | | | | |
#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | |
#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | |
#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | |
#define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 |
#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 |
#define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 |
#define SDM_AVERAGE_LINE_TO_LINE_VOLTS 0x00CE // V | 1 | | | | | | 1 |
#define SDM_NEUTRAL_CURRENT 0x00E0 // A | 1 | | | | | | 1 |
#define SDM_PHASE_1_LN_VOLTS_THD 0x00EA // % | 1 | | | | | | |
#define SDM_PHASE_2_LN_VOLTS_THD 0x00EC // % | 1 | | | | | | |
#define SDM_PHASE_3_LN_VOLTS_THD 0x00EE // % | 1 | | | | | | |
#define SDM_PHASE_1_CURRENT_THD 0x00F0 // % | 1 | | | | | | |
#define SDM_PHASE_2_CURRENT_THD 0x00F2 // % | 1 | | | | | | |
#define SDM_PHASE_3_CURRENT_THD 0x00F4 // % | 1 | | | | | | |
#define SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD 0x00F8 // % | 1 | | | | | | |
#define SDM_AVERAGE_LINE_CURRENT_THD 0x00FA // % | 1 | | | | | | |
#define SDM_TOTAL_SYSTEM_POWER_FACTOR_INV 0x00FE // | 1 | | | | | | |
#define SDM_PHASE_1_CURRENT_DEMAND 0x0102 // A | 1 | 1 | | | | | |
#define SDM_PHASE_2_CURRENT_DEMAND 0x0104 // A | 1 | | | | | | |
#define SDM_PHASE_3_CURRENT_DEMAND 0x0106 // A | 1 | | | | | | |
#define SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND 0x0108 // A | 1 | 1 | | | | | |
#define SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND 0x010A // A | 1 | | | | | | |
#define SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND 0x010C // A | 1 | | | | | | |
#define SDM_LINE_1_TO_LINE_2_VOLTS_THD 0x014E // % | 1 | | | | | | |
#define SDM_LINE_2_TO_LINE_3_VOLTS_THD 0x0150 // % | 1 | | | | | | |
#define SDM_LINE_3_TO_LINE_1_VOLTS_THD 0x0152 // % | 1 | | | | | | |
#define SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD 0x0154 // % | 1 | | | | | | |
#define SDM_TOTAL_ACTIVE_ENERGY 0x0156 // kWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
#define SDM_TOTAL_REACTIVE_ENERGY 0x0158 // kVArh | 1 | 1 | 1 | 1 | 1 | | 1 |
#define SDM_L1_IMPORT_ACTIVE_ENERGY 0x015A // kWh | 1 | | | | | | |
#define SDM_L2_IMPORT_ACTIVE_ENERGY 0x015C // kWh | 1 | | | | | | |
#define SDM_L3_IMPORT_ACTIVE_ENERGY 0x015E // kWh | 1 | | | | | | |
#define SDM_L1_EXPORT_ACTIVE_ENERGY 0x0160 // kWh | 1 | | | | | | |
#define SDM_L2_EXPORT_ACTIVE_ENERGY 0x0162 // kWh | 1 | | | | | | |
#define SDM_L3_EXPORT_ACTIVE_ENERGY 0x0164 // kWh | 1 | | | | | | |
#define SDM_L1_TOTAL_ACTIVE_ENERGY 0x0166 // kWh | 1 | | | | | | |
#define SDM_L2_TOTAL_ACTIVE_ENERGY 0x0168 // kWh | 1 | | | | | | |
#define SDM_L3_TOTAL_ACTIVE_ENERGY 0x016a // kWh | 1 | | | | | | |
#define SDM_L1_IMPORT_REACTIVE_ENERGY 0x016C // kVArh | 1 | | | | | | |
#define SDM_L2_IMPORT_REACTIVE_ENERGY 0x016E // kVArh | 1 | | | | | | |
#define SDM_L3_IMPORT_REACTIVE_ENERGY 0x0170 // kVArh | 1 | | | | | | |
#define SDM_L1_EXPORT_REACTIVE_ENERGY 0x0172 // kVArh | 1 | | | | | | |
#define SDM_L2_EXPORT_REACTIVE_ENERGY 0x0174 // kVArh | 1 | | | | | | |
#define SDM_L3_EXPORT_REACTIVE_ENERGY 0x0176 // kVArh | 1 | | | | | | |
#define SDM_L1_TOTAL_REACTIVE_ENERGY 0x0178 // kVArh | 1 | | | | | | |
#define SDM_L2_TOTAL_REACTIVE_ENERGY 0x017A // kVArh | 1 | | | | | | |
#define SDM_L3_TOTAL_REACTIVE_ENERGY 0x017C // kVArh | 1 | | | | | | |
#define SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY 0x0180 // kWh | | 1 | | | | 1 | 1 |
#define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | |
#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 |
#define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 |
#define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 |
#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 |
#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 |
//---------------------------------------------------------------------------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------
// REGISTERS LIST FOR DDM DEVICE |
//---------------------------------------------------------------------------------------------------------
// REGISTER NAME REGISTER ADDRESS UNIT | DDM18SD |
//---------------------------------------------------------------------------------------------------------
#define DDM_PHASE_1_VOLTAGE 0x0000 // V | 1 |
#define DDM_PHASE_1_CURRENT 0x0008 // A | 1 |
#define DDM_PHASE_1_POWER 0x0012 // W | 1 |
#define DDM_PHASE_1_REACTIVE_POWER 0x001A // VAr | 1 |
#define DDM_PHASE_1_POWER_FACTOR 0x002A // | 1 |
#define DDM_FREQUENCY 0x0036 // Hz | 1 |
#define DDM_IMPORT_ACTIVE_ENERGY 0x0100 // kWh | 1 |
#define DDM_IMPORT_REACTIVE_ENERGY 0x0400 // kVArh | 1 |
//---------------------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------------------
// REGISTERS LIST FOR DEVNAME DEVICE |
//---------------------------------------------------------------------------------------------------------
// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME |
//---------------------------------------------------------------------------------------------------------
//#define DEVNAME_VOLTAGE 0x0000 // V | 1 |
//#define DEVNAME_CURRENT 0x0002 // A | 1 |
//#define DEVNAME_POWER 0x0004 // W | 1 |
//---------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------
class SDM {
public:
#if defined ( USE_HARDWARESERIAL ) // hardware serial
#if defined ( ESP8266 ) // on esp8266
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, bool swapuart = SWAPHWSERIAL);
#elif defined ( ESP32 ) // on esp32
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN);
#else // on avr
SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG);
#endif
#else // software serial
#if defined ( ESP8266 ) || defined ( ESP32 ) // on esp8266/esp32
SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN);
#else // on avr
SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN);
#endif
#endif
virtual ~SDM();
void begin(void);
float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node
uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase)
uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase)
uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false)
void clearErrCode(); // clear last errorcode
void clearErrCount(); // clear total errors count
void clearSuccCount(); // clear total success count
void setMsTurnaround(uint16_t _msturnaround = WAITING_TURNAROUND_DELAY); // set new value for WAITING_TURNAROUND_DELAY (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY
void setMsTimeout(uint16_t _mstimeout = RESPONSE_TIMEOUT); // set new value for RESPONSE_TIMEOUT (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY
uint16_t getMsTurnaround(); // get current value of WAITING_TURNAROUND_DELAY (ms)
uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms)
private:
#if defined ( USE_HARDWARESERIAL )
HardwareSerial& sdmSer;
#else
SoftwareSerial& sdmSer;
#endif
#if defined ( USE_HARDWARESERIAL )
int _config = SDM_UART_CONFIG;
#if defined ( ESP8266 )
bool _swapuart = SWAPHWSERIAL;
#elif defined ( ESP32 )
int8_t _rx_pin = -1;
int8_t _tx_pin = -1;
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
int _config = SDM_UART_CONFIG;
#endif
int8_t _rx_pin = -1;
int8_t _tx_pin = -1;
#endif
long _baud = SDM_UART_BAUD;
int _dere_pin = DERE_PIN;
uint16_t readingerrcode = SDM_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error
uint16_t msturnaround = WAITING_TURNAROUND_DELAY;
uint16_t mstimeout = RESPONSE_TIMEOUT;
uint32_t readingerrcount = 0; // total errors counter
uint32_t readingsuccesscount = 0; // total success counter
uint16_t calculateCRC(uint8_t *array, uint8_t len);
void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms
void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM
};
#endif // SDM_h

View File

@ -0,0 +1,93 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter
* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core)
* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/
/*
* USER CONFIG:
*/
//------------------------------------------------------------------------------
/*
* define or undefine USE_HARDWARESERIAL (uncomment only one or none)
*/
//#undef USE_HARDWARESERIAL
#define USE_HARDWARESERIAL
//------------------------------------------------------------------------------
/*
* define user baudrate
*/
#define SDM_UART_BAUD 9600
//------------------------------------------------------------------------------
/*
* define user SDM_RX_PIN and SDM_TX_PIN for esp/avr Software Serial option
* or ESP32 with Hardware Serial if default core pins are not suitable
*/
#if defined ( USE_HARDWARESERIAL )
#if defined ( ESP32 )
#define SDM_RX_PIN 13
#define SDM_TX_PIN 32
#endif
#else
#if defined ( ESP8266 ) || defined ( ESP32 )
#define SDM_RX_PIN 13
#define SDM_TX_PIN 15
#else
#define SDM_RX_PIN 10
#define SDM_TX_PIN 11
#endif
#endif
//------------------------------------------------------------------------------
/*
* define user DERE_PIN for control MAX485 DE/RE lines (connect DE & /RE together to this pin)
*/
//#define DERE_PIN NOT_A_PIN
//------------------------------------------------------------------------------
#if defined ( USE_HARDWARESERIAL )
/*
* define user SDM_UART_CONFIG for hardware serial
*/
//#define SDM_UART_CONFIG SERIAL_8N1
//----------------------------------------------------------------------------
/*
* define user SWAPHWSERIAL, if true(1) then swap uart pins from 3/1 to 13/15 (only ESP8266)
*/
//#define SWAPHWSERIAL 0
#else
/*
* define user SDM_UART_CONFIG for software serial
*/
//#define SDM_UART_CONFIG SWSERIAL_8N1
#endif
//------------------------------------------------------------------------------
/*
* define user WAITING_TURNAROUND_DELAY time in ms to wait for process current request
*/
//#define WAITING_TURNAROUND_DELAY 200
//------------------------------------------------------------------------------
/*
* define user RESPONSE_TIMEOUT time in ms to wait for return response from all devices before next request
*/
//#define RESPONSE_TIMEOUT 500
//------------------------------------------------------------------------------

View File

@ -20,13 +20,17 @@ platform = espressif32@>=6.0.1
build_flags = build_flags =
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz -DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
-Wall -Wextra -Werror -Wall -Wextra -Werror
-std=c++17
-std=gnu++17
build_unflags =
-std=gnu++11
lib_deps = lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.21.0 bblanchon/ArduinoJson @ ^6.21.0
https://github.com/bertmelis/espMqttClient.git#v1.4.1 https://github.com/bertmelis/espMqttClient.git#v1.4.1
nrf24/RF24 @ ^1.4.5 nrf24/RF24 @ ^1.4.5
olikraus/U8g2 @ ^2.34.13 olikraus/U8g2 @ ^2.34.16
buelowp/sunset @ ^1.1.7 buelowp/sunset @ ^1.1.7
extra_scripts = extra_scripts =

View File

@ -18,7 +18,6 @@
;upload_port = COM4 ;upload_port = COM4
; you can define your personal board and/or settings here ; you can define your personal board and/or settings here
; non functional example: ; non functional example:

View File

@ -115,14 +115,21 @@ bool ConfigurationClass::write()
vedirect["updates_only"] = config.Vedirect_UpdatesOnly; vedirect["updates_only"] = config.Vedirect_UpdatesOnly;
vedirect["poll_interval"] = config.Vedirect_PollInterval; vedirect["poll_interval"] = config.Vedirect_PollInterval;
JsonObject powermeter = doc.createNestedObject("powermeter");
powermeter["enabled"] = config.PowerMeter_Enabled;
powermeter["interval"] = config.PowerMeter_Interval;
powermeter["source"] = config.PowerMeter_Source;
powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter_MqttTopicPowerMeter1;
powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter_MqttTopicPowerMeter2;
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3;
powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate;
powermeter["sdmaddress"] = config.PowerMeter_SdmAddress;
JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); JsonObject powerlimiter = doc.createNestedObject("powerlimiter");
powerlimiter["enabled"] = config.PowerLimiter_Enabled; powerlimiter["enabled"] = config.PowerLimiter_Enabled;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled;
powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy;
powerlimiter["interval"] = config.PowerLimiter_Interval; powerlimiter["interval"] = config.PowerLimiter_Interval;
powerlimiter["mqtt_topic_powermeter_1"] = config.PowerLimiter_MqttTopicPowerMeter1;
powerlimiter["mqtt_topic_powermeter_2"] = config.PowerLimiter_MqttTopicPowerMeter2;
powerlimiter["mqtt_topic_powermeter_3"] = config.PowerLimiter_MqttTopicPowerMeter3;
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter;
powerlimiter["inverter_id"] = config.PowerLimiter_InverterId; powerlimiter["inverter_id"] = config.PowerLimiter_InverterId;
powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId; powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId;
@ -281,14 +288,22 @@ bool ConfigurationClass::read()
config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY;
config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL; config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL;
JsonObject powermeter = doc["powermeter"];
config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED;
config.PowerMeter_Interval = powermeter["interval"] | POWERMETER_INTERVAL;
config.PowerMeter_Source = powermeter["source"] | POWERMETER_SOURCE;
strlcpy(config.PowerMeter_MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter1));
strlcpy(config.PowerMeter_MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter2));
strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3));
config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
JsonObject powerlimiter = doc["powerlimiter"]; JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED;
config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY;
config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; config.PowerLimiter_Interval = POWERLIMITER_INTERVAL;
strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, powerlimiter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter1));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, powerlimiter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter2));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, powerlimiter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter3));
config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;

View File

@ -126,9 +126,12 @@ void MqttHandleInverterClass::publishField(std::shared_ptr<InverterAbstract> inv
return; return;
} }
MqttSettings.publish(topic, String( String value = String(
inv->Statistics()->getChannelFieldValue(type, channel, fieldId), inv->Statistics()->getChannelFieldValue(type, channel, fieldId),
static_cast<unsigned int>(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId)))); static_cast<unsigned int>(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId)));
value.trim();
MqttSettings.publish(topic, value);
} }
String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId)

View File

@ -4,6 +4,7 @@
*/ */
#include "Battery.h" #include "Battery.h"
#include "PowerMeter.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include "Configuration.h" #include "Configuration.h"
#include "MqttSettings.h" #include "MqttSettings.h"
@ -16,60 +17,17 @@ PowerLimiterClass PowerLimiter;
void PowerLimiterClass::init() void PowerLimiterClass::init()
{ {
using std::placeholders::_1; _lastCommandSent = 0;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
CONFIG_T& config = Configuration.get();
// Zero export power limiter
if (strlen(config.PowerLimiter_MqttTopicPowerMeter1) != 0) {
MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter1, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
}
if (strlen(config.PowerLimiter_MqttTopicPowerMeter2) != 0) {
MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter2, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
}
if (strlen(config.PowerLimiter_MqttTopicPowerMeter3) != 0) {
MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
}
_lastCommandSent = 0;
_lastLoop = 0; _lastLoop = 0;
_lastPowerMeterUpdate = 0;
_lastRequestedPowerLimit = 0; _lastRequestedPowerLimit = 0;
} }
void PowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
CONFIG_T& config = Configuration.get();
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter1) == 0) {
_powerMeter1Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter2) == 0) {
_powerMeter2Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter3) == 0) {
_powerMeter3Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
_lastPowerMeterUpdate = millis();
}
void PowerLimiterClass::loop() void PowerLimiterClass::loop()
{ {
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter_Enabled if (!config.PowerLimiter_Enabled
|| !MqttSettings.getConnected() || !config.PowerMeter_Enabled
|| !Hoymiles.getRadio()->isIdle() || !Hoymiles.getRadio()->isIdle()
|| (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000)
|| (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) {
@ -93,7 +51,7 @@ void PowerLimiterClass::loop()
return; return;
} }
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) {
MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n",
dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing());
} }
@ -209,7 +167,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
{ {
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
int32_t newPowerLimit = round(_powerMeter1Power + _powerMeter2Power + _powerMeter3Power); int32_t newPowerLimit = round(PowerMeter.getPowerTotal());
float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF);
int32_t victronChargePower = this->getDirectSolarPower(); int32_t victronChargePower = this->getDirectSolarPower();
@ -218,7 +176,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n",
victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit);
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) {
if (config.PowerLimiter_IsInverterBehindPowerMeter) { if (config.PowerLimiter_IsInverterBehindPowerMeter) {
// If the inverter the behind the power meter (part of measurement), // If the inverter the behind the power meter (part of measurement),
// the produced power of this inverter has also to be taken into account. // the produced power of this inverter has also to be taken into account.

120
src/PowerMeter.cpp Normal file
View File

@ -0,0 +1,120 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "PowerMeter.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "SDM.h"
#include "MessageOutput.h"
#include <ctime>
PowerMeterClass PowerMeter;
SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN);
void PowerMeterClass::init()
{
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
_lastPowerMeterUpdate = 0;
CONFIG_T& config = Configuration.get();
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter2, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter3, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
mqttInitDone = true;
sdm.begin();
}
void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
CONFIG_T& config = Configuration.get();
if(config.PowerMeter_Enabled && config.PowerMeter_Source == 0){
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter1) == 0) {
_powerMeter1Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter2) == 0) {
_powerMeter2Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter3) == 0) {
_powerMeter3Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal());
}
_lastPowerMeterUpdate = millis();
}
float PowerMeterClass::getPowerTotal(){
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
}
uint32_t PowerMeterClass::getLastPowerMeterUpdate(){
return _lastPowerMeterUpdate;
}
void PowerMeterClass::mqtt(){
if (!MqttSettings.getConnected()){
return;
}else{
String topic = "powermeter";
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power));
MqttSettings.publish(topic + "/powertotal", String(getPowerTotal()));
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage));
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage));
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage));
MqttSettings.publish(topic + "/import", String(_PowerMeterImport));
MqttSettings.publish(topic + "/export", String(_PowerMeterExport));
}
}
void PowerMeterClass::loop()
{
CONFIG_T& config = Configuration.get();
if(config.PowerMeter_Enabled && millis() - _lastPowerMeterUpdate >= (config.PowerMeter_Interval * 1000)){
uint8_t _address = config.PowerMeter_SdmAddress;
if(config.PowerMeter_Source == 1){
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
_powerMeter2Power = 0.0;
_powerMeter3Power = 0.0;
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
_powerMeter2Voltage = 0.0;
_powerMeter3Voltage = 0.0;
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
}
if(config.PowerMeter_Source == 2){
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
}
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal());
mqtt();
_lastPowerMeterUpdate = millis();
}
}

View File

@ -31,6 +31,7 @@ void WebApiClass::init()
_webApiNetwork.init(&_server); _webApiNetwork.init(&_server);
_webApiNtp.init(&_server); _webApiNtp.init(&_server);
_webApiPower.init(&_server); _webApiPower.init(&_server);
_webApiPowerMeter.init(&_server);
_webApiPowerLimiter.init(&_server); _webApiPowerLimiter.init(&_server);
_webApiPrometheus.init(&_server); _webApiPrometheus.init(&_server);
_webApiSecurity.init(&_server); _webApiSecurity.init(&_server);
@ -60,6 +61,7 @@ void WebApiClass::loop()
_webApiNetwork.loop(); _webApiNetwork.loop();
_webApiNtp.loop(); _webApiNtp.loop();
_webApiPower.loop(); _webApiPower.loop();
_webApiPowerMeter.loop();
_webApiPowerLimiter.loop(); _webApiPowerLimiter.loop();
_webApiSecurity.loop(); _webApiSecurity.loop();
_webApiSysstatus.loop(); _webApiSysstatus.loop();

View File

@ -10,6 +10,8 @@
#include "WebApi.h" #include "WebApi.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "helper.h" #include "helper.h"
#include "PowerLimiter.h"
#include "PowerMeter.h"
#include <AsyncJson.h> #include <AsyncJson.h>
void WebApiMqttClass::init(AsyncWebServer* server) void WebApiMqttClass::init(AsyncWebServer* server)
@ -318,6 +320,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
MqttSettings.performReconnect(); MqttSettings.performReconnect();
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();
MqttHandleVedirectHass.forceUpdate(); MqttHandleVedirectHass.forceUpdate();
PowerMeter.init();
PowerLimiter.init();
} }
String WebApiMqttClass::getRootCaCertInfo(const char* cert) String WebApiMqttClass::getRootCaCertInfo(const char* cert)

View File

@ -8,7 +8,9 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "MqttHandleHass.h" #include "MqttHandleHass.h"
#include "MqttHandleVedirectHass.h"
#include "MqttSettings.h" #include "MqttSettings.h"
#include "PowerMeter.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include "WebApi.h" #include "WebApi.h"
#include "helper.h" #include "helper.h"
@ -38,9 +40,6 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
root[F("enabled")] = config.PowerLimiter_Enabled; root[F("enabled")] = config.PowerLimiter_Enabled;
root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled;
root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy;
root[F("mqtt_topic_powermeter_1")] = config.PowerLimiter_MqttTopicPowerMeter1;
root[F("mqtt_topic_powermeter_2")] = config.PowerLimiter_MqttTopicPowerMeter2;
root[F("mqtt_topic_powermeter_3")] = config.PowerLimiter_MqttTopicPowerMeter3;
root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter;
root[F("inverter_id")] = config.PowerLimiter_InverterId; root[F("inverter_id")] = config.PowerLimiter_InverterId;
root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId; root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId;
@ -122,9 +121,6 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
config.PowerLimiter_Enabled = root[F("enabled")].as<bool>(); config.PowerLimiter_Enabled = root[F("enabled")].as<bool>();
config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as<bool>(); config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as<bool>();
config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as<uint8_t>(); config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as<uint8_t>();
strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter1));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter2));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter3));
config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>(); config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>();
config.PowerLimiter_InverterId = root[F("inverter_id")].as<uint8_t>(); config.PowerLimiter_InverterId = root[F("inverter_id")].as<uint8_t>();
config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as<uint8_t>(); config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as<uint8_t>();
@ -146,7 +142,4 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
response->setLength(); response->setLength();
request->send(response); request->send(response);
MqttSettings.performReconnect(); // TODO(helge) is this really needed
PowerLimiter.init();
} }

125
src/WebApi_powermeter.cpp Normal file
View File

@ -0,0 +1,125 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "WebApi_powermeter.h"
#include "VeDirectFrameHandler.h"
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Configuration.h"
#include "MqttHandleVedirectHass.h"
#include "MqttHandleHass.h"
#include "MqttSettings.h"
#include "PowerLimiter.h"
#include "PowerMeter.h"
#include "WebApi.h"
#include "helper.h"
void WebApiPowerMeterClass::init(AsyncWebServer* server)
{
using std::placeholders::_1;
_server = server;
_server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1));
_server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1));
_server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1));
}
void WebApiPowerMeterClass::loop()
{
}
void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.PowerMeter_Enabled;
root[F("source")] = config.PowerMeter_Source;
root[F("interval")] = config.PowerMeter_Interval;
root[F("mqtt_topic_powermeter_1")] = config.PowerMeter_MqttTopicPowerMeter1;
root[F("mqtt_topic_powermeter_2")] = config.PowerMeter_MqttTopicPowerMeter2;
root[F("mqtt_topic_powermeter_3")] = config.PowerMeter_MqttTopicPowerMeter3;
root[F("sdmbaudrate")] = config.PowerMeter_SdmBaudrate;
root[F("sdmaddress")] = config.PowerMeter_SdmAddress;
response->setLength();
request->send(response);
}
void WebApiPowerMeterClass::onAdminGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
this->onStatus(request);
}
void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning");
if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!");
response->setLength();
request->send(response);
return;
}
String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg[F("message")] = F("Data too large!");
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg[F("message")] = F("Failed to parse data!");
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("enabled") && root.containsKey("source"))) {
retMsg[F("message")] = F("Values are missing!");
response->setLength();
request->send(response);
return;
}
CONFIG_T& config = Configuration.get();
config.PowerMeter_Enabled = root[F("enabled")].as<bool>();
config.PowerMeter_Source = root[F("source")].as<uint8_t>();
config.PowerMeter_Interval = root[F("interval")].as<uint32_t>();
strlcpy(config.PowerMeter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as<String>().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter1));
strlcpy(config.PowerMeter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as<String>().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter2));
strlcpy(config.PowerMeter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as<String>().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter3));
config.PowerMeter_SdmBaudrate = root[F("sdmbaudrate")].as<uint32_t>();
config.PowerMeter_SdmAddress = root[F("sdmaddress")].as<uint8_t>();
Configuration.write();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
response->setLength();
request->send(response);
yield();
delay(1000);
yield();
ESP.restart();
}

View File

@ -61,14 +61,16 @@ void WebApiWsLiveClass::loop()
if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) {
try { try {
DynamicJsonDocument root(40960);
JsonVariant var = root;
generateJsonResponse(var);
String buffer; String buffer;
if (buffer) { // free JsonDocument as soon as possible
{
DynamicJsonDocument root(40960);
JsonVariant var = root;
generateJsonResponse(var);
serializeJson(root, buffer); serializeJson(root, buffer);
}
if (buffer) {
if (Configuration.get().Security_AllowReadonly) { if (Configuration.get().Security_AllowReadonly) {
_ws.setAuthentication("", ""); _ws.setAuthentication("", "");
} else { } else {

View File

@ -20,6 +20,7 @@
#include "SunPosition.h" #include "SunPosition.h"
#include "Utils.h" #include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "PowerMeter.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include "defaults.h" #include "defaults.h"
#include <Arduino.h> #include <Arduino.h>
@ -146,6 +147,8 @@ void setup()
} else { } else {
MessageOutput.println(F("Invalid pin config")); MessageOutput.println(F("Invalid pin config"));
} }
// Power meter
PowerMeter.init();
// Dynamic power limiter // Dynamic power limiter
PowerLimiter.init(); PowerLimiter.init();
@ -165,6 +168,8 @@ void loop()
{ {
NetworkSettings.loop(); NetworkSettings.loop();
yield(); yield();
PowerMeter.loop();
yield();
PowerLimiter.loop(); PowerLimiter.loop();
yield(); yield();
InverterSettings.loop(); InverterSettings.loop();

View File

@ -21,7 +21,7 @@
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.9.2", "@intlify/unplugin-vue-i18n": "^0.9.3",
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
"@types/bootstrap": "^5.2.6", "@types/bootstrap": "^5.2.6",
"@types/node": "^18.15.3", "@types/node": "^18.15.3",
@ -34,8 +34,8 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.59.3", "sass": "^1.59.3",
"terser": "^5.16.6", "terser": "^5.16.6",
"typescript": "^4.9.5", "typescript": "^5.0.2",
"vite": "^4.2.0", "vite": "^4.2.1",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.1.0", "vite-plugin-css-injected-by-js": "^3.1.0",
"vue-tsc": "^1.2.0" "vue-tsc": "^1.2.0"

View File

@ -51,6 +51,9 @@
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link>
</li> </li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/powermeter">{{ $t('menu.PowerMeterSettings') }}</router-link>
</li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/powerlimiter">Dynamic Power Limiter</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/powerlimiter">Dynamic Power Limiter</router-link>
</li> </li>

View File

@ -11,8 +11,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(category) in categories"> <template v-for="(category) in categories" :key="category">
<tr v-for="(prop, prop_idx) in properties(category)"> <tr v-for="(prop, prop_idx) in properties(category)" :key="prop">
<td v-if="prop_idx == 0" :rowspan="properties(category).length"> <td v-if="prop_idx == 0" :rowspan="properties(category).length">
{{ capitalizeFirstLetter(category) }}</td> {{ capitalizeFirstLetter(category) }}</td>
<td :class="{ 'table-danger': !isEqual(category, prop) }">{{ prop }}</td> <td :class="{ 'table-danger': !isEqual(category, prop) }">{{ prop }}</td>
@ -80,14 +80,25 @@ export default defineComponent({
return Array.from(new Set(total)).sort(); return Array.from(new Set(total)).sort();
}, },
isEqual(category: string, prop: string): boolean { isEqual(category: string, prop: string): boolean {
if (!((this.selectedPinAssignment as Device)[category as keyof Device])) { let comSel = 999999;
return false; let comCur = 999999;
if ((this.selectedPinAssignment as Device)[category as keyof Device]) {
comSel = (this.selectedPinAssignment as any)[category][prop];
} }
if (!((this.currentPinAssignment as Device)[category as keyof Device])) { if ((this.currentPinAssignment as Device)[category as keyof Device]) {
return false; comCur = (this.currentPinAssignment as any)[category][prop];
} }
return (this.selectedPinAssignment as any)[category][prop] == (this.currentPinAssignment as any)[category][prop]; if (comSel == -1 || comSel == 255 || comSel == undefined) {
comSel = 999999;
}
if (comCur == -1 || comCur == 255 || comSel == undefined) {
comCur = 999999;
}
return comSel == comCur;
}, },
capitalizeFirstLetter(value: string): string { capitalizeFirstLetter(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1); return value.charAt(0).toUpperCase() + value.slice(1);

View File

@ -452,6 +452,24 @@
"UpdatesOnly": "Nur Änderungen senden:", "UpdatesOnly": "Nur Änderungen senden:",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{
"PowerMeterSettings": "Stromzähler Einstellungen",
"PowerMeterConfiguration": "Stromzähler Konfiguration",
"PowerMeterEnable": "Aktiviere Stromzähler",
"PowerMeterParameter": "Power Meter Parameter",
"PowerMeterSource": "Stromzählertyp",
"MQTT": "MQTT Konfiguration",
"typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1",
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)",
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)",
"SDM": "SDM-Stromzähler Konfiguration",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Adresse",
"Save": "@:dtuadmin.Save"
},
"powerlimiteradmin": { "powerlimiteradmin": {
"PowerLimiterSettings": "Power Limiter Einstellungen", "PowerLimiterSettings": "Power Limiter Einstellungen",
"PowerLimiterConfiguration": "Power Limiter Konfiguration", "PowerLimiterConfiguration": "Power Limiter Konfiguration",
@ -472,10 +490,7 @@
"TargetPowerConsumptionHysteresisHint": "Wert um den der Zielstromverbrauch schwanken darf, ohne dass nachgeregelt wird.", "TargetPowerConsumptionHysteresisHint": "Wert um den der Zielstromverbrauch schwanken darf, ohne dass nachgeregelt wird.",
"LowerPowerLimit": "Unteres Leistungslimit", "LowerPowerLimit": "Unteres Leistungslimit",
"UpperPowerLimit": "Oberes Leistungslimit", "UpperPowerLimit": "Oberes Leistungslimit",
"PowerMeters": "Leistungsmesser - MQTT", "PowerMeters": "Leistungsmesser",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (Optional)",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (Optional)",
"BatterySocStartThreshold": "Akku SOC - Start", "BatterySocStartThreshold": "Akku SOC - Start",
"BatterySocStopThreshold": "Akku SOC - Stop", "BatterySocStopThreshold": "Akku SOC - Stop",
"VoltageStartThreshold": "DC Spannung - Start", "VoltageStartThreshold": "DC Spannung - Start",

View File

@ -10,6 +10,7 @@
"DTUSettings": "DTU Settings", "DTUSettings": "DTU Settings",
"DeviceManager": "Device-Manager", "DeviceManager": "Device-Manager",
"VedirectSettings": "Ve.direct Settings", "VedirectSettings": "Ve.direct Settings",
"PowerMeterSettings": "Power Meter Settings",
"BatterySettings": "@:batteryadmin.BatterySettings", "BatterySettings": "@:batteryadmin.BatterySettings",
"ConfigManagement": "Config Management", "ConfigManagement": "Config Management",
"FirmwareUpgrade": "Firmware Upgrade", "FirmwareUpgrade": "Firmware Upgrade",
@ -452,6 +453,24 @@
"UpdatesOnly": "Send only updates:", "UpdatesOnly": "Send only updates:",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{
"PowerMeterSettings": "Power Meter Settings",
"PowerMeterConfiguration": "Power Meter Configuration",
"PowerMeterEnable": "Enable Power Meter",
"PowerMeterParameter": "Power Meter Parameter",
"PowerMeterSource": "Power Meter type",
"MQTT": "MQTT Parameter",
"typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
"SDM": "SDM-Power Meter Parameter",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Address",
"Save": "@:dtuadmin.Save"
},
"powerlimiteradmin": { "powerlimiteradmin": {
"PowerLimiterSettings": "Power Limiter Settings", "PowerLimiterSettings": "Power Limiter Settings",
"PowerLimiterConfiguration": "Power Limiter Configuration", "PowerLimiterConfiguration": "Power Limiter Configuration",
@ -472,7 +491,7 @@
"TargetPowerConsumptionHysteresisHint": "Value around which the target power consumption fluctuates without readjustment.", "TargetPowerConsumptionHysteresisHint": "Value around which the target power consumption fluctuates without readjustment.",
"LowerPowerLimit": "Lower power limit", "LowerPowerLimit": "Lower power limit",
"UpperPowerLimit": "Upper power limit", "UpperPowerLimit": "Upper power limit",
"PowerMeters": "Power meters - MQTT", "PowerMeters": "Power meter",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (optional)", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (optional)",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (optional)", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (optional)",

View File

@ -7,6 +7,7 @@ import DtuAdminView from '@/views/DtuAdminView.vue';
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'; import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
import HomeView from '@/views/HomeView.vue'; import HomeView from '@/views/HomeView.vue';
import VedirectAdminView from '@/views/VedirectAdminView.vue' import VedirectAdminView from '@/views/VedirectAdminView.vue'
import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue'
import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue' import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue'
import VedirectInfoView from '@/views/VedirectInfoView.vue' import VedirectInfoView from '@/views/VedirectInfoView.vue'
import InverterAdminView from '@/views/InverterAdminView.vue'; import InverterAdminView from '@/views/InverterAdminView.vue';
@ -86,6 +87,11 @@ const router = createRouter({
name: 'Ve.direct Settings', name: 'Ve.direct Settings',
component: VedirectAdminView component: VedirectAdminView
}, },
{
path: '/settings/powermeter',
name: 'Power meter Settings',
component: PowerMeterAdminView
},
{ {
path: '/settings/powerlimiter', path: '/settings/powerlimiter',
name: 'Power limiter Settings', name: 'Power limiter Settings',

View File

@ -2,9 +2,6 @@ export interface PowerLimiterConfig {
enabled: boolean; enabled: boolean;
solar_passtrough_enabled: boolean; solar_passtrough_enabled: boolean;
battery_drain_strategy: number; battery_drain_strategy: number;
mqtt_topic_powermeter_1: string;
mqtt_topic_powermeter_2: string;
mqtt_topic_powermeter_3: string;
is_inverter_behind_powermeter: boolean; is_inverter_behind_powermeter: boolean;
inverter_id: number; inverter_id: number;
inverter_channel_id: number; inverter_channel_id: number;

View File

@ -0,0 +1,10 @@
export interface PowerMeterConfig {
enabled: boolean;
source: number;
interval: number;
mqtt_topic_powermeter_1: string;
mqtt_topic_powermeter_2: string;
mqtt_topic_powermeter_3: string;
sdmbaudrate: number;
sdmaddress: number;
}

View File

@ -114,36 +114,6 @@
<CardElement :text="$t('powerlimiteradmin.PowerMeters')" textVariant="text-bg-primary" add-space <CardElement :text="$t('powerlimiteradmin.PowerMeters')" textVariant="text-bg-primary" add-space
v-show="powerLimiterConfigList.enabled" v-show="powerLimiterConfigList.enabled"
> >
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter1" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter1') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter1"
placeholder="shellies/shellyem3/emeter/0/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_1" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter2" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter2') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter2"
placeholder="shellies/shellyem3/emeter/1/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_2" required/>
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter3" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter3') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter3"
placeholder="shellies/shellyem3/emeter/2/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_3" required/>
</div>
</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputRetain">{{ $t('powerlimiteradmin.InverterIsBehindPowerMeter') }}</label> <label class="col-sm-2 form-check-label" for="inputRetain">{{ $t('powerlimiteradmin.InverterIsBehindPowerMeter') }}</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -0,0 +1,160 @@
<template>
<BasePage :title="$t('powermeteradmin.PowerMeterSettings')" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<form @submit="savePowerMeterConfig">
<div class="card">
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.PowerMeterConfiguration') }}</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputPowerMeterEnable">{{ $t('powermeteradmin.PowerMeterEnable') }}</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputPowerMeterEnable"
v-model="powerMeterConfigList.enabled" />
</div>
</div>
</div>
<div class="row mb-3" v-show="powerMeterConfigList.enabled">
<label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.PowerMeterSource') }}</label>
<div class="col-sm-10">
<select class="form-select" v-model="powerMeterConfigList.source">
<option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key">
{{ source.value }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="card" v-if="powerMeterConfigList.source === 0 && powerMeterConfigList.enabled" >
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.MQTT') }}</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter1" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter1') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter1"
placeholder="shellies/shellyem3/emeter/0/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_1" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter2" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter2') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter2"
placeholder="shellies/shellyem3/emeter/1/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_2" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter3" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter3') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter3"
placeholder="shellies/shellyem3/emeter/2/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_3" />
</div>
</div>
</div>
</div>
</div>
<div class="card" v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2) && powerMeterConfigList.enabled" >
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.SDM') }}</div>
<div class="card-body">
<div class="row mb-3">
<label for="sdmbaudrate" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.sdmbaudrate') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="sdmbaudrate"
placeholder="9600" v-model="powerMeterConfigList.sdmbaudrate" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="sdmaddress" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.sdmaddress') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="sdmaddress"
placeholder="1" v-model="powerMeterConfigList.sdmaddress" />
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary mb-3">{{ $t('powermeteradmin.Save') }}</button>
</form>
</BasePage>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterConfig } from "@/types/PowerMeterConfig";
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
},
data() {
return {
dataLoading: true,
powerMeterConfigList: {} as PowerMeterConfig,
powerMeterSourceList: [
{ key: 0, value: this.$t('powermeteradmin.typeMQTT') },
{ key: 1, value: this.$t('powermeteradmin.typeSDM1ph') },
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') },
],
alertMessage: "",
alertType: "info",
showAlert: false,
};
},
created() {
this.getPowerMeterConfig();
},
methods: {
getPowerMeterConfig() {
this.dataLoading = true;
fetch("/api/powermeter/config", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.powerMeterConfigList = data;
this.dataLoading = false;
});
},
savePowerMeterConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(this.powerMeterConfigList));
fetch("/api/powermeter/config", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
);
},
},
});
</script>

View File

@ -2,6 +2,7 @@
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "5.0",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]

View File

@ -168,16 +168,17 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@intlify/bundle-utils@^5.1.2": "@intlify/bundle-utils@^5.3.1":
version "5.2.0" version "5.3.1"
resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-5.2.0.tgz#9dc11138d232d7cfb1163feb850f653ce4dfedaf" resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-5.3.1.tgz#e0c609920448f5d3289ec967c8a51b0c6f80fa9d"
integrity sha512-rIfoNUTBoZK6IfaEeuoYMQZSuAXhPyZoy+UsdZj+V4eM632ynN1bGt5ttkpGO8xe0c+esfYslgJxBz//bdu4qg== integrity sha512-Lfrl3zlVmUy9Gqf9K9uuCCdhd5WxjkSQWIdEYQn1Uso4bjy4iHq9GZQYyvoA6+KFiDoVOS0LE3T+c7jwhLkqgg==
dependencies: dependencies:
"@intlify/message-compiler" next "@intlify/message-compiler" next
"@intlify/shared" next "@intlify/shared" next
acorn "^8.8.2" acorn "^8.8.2"
estree-walker "^2.0.2" estree-walker "^2.0.2"
jsonc-eslint-parser "^1.0.1" jsonc-eslint-parser "^1.0.1"
magic-string "^0.30.0"
source-map "0.6.1" source-map "0.6.1"
yaml-eslint-parser "^0.3.2" yaml-eslint-parser "^0.3.2"
@ -224,12 +225,12 @@
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.16.tgz#74f254dbb7eac633b86d690a341349db29573896" resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.16.tgz#74f254dbb7eac633b86d690a341349db29573896"
integrity sha512-kXbm4svALe3lX+EjdJxfnabOphqS4yQ1Ge/iIlR8tvUiYRCoNz3hig1M4336iY++Dfx5ytEQJPNjIcknNIuvig== integrity sha512-kXbm4svALe3lX+EjdJxfnabOphqS4yQ1Ge/iIlR8tvUiYRCoNz3hig1M4336iY++Dfx5ytEQJPNjIcknNIuvig==
"@intlify/unplugin-vue-i18n@^0.9.2": "@intlify/unplugin-vue-i18n@^0.9.3":
version "0.9.2" version "0.9.3"
resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.9.2.tgz#7d9166a1a84343da6632c80815150487ac7f533f" resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.9.3.tgz#2f8dab79492a8c7218f55526954d0b5d8940009b"
integrity sha512-cNfa90+NVNdYJ0qqwRaEb2kGGp9zAve2xaAKCL7EzcQcvSWw42mhiOxcNkUc1QKlXnSHERMd6aT4/GUlFT1zBw== integrity sha512-23DMh2r0qA7UZfaQhF09ZHhifgTyKcbmVsCo+qHvu9q1EU8OF18VlhxMHMksDR5NBDvRXj3Lmu8lT84XDrUlSw==
dependencies: dependencies:
"@intlify/bundle-utils" "^5.1.2" "@intlify/bundle-utils" "^5.3.1"
"@intlify/shared" next "@intlify/shared" next
"@rollup/pluginutils" "^5.0.2" "@rollup/pluginutils" "^5.0.2"
"@vue/compiler-sfc" "^3.2.47" "@vue/compiler-sfc" "^3.2.47"
@ -277,7 +278,7 @@
"@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": "@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13":
version "1.4.14" version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
@ -1670,6 +1671,13 @@ magic-string@^0.25.7:
dependencies: dependencies:
sourcemap-codec "^1.4.8" sourcemap-codec "^1.4.8"
magic-string@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13"
memorystream@^0.3.1: memorystream@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
@ -2251,10 +2259,10 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^4.9.5: typescript@^5.0.2:
version "4.9.5" version "5.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.2.tgz#891e1a90c5189d8506af64b9ef929fca99ba1ee5"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== integrity sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==
unbox-primitive@^1.0.2: unbox-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
@ -2315,10 +2323,10 @@ vite-plugin-css-injected-by-js@^3.1.0:
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.1.0.tgz#d1160c975d40f256692e2465832e6ff18c22b3a3" resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.1.0.tgz#d1160c975d40f256692e2465832e6ff18c22b3a3"
integrity sha512-qogCmpocZfcbSAYZQjS88ieIY0PzLUm7RkLFWFgAxkXdz3N6roZbSTNTxeIOj5IxFbZWACUPuVBBoo6qCuXDcw== integrity sha512-qogCmpocZfcbSAYZQjS88ieIY0PzLUm7RkLFWFgAxkXdz3N6roZbSTNTxeIOj5IxFbZWACUPuVBBoo6qCuXDcw==
vite@^4.2.0: vite@^4.2.1:
version "4.2.0" version "4.2.1"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.0.tgz#d4e6eafbc034f3faf0ab376bd5b76ac15775eb99" resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.1.tgz#6c2eb337b0dfd80a9ded5922163b94949d7fc254"
integrity sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g== integrity sha512-7MKhqdy0ISo4wnvwtqZkjke6XN4taqQ2TBaTccLIpOKv7Vp2h4Y+NpmWCnGDeSvvn45KxvWgGyb0MkHvY1vgbg==
dependencies: dependencies:
esbuild "^0.17.5" esbuild "^0.17.5"
postcss "^8.4.21" postcss "^8.4.21"

Binary file not shown.