Merge branch 'development'

This commit is contained in:
helgeerbe 2024-01-04 15:47:46 +01:00
commit aa5a762d2a
43 changed files with 805 additions and 1117 deletions

View File

@ -96,32 +96,6 @@ Several screenshots of the frontend can be found here: [Screenshots](docs/screen
* A documentation of the Web API can be found here: [Web-API Documentation](docs/Web-API.md)
* Home Assistant auto discovery is supported. [Example image](https://user-images.githubusercontent.com/59169507/217558862-a83846c5-6070-43cd-9a0b-90a8b2e2e8c6.png)
### Dynamic Power Limiter
The dynamic power limiter is responsible for automatic inverter power adjustment. It will take the Power Meter (i.e. currently consumed power), the solar power and the battery charge state into account. The dynamic power limiter supports a few different strategies that can be configured from the user interface:
* Solar Passthrough is off
* When using this strategy the inverter is steered such that the currently consumed power (as provided by the power meter) is compensated for. This is done as long as the battery charge state is above the limit set by the stop threshold. The inverter is turned off if the battery reaches the limit and is only re-enabled if the battery charge state reaches the limit set by start threshold.
* Solar Passthrough is on and the battery drain strategy is empty when full
* This case applies the same strategy as the strategy above. In addition Solar Power will be used to compensate for the currently used energy in cases where the battery discharge is disabled. In this case the inverter power limit is constrained to the input solar power and the power meter value so that battery discharge is avoided.
* Solar Passthrough is on and the battery drain strategy is empty at night
* When using this strategy the inverter is steered such that the currently consumed power (as provided by the power meter) is compensated for. During daytime energy is taken from solar and from the battery, if the battery level is above the start threshold. At night battery power is used until the battery level reaches the stop threshold. When operating on solar power only (i.e. without using the battery) the inverter power limit is constrained to the input solar power and the power meter value so that battery discharge is avoided. The daytime / nighttime switch is based on the Victron MPPT Solar Charger power and 20W input are required in this case.
Other settings are:
* The inverter ID configures the inverter that is controlled by the power limiter. The power limiter can only control a single inverter at this point in time.
* Channel ID is the inverter input channel ID that is used for battery voltage readings.
* Target power consumption specifies the power to be either consumed from the grid (when set to a positive value) or fed back into the grid (when set to a negative value).
* The hysteresis value helps optimize communication with the inverter by skipping unnecessary power limit updates. An update is only sent if the absolute difference between the newly computed power limit and the previously set limit matches or exceeds the hysteresis value. This approach can conserve both airtime and CPU resources.
* Power limits control the min / max limits of the inverter
* Inverter is behind power meter. Select this if your inverter power is measured by the power meter. This is typically the case.
* Battery start and stop threshold can be configured using voltage and / or state of charge values. Stage of charge values requires a Pylontech battery at this point.
* A Battery full solar passthrough threshold can be configured using voltage or state of charge value. Stage of charge values requires a Pylontech battery at this point. The option can be used if the battery is full and will steer the inverter according to solar power reported by the Victron MPPT Solar Charger.
![image](https://user-images.githubusercontent.com/59169507/222155765-9fff47a4-8ffa-42cf-8671-6359288e0cab.png)
#### Power Limiter States
![PowerLimiterInverterStates](https://github.com/helgeerbe/OpenDTU-OnBattery/blob/development/docs/PowerLimiterInverterStates.png)
### Huawei PSU
The Huawei PSU can be used to charge a battery. This can be be useful if an external (AC) connected solar system shall be utilized or if variable energy prices should be exploited.

View File

@ -1,12 +1,8 @@
# Device Profiles
It is possible to change hardware settings like pin assignments or ethernet support using a json file. The json file can be uploaded using the configuration management in the web interface. Just select "Pin Mapping (pin_mapping.json)" in the recovery section.
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/device_profiles/>
When the file is uploaded the ESP performs a reboot. This is required as the pin settings could have changed within the file. By default all the pin assignments are used as compiled into the firmware.
To change the device profile, navigate to the "Device Manager" and selected the appropriate profile. You can see the current (Active) and the new (Selected) in assignment in the table below the combobox.
## Structure of the json file
## Structure of the json file for openDTU-onBattery (outdated example)
```json
[
@ -93,47 +89,3 @@ To change the device profile, navigate to the "Device Manager" and selected the
}
]
```
The json file can contain multiple profiles. Each profile requires a name and different parameters. If one parameter is not set, the default value, as compiled into the firmware is used. The example above shows all the currently supported values. Others may follow. Sample files for some boards can be found [here](DeviceProfiles/). This means you can just flash the generic bin file and upload the json file. Then you select your board and everything works hopyfully as expected.
## Implemented configuration values
| Parameter | Data Type | Description |
| ------------- | --------- | ----------- |
| name | string | Unique name of the profile (max 63 characters) |
| nrf24.miso | number | MISO Pin |
| nrf24.mosi | number | MOSI Pin |
| nrf24.clk | number | Clock Pin |
| nrf24.irq | number | Interrupt Pin |
| nrf24.en | number | Enable Pin |
| nrf24.cs | number | Chip Select Pin |
| cmt.sdio | number | SDIO Pin |
| cmt.clk | number | CLK Pin |
| cmt.cs | number | CS Pin |
| cmt.fcs | number | FCS Pin |
| cmt.gpio2 | number | GPIO2 Pin (optional) |
| cmt.gpio3 | number | GPIO3 Pin (optional) |
| eth.enabled | boolean | Enable/Disable the ethernet stack |
| eth.phy_addr | number | Unique PHY addr |
| eth.power | number | Power Pin (if available). Use -1 for not assigned pins. |
| eth.mdc | number | Serial Management Interface MDC Pin. Use -1 for not assigned pins. |
| eth.mdio | number | Serial Management Interface MDIO Pin. Use -1 for not assigned pins. |
| eth.type | number | Possible values:<br>* 0 = ETH_PHY_LAN8720<br>* 1 = ETH_PHY_TLK110<br>* 2 = ETH_PHY_RTL8201<br>* 3 = ETH_PHY_DP83848<br>* 4 = ETH_PHY_DM9051<br>* 5 = ETH_PHY_KSZ8041<br>* 6 = ETH_PHY_KSZ8081 |
| eth.clk_mode | number | Possible values:<br>* 0 = ETH_CLOCK_GPIO0_IN<br>* 1 = ETH_CLOCK_GPIO0_OUT<br>* 2 = ETH_CLOCK_GPIO16_OUT<br>* 3 = ETH_CLOCK_GPIO17_OUT |
| display.type | number | Specify type of display. Possible values:<br>* 0 = None (default)<br>* 1 = PCD8544 <br>* 2 = SSD1306 <br>* 3 = SH1106 |
| display.data | number | Data Pin (e.g. SDA for i2c displays) required for all displays. Use 255 for not assigned pins. |
| display.clk | number | Clock Pin (e.g. SCL for i2c displays) required for SSD1306 and SH1106. Use 255 for not assigned pins. |
| display.cs | number | Chip Select Pin required for PCD8544. Use 255 for not assigned pins. |
| display.reset | number | Reset Pin required for PCD8544, optional for all other displays. Use 255 for not assigned pins. |
| victron.rx | number | Victron Ve.direct Rx pin |
| victron.tx | number | Victron Ve.direct Tx pin |
| battery.rx | number | Pylontech CAN bus battery Rx pin |
| battery.tx | number | Pylontech CAN bus battery Tx pin |
| huawei.miso | number | MISO Pin for Huawei CAN bus interface |
| huawei.mosi | number | MOSI Pin for Huawei CAN bus interface |
| huawei.clk | number | CLK Pin for Huawei CAN bus interface |
| huawei.cs | number | CS Pin for Huawei CAN bus interface |
| huawei.irq | number | IRQ Pin for Huawei CAN bus interface |
| huawei.power | number | Power Pin for Huawei power control (e.g. using slot detect) |
| led.led0 | number | LED pin for network indication. Blinking = WLAN connected but NTP & MQTT (if enabled) disconnected. On = WLAN, NTP, MQTT connected. Off = Network not connected |
| led.led1 | number | LED pin for inverter indication. On = All inverters reachable & producing. Blinking = All inverters reachable but not producing. Off = At least one inverter is not reachable. Only inverters with polling enabled are considered. |

View File

@ -73,6 +73,25 @@
"clk": 22
}
},
{
"name": "NRF24 with SSD1309",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 4,
"cs": 5
},
"eth": {
"enabled": false
},
"display": {
"type": 4,
"data": 21,
"clk": 22
}
},
{
"name": "CMT2300A with SSD1306",
"nrf24": {
@ -127,6 +146,33 @@
"clk": 22
}
},
{
"name": "CMT2300A with SSD1309",
"nrf24": {
"miso": -1,
"mosi": -1,
"clk": -1,
"irq": -1,
"en": -1,
"cs": -1
},
"cmt": {
"clk": 18,
"cs": 4,
"fcs": 5,
"sdio": 23,
"gpio2": 19,
"gpio3": 16
},
"eth": {
"enabled": false
},
"display": {
"type": 4,
"data": 21,
"clk": 22
}
},
{
"name": "NRF24 + CMT2300A",
"nrf24": {

View File

@ -77,5 +77,33 @@
"data": 33,
"clk": 32
}
},
{
"name": "Olimex ESP32-POE with SSD1309",
"links": [
{"name": "Datasheet", "url": "https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware"}
],
"nrf24": {
"miso": 15,
"mosi": 2,
"clk": 14,
"irq": 13,
"en": 16,
"cs": 5
},
"eth": {
"enabled": true,
"phy_addr": 0,
"power": 12,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 3
},
"display": {
"type": 4,
"data": 33,
"clk": 32
}
}
]

View File

@ -49,5 +49,33 @@
"data": 5,
"clk": 17
}
},
{
"name": "WT32-ETH01 with SSD1309",
"links": [
{"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"}
],
"nrf24": {
"miso": 4,
"mosi": 2,
"clk": 32,
"irq": 33,
"en": 14,
"cs": 15
},
"eth": {
"enabled": true,
"phy_addr": 1,
"power": 16,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 0
},
"display": {
"type": 4,
"data": 5,
"clk": 17
}
}
]

View File

@ -1,88 +1,3 @@
# MQTT Topics
The base topic, as configured in the web GUI is prepended to all follwing topics.
## General topics
| Topic | R / W | Description | Value / Unit |
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| dtu/ip | R | IP address of OpenDTU | IP address |
| dtu/hostname | R | Current hostname of the dtu (as set in web GUI) | |
| dtu/rssi | R | WiFi network quality | db value |
| dtu/status | R | Indicates whether OpenDTU network is reachable | online / offline |
| dtu/uptime | R | Time in seconds since startup | seconds |
## Inverter total topics
Enabled inverter means, that only inverters with "Poll inverter data" enabled are considered.
| Topic | R / W | Description | Value / Unit |
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| ac/power | R | Sum of AC active power of all enabled inverters | W |
| ac/yieldtotal | R | Sum of energy converted to AC since reset watt hours of all enabled inverters | Kilo watt hours (kWh) |
| ac/yieldday | R | Sum of energy converted to AC per day in watt hours of all enabled inverters | Watt hours (Wh)
| ac/is_valid | R | Indicator whether all enabled inverters where reachable | 0 or 1 |
| dc/power | R | Sum of DC power of all enabled inverters | Watt (W) |
| dc/irradiation | R | Produced power of all enabled inverter stripes with defined irradiation settings divided by sum of all enabled inverters irradiation | % |
| dc/is_valid | R | Indicator whether all enabled inverters where reachable | 0 or 1 |
## Inverter specific topics
serial will be replaced with the serial number of the inverter.
| Topic | R / W | Description | Value / Unit |
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/name | R | Name of the inverter as configured in web GUI | |
| [serial]/device/bootloaderversion | R | Bootloader version of the inverter | |
| [serial]/device/fwbuildversion | R | Firmware version of the inverter | |
| [serial]/device/fwbuilddatetime | R | Build date / time of inverter firmware | |
| [serial]/device/hwpartnumber | R | Hardware part number of the inverter | |
| [serial]/device/hwversion | R | Hardware version of the inverter | |
| [serial]/status/reachable | R | Indicates whether the inverter is reachable | 0 or 1 |
| [serial]/status/producing | R | Indicates whether the inverter is producing AC power | 0 or 1 |
| [serial]/status/last_update | R | Unix timestamp of last inverter statistics udpate | seconds since JAN 01 1970 (UTC) |
### AC channel / global specific topics
| Topic | R / W | Description | Value / Unit |
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/0/current | R | AC current in ampere | Ampere (A) |
| [serial]/0/efficiency | R | Ratio AC Power over DC Power in percent | % |
| [serial]/0/frequency | R | AC frequency in hertz | Hertz (Hz) |
| [serial]/0/power | R | AC active power in watts | Watt (W) |
| [serial]/0/powerdc | R | DC power in watts | Watt (W) |
| [serial]/0/powerfactor | R | Power factor in percent | % |
| [serial]/0/reactivepower | R | AC reactive power in VAr | VAr |
| [serial]/0/temperature | R | Temperature of inverter in degree celsius | Degree Celsius (°C) |
| [serial]/0/voltage | R | AC voltage in volt | Volt (V) |
| [serial]/0/yieldday | R | Energy converted to AC per day in watt hours | Watt hours (Wh) |
| [serial]/0/yieldtotal | R | Energy converted to AC since reset watt hours | Kilo watt hours (kWh) |
### DC input channel topics
[1-4] represents the different inputs. The amount depends on the inverter model.
| Topic | R / W | Description | Value / Unit |
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/[1-4]/current | R | DC current of specific input in ampere | Ampere (A) |
| [serial]/[1-4]/name | R | Name of the DC input channel as configured in web GUI| |
| [serial]/[1-4]/irradiation | R | Ratio DC Power over set maximum power (in web GUI) | % |
| [serial]/[1-4]/power | R | DC power of specific input in watt | Watt (W) |
| [serial]/[1-4]/voltage | R | DC voltage of specific input in volt | Volt (V) |
| [serial]/[1-4]/yieldday | R | Energy converted to AC per day on specific input | Watt hours (Wh) |
| [serial]/[1-4]/yieldtotal | R | Energy converted to AC since reset on specific input | Kilo watt hours (kWh) |
### Inverter limit specific topics
cmd topics are used to set values. Status topics are updated from values set in the inverter.
| Topic | R / W | Description | Value / Unit |
| ----------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
| [serial]/status/limit_relative | R | Current applied production limit of the inverter | % of total possible output |
| [serial]/status/limit_absolute | R | Current applied production limit of the inverter | Watt (W) |
| [serial]/cmd/limit_persistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
| [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. | Watt (W) |
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. The value must be published non-retained, otherwise it will be ignored! | % |
| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will set immediatly within the inverter but show up in the web GUI and limit_relative topic after around 4 minutes. If you are using a already known inverter (known Hardware ID), the updated value will show up within a few seconds. The value must be published non-retained, otherwise it will be ignored! | Watt (W) |
| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 |
| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 |
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/mqtt_topics/>

View File

@ -1,6 +1,6 @@
# Web API
Information in JSON format can be obtained through the web API
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/web_api/>
## List of URLs
@ -8,36 +8,6 @@ This list may be incomplete
| GET/POST | Auth required | URL |
| -------- | --- | -- |
| Get | yes | /api/config/get |
| Post | yes | /api/config/delete |
| Get | yes | /api/config/list |
| Post | yes | /api/config/upload |
| Get+Post | yes | /api/device/config |
| Get | no | /api/devinfo/status |
| Get+Post | yes | /api/dtu/config |
| Get | no | /api/eventlog/status?inv=inverter-serialnumber |
| Post | yes | /api/firmware/update |
| Get | yes | /api/inverter/list |
| Post | yes | /api/inverter/add |
| Post | yes | /api/inverter/del |
| Post | yes | /api/inverter/edit |
| Post | yes | /api/limit/config |
| Get | no | /api/limit/status |
| Get | no | /api/livedata/status |
| Post | yes | /api/maintenance/reboot |
| Get+Post | yes | /api/mqtt/config |
| Get | no | /api/mqtt/status |
| Get+Post | yes | /api/network/config |
| Get | no | /api/network/status |
| Get+Post | yes | /api/ntp/config |
| Get | no | /api/ntp/status |
| Get+Post | yes | /api/ntp/time |
| Get | no | /api/power/status |
| Post | yes | /api/power/config |
| Get | no | /api/prometheus/metrics |
| Get+Post | yes | /api/security/config |
| Get | yes | /api/security/authenticate |
| Get | no | /api/system/status |
| Get | no | /api/vedirectlivedata/status |
| Get | no | /api/vedirect/status |
| Get | no | /api/huawei/status |
@ -47,455 +17,6 @@ This list may be incomplete
| Get | no | /api/battery/status |
| Get | no | /api/powerlimiter/status |
## Examples of Use
### Important notes
- IP addresses and serial numbers in this examples are anonymized. Adjust to your own needs.
- The output from curl is without a linefeed at the end, so please be careful when copying the output - do not accidentally add the shell prompt directly after it.
- When POSTing config data to OpenDTU, always send all settings back, even if only one setting was changed. Sending single settings is not supported and you will receive a response `{"type":"warning","message":"Values are missing!"}`
- When POSTing, always put single quotes around the data part. Do not confuse the single quote `'` with the backtick `` ` ``. You have been warned.
- Some API calls have a single URL for GET and POST - e.g. `/api/ntp/config`
- Other API calls use e.g. `/api/limit/status` to GET data and a different URL `/api/limit/config` to POST data.
- If you want to investigate the web api communication, a good tool is [Postman](https://www.postman.com/)
- Settings API require username and password provided with Basic Authentication credentials
- If you disable the readonly access to the web API, every endpoint requires authentication
### Get information
You can "talk" to the OpenDTU with a command line tool like `curl`. The output is in plain JSON, without carriage return/linefeed and is therefore not very human readable.
#### Get current livedata
```bash
$ curl http://192.168.10.10/api/livedata/status
{"inverters":[{"serial":"11617160xxxx","name":"Meine Solaranlage","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"2":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"3":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0},{"serial":"11417160xxxx","name":"test","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":"test 1"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":"test 2"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0}],"total":{"Power":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":2}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}}
```
To enhance readability (and filter information) use the JSON command line processor `jq`.
```bash
$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq
{
"inverters": [
{
"serial": "116171603546",
"name": "Meine Solaranlage",
"data_age": 7038,
"reachable": false,
"producing": false,
"limit_relative": 0,
"limit_absolute": -1,
"AC": {
"0": {
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"Power DC": {
"v": 0,
"u": "W",
"d": 1
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Frequency": {
"v": 0,
"u": "Hz",
"d": 2
},
"PowerFactor": {
"v": 0,
"u": "",
"d": 3
},
"ReactivePower": {
"v": 0,
"u": "var",
"d": 1
},
"Efficiency": {
"v": 0,
"u": "%",
"d": 3
}
}
},
"DC": {
"0": {
"name": {
"u": ""
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Irradiation": {
"v": 0,
"u": "%",
"d": 3
}
},
"1": {
"name": {
"u": ""
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Irradiation": {
"v": 0,
"u": "%",
"d": 3
}
},
"2": {
"name": {
"u": ""
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Irradiation": {
"v": 0,
"u": "%",
"d": 3
}
},
"3": {
"name": {
"u": ""
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
}
}
},
"INV": {
"0": {
"Temperature": {
"v": 0,
"u": "°C",
"d": 1
}
}
},
"events": 0
},
{
"serial": "114171603548",
"name": "test",
"data_age": 7038,
"reachable": false,
"producing": false,
"limit_relative": 0,
"limit_absolute": -1,
"AC": {
"0": {
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"Power DC": {
"v": 0,
"u": "W",
"d": 1
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Frequency": {
"v": 0,
"u": "Hz",
"d": 2
},
"PowerFactor": {
"v": 0,
"u": "",
"d": 3
},
"ReactivePower": {
"v": 0,
"u": "var",
"d": 1
},
"Efficiency": {
"v": 0,
"u": "%",
"d": 3
}
}
},
"DC": {
"0": {
"name": {
"u": "test 1"
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Irradiation": {
"v": 0,
"u": "%",
"d": 3
}
},
"1": {
"name": {
"u": "test 2"
},
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"Voltage": {
"v": 0,
"u": "V",
"d": 1
},
"Current": {
"v": 0,
"u": "A",
"d": 2
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 3
},
"Irradiation": {
"v": 0,
"u": "%",
"d": 3
}
}
},
"INV": {
"0": {
"Temperature": {
"v": 0,
"u": "°C",
"d": 1
}
}
},
"events": 0
}
],
"total": {
"Power": {
"v": 0,
"u": "W",
"d": 1
},
"YieldDay": {
"v": 0,
"u": "Wh",
"d": 0
},
"YieldTotal": {
"v": 0,
"u": "kWh",
"d": 2
}
},
"hints": {
"time_sync": false,
"radio_problem": false,
"default_password": false
}
}
```
The eventlog can be fetched with the inverter serial number as parameter:
```bash
$ curl --no-progress-meter http://192.168.10.10/api/eventlog/status?inv=11418186xxxx | jq
{
"11418186xxxx": {
"count": 4,
"events": [
{
"message_id": 1,
"message": "Inverter start",
"start_time": 28028,
"end_time": 28028
},
{
"message_id": 209,
"message": "PV-1: No input",
"start_time": 28036,
"end_time": 0
},
{
"message_id": 2,
"message": "DTU command failed",
"start_time": 28092,
"end_time": 28092
},
{
"message_id": 207,
"message": "MPPT-A: Input undervoltage",
"start_time": 28336,
"end_time": 0
}
]
}
}
```
### Victron REST-API (/api/vedirectlivedata/status):
````JSON
{
@ -521,78 +42,3 @@ $ curl --no-progress-meter http://192.168.10.10/api/eventlog/status?inv=11418186
"H23":{"v":737,"u":"W"}
}
````
#### combine curl and jq
`jq` can filter specific fields from json output.
For example, filter out the current total power:
```bash
$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq '.total | .Power.v'
140.7999878
```
#### Get information where login is required
When config data is requested, username and password have to be provided to `curl`
Username is always `admin`, the default password is `openDTU42`. The password is used for both the admin login and the Admin-mode Access Point.
```bash
$ curl --u admin:openDTU42 http://192.168.10.10/api/ntp/config
{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"}
```
### Post information
With HTTP POST commands information can be written to the OpenDTU.
The Web API is designed to allow the web frontend in the web browser to communicate with the OpenDTU software running on the ESP32. It is not designed to be intuitive or user-friendly, so please follow the instructions here.
#### Example 1: change ntp settings
If you want to configure the ntp server setting, first fetch the information from the web API:
```bash
$ curl -u "admin:password" http://192.168.10.10/api/ntp/config
{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"}
```
Then, second step, send your new settings. Use the text output from curl in the first step, add `data=` and enclose the whole data with single quotes.
```bash
$ curl -u "admin:password" http://192.168.10.10/api/ntp/config -d 'data={"ntp_server":"my.own.ntp.server.home","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"}'
{"type":"success","message":"Settings saved!"}
```
You will receive a json formatted response.
#### Example 2: change power limit
In the second example, I want to change the non persistent power limit of an inverter. Again, first fetch current data:
```bash
$ curl http://192.168.10.10/api/limit/status
{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Ok"}}
```
I see data from two configured inverters.
Now I set the relative power limit of inverter with serialnumber `11418180xxxx` to 50%.
```bash
$ curl -u "admin:password" http://192.168.10.10/api/limit/config -d 'data={"serial":"11418180xxxx", "limit_type":1, "limit_value":50}'
{"type":"success","message":"Settings saved!"}
```
Then I read again the limit status. In the first answer the status is `pending`, some seconds later it changed to `OK`.
```bash
$ curl http://192.168.10.10/api/limit/status
{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Pending"}}
...
$ curl http://192.168.10.10/api/limit/status
{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":50,"max_power":800,"limit_set_status":"Ok"}}
```

View File

@ -28,16 +28,6 @@ class BatteryStats {
bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
protected:
template<typename T>
void addLiveViewValue(JsonVariant& root, std::string const& name,
T&& value, std::string const& unit, uint8_t precision) const;
void addLiveViewText(JsonVariant& root, std::string const& name,
std::string const& text) const;
void addLiveViewWarning(JsonVariant& root, std::string const& name,
bool warning) const;
void addLiveViewAlarm(JsonVariant& root, std::string const& name,
bool alarm) const;
String _manufacturer = "unknown";
uint8_t _SoC = 0;
uint32_t _lastUpdateSoC = 0;
@ -138,3 +128,16 @@ class VictronSmartShuntStats : public BatteryStats {
bool _alarmLowTemperature;
bool _alarmHighTemperature;
};
class MqttBatteryStats : public BatteryStats {
public:
// since the source of information was MQTT in the first place,
// we do NOT publish the same data under a different topic.
void mqttPublish() const final { }
// the SoC is the only interesting value in this case, which is already
// displayed at the top of the live view. do not generate a card.
void getLiveViewData(JsonVariant& root) const final { }
void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = _lastUpdate = millis(); }
};

View File

@ -226,6 +226,7 @@ struct CONFIG_T {
uint8_t Provider;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery;
struct {

View File

@ -11,6 +11,8 @@ enum DisplayType_t {
PCD8544,
SSD1306,
SH1106,
SSD1309,
DisplayType_Max,
};
class DisplayGraphicClass {
@ -35,6 +37,7 @@ private:
void printText(const char* text, const uint8_t line);
void calcLineHeights();
void setFont(const uint8_t line);
bool isValidDisplay();
Task _loopTask;

View File

@ -69,7 +69,7 @@
// Wait time/current before shuting down the PSU / charger
// This is set to allow the fan to run for some time
#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 1.0
#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75
// Updateinterval used to request new values from the PSU
#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500

View File

@ -2,12 +2,13 @@
#pragma once
#include <AsyncWebSocket.h>
#include <HardwareSerial.h>
#include <Stream.h>
#include <TaskSchedulerDeclarations.h>
#include <Print.h>
#include <freertos/task.h>
#include <mutex>
#define BUFFER_SIZE 500
#include <vector>
#include <unordered_map>
#include <queue>
class MessageOutputClass : public Print {
public:
@ -21,13 +22,19 @@ private:
Task _loopTask;
using message_t = std::vector<uint8_t>;
// we keep a buffer for every task and only write complete lines to the
// serial output and then move them to be pushed through the websocket.
// this way we prevent mangling of messages from different contexts.
std::unordered_map<TaskHandle_t, message_t> _task_messages;
std::queue<message_t> _lines;
AsyncWebSocket* _ws = nullptr;
char _buffer[BUFFER_SIZE];
uint16_t _buff_pos = 0;
uint32_t _lastSend = 0;
bool _forceSend = false;
std::mutex _msgLock;
void serialWrite(message_t const& m);
};
extern MessageOutputClass MessageOutput;

22
include/MqttBattery.h Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include "Battery.h"
#include <espMqttClient.h>
class MqttBattery : public BatteryProvider {
public:
MqttBattery() = default;
bool init(bool verboseLogging) final;
void deinit() final;
void loop() final { return; } // this class is event-driven
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private:
bool _verboseLogging = false;
String _socTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
void onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};

View File

@ -5,6 +5,9 @@
#include <Huawei_can.h>
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandleHuaweiClass {
public:
@ -12,13 +15,30 @@ public:
private:
void loop();
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
enum class Topic : unsigned {
LimitOnlineVoltage,
LimitOnlineCurrent,
LimitOfflineVoltage,
LimitOfflineCurrent,
Mode
};
void onMqttMessage(Topic t,
const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len,
size_t index, size_t total);
Task _loopTask;
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandleHuaweiClass MqttHandleHuawei;

View File

@ -4,6 +4,9 @@
#include "Configuration.h"
#include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandlePowerLimiterClass {
public:
@ -18,6 +21,11 @@ private:
uint32_t _lastPublishStats;
uint32_t _lastPublish;
// MQTT callbacks to process updates on subscribed topics are executed in
// the MQTT thread's context. we use this queue to switch processing the
// user requests into the main loop's context (TaskScheduler context).
mutable std::mutex _mqttMutex;
std::deque<std::function<void()>> _mqttCallbacks;
};
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;

View File

@ -9,13 +9,16 @@
#include <frozen/string.h>
const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> GridProfileParser::_profileTypes = { {
{ 0x02, 0x00, "no data (yet)" },
{ 0x03, 0x00, "Germany - DE_VDE4105_2018" },
{ 0x0a, 0x00, "European - EN 50549-1:2019" },
{ 0x0c, 0x00, "AT Tor - EU_EN50438" },
{ 0x0d, 0x04, "France" },
{ 0x12, 0x00, "Poland - EU_EN50438" },
{ 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" },
{ 0x02, 0x00, "US - NA_IEEE1547_240V" },
{ 0x03, 0x00, "DE - DE_VDE4105_2018" },
{ 0x03, 0x01, "XX - unknown" },
{ 0x0a, 0x00, "XX - EN 50549-1:2019" },
{ 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" },
{ 0x0d, 0x04, "FR -" },
{ 0x10, 0x00, "ES - ES_RD1699" },
{ 0x12, 0x00, "PL - EU_EN50438" },
{ 0x29, 0x00, "NL - NL_NEN-EN50549-1_2019" },
{ 0x37, 0x00, "CH - CH_NA EEA-NE7-CH2020" },
} };
constexpr frozen::map<uint8_t, frozen::string, 12> profileSection = {
@ -45,19 +48,19 @@ constexpr GridProfileItemDefinition_t make_value(frozen::string Name, frozen::st
return v;
}
constexpr frozen::map<uint8_t, GridProfileItemDefinition_t, 0x39> itemDefinitions = {
constexpr frozen::map<uint8_t, GridProfileItemDefinition_t, 0x42> itemDefinitions = {
{ 0x01, make_value("Nominale Voltage (NV)", "V", 10) },
{ 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) },
{ 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) },
{ 0x04, make_value("High Voltage 1 (HV1)", "V", 10) },
{ 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) },
{ 0x06, make_value("Low Voltage 2 (LV2)", "V", 10) },
{ 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 10) },
{ 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 100) },
{ 0x08, make_value("High Voltage 2 (HV2)", "V", 10) },
{ 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 10) },
{ 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 100) },
{ 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) },
{ 0x0b, make_value("High Voltage 3 (HV3)", "V", 10) },
{ 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 10) },
{ 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 100) },
{ 0x0d, make_value("Nominal Frequency", "Hz", 100) },
{ 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) },
{ 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) },
@ -94,7 +97,7 @@ constexpr frozen::map<uint8_t, GridProfileItemDefinition_t, 0x39> itemDefinition
{ 0x2e, make_value("Voltage Set Point V3", "V", 10) },
{ 0x2f, make_value("Voltage Set Point V4", "V", 10) },
{ 0x30, make_value("Reactive Set Point Q4", "%Pn", 10) },
{ 0x31, make_value("Setting Time (Tr)", "s", 10) },
{ 0x31, make_value("VV Setting Time (Tr)", "s", 10) },
{ 0x32, make_value("SPF Function Activated", "bool", 1) },
{ 0x33, make_value("Power Factor (PF)", "", 100) },
{ 0x34, make_value("RPC Function Activated", "bool", 1) },
@ -102,6 +105,15 @@ constexpr frozen::map<uint8_t, GridProfileItemDefinition_t, 0x39> itemDefinition
{ 0x36, make_value("WPF Function Activated", "bool", 1) },
{ 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) },
{ 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) },
{ 0x39, make_value("Low Voltage 3 (LV3)", "V", 10) },
{ 0x3a, make_value("LV3 Maximum Trip Time (MTT)", "s", 100) },
{ 0x3b, make_value("Momentary Cessition Low Voltage", "V", 10) },
{ 0x3c, make_value("Momentary Cessition High Voltage", "V", 10) },
{ 0x3d, make_value("FW Settling Time (Tr)", "s", 10) },
{ 0x3e, make_value("LF2 Maximum Trip Time (MTT)", "s", 100) },
{ 0x3f, make_value("HF2 Maximum Trip time (MTT)", "s", 100) },
{ 0x40, make_value("Short Interruption Reconnect Time (SRT)", "s", 10) },
{ 0x41, make_value("Short Interruption Time (SIT)", "s", 10) },
{ 0xff, make_value("Unkown Value", "", 1) },
};
@ -114,6 +126,24 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x00, 0x00, 0x04 },
{ 0x00, 0x00, 0x05 },
// Version 0x01
{ 0x00, 0x01, 0x01 },
{ 0x00, 0x01, 0x02 },
{ 0x00, 0x01, 0x03 },
{ 0x00, 0x01, 0x04 },
{ 0x00, 0x01, 0x05 },
{ 0x00, 0x01, 0x08 },
{ 0x00, 0x01, 0x09 },
// Version 0x02
{ 0x00, 0x02, 0x01 },
{ 0x00, 0x02, 0x02 },
{ 0x00, 0x02, 0x03 },
{ 0x00, 0x02, 0x04 },
{ 0x00, 0x02, 0x05 },
{ 0x00, 0x02, 0x06 },
{ 0x00, 0x02, 0x07 },
// Version 0x03
{ 0x00, 0x03, 0x01 },
{ 0x00, 0x03, 0x02 },
@ -178,10 +208,10 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x00, 0x35, 0x07 },
{ 0x00, 0x35, 0x08 },
{ 0x00, 0x35, 0x09 },
{ 0x00, 0x35, 0xff },
{ 0x00, 0x35, 0xff },
{ 0x00, 0x35, 0xff },
{ 0x00, 0x35, 0xff },
{ 0x00, 0x35, 0x39 },
{ 0x00, 0x35, 0x3a },
{ 0x00, 0x35, 0x3b },
{ 0x00, 0x35, 0x3c },
// Frequency (H/LFRT)
// Version 0x00
@ -198,9 +228,9 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x10, 0x03, 0x10 },
{ 0x10, 0x03, 0x11 },
{ 0x10, 0x03, 0x12 },
{ 0x10, 0x03, 0x13 },
{ 0x10, 0x03, 0x3e },
{ 0x10, 0x03, 0x14 },
{ 0x10, 0x03, 0x15 },
{ 0x10, 0x03, 0x3f },
// Island Detection (ID)
// Version 0x00
@ -220,8 +250,8 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x30, 0x07, 0x19 },
{ 0x30, 0x07, 0x1a },
{ 0x30, 0x07, 0x1b },
{ 0x30, 0x07, 0xff },
{ 0x30, 0x07, 0xff },
{ 0x30, 0x07, 0x40 },
{ 0x30, 0x07, 0x41 },
// Ramp Rates (RR)
// Version 0x00
@ -255,7 +285,7 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x50, 0x11, 0x1f },
{ 0x50, 0x11, 0x20 },
{ 0x50, 0x11, 0x21 },
{ 0x50, 0x11, 0x22 },
{ 0x50, 0x11, 0x3d },
// Volt Watt (VW)
// Version 0x00

View File

@ -4,8 +4,8 @@
#include <list>
#define GRID_PROFILE_SIZE 141
#define PROFILE_TYPE_COUNT 7
#define SECTION_VALUE_COUNT 144
#define PROFILE_TYPE_COUNT 10
#define SECTION_VALUE_COUNT 158
typedef struct {
uint8_t lIdx;

View File

@ -19,12 +19,16 @@ extra_configs =
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
framework = arduino
platform = espressif32@6.3.2
platform = espressif32@6.5.0
build_flags =
-DPIOENV=\"$PIOENV\"
-D_TASK_STD_FUNCTION=1
-Wall -Wextra -Werror -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and
; https://github.com/espressif/arduino-esp32/issues/9045
; -Werror
-std=c++17
-std=gnu++17
build_unflags =
@ -35,7 +39,7 @@ lib_deps =
bblanchon/ArduinoJson @ ^6.21.4
https://github.com/bertmelis/espMqttClient.git#v1.5.0
nrf24/RF24 @ ^1.4.8
olikraus/U8g2 @ ^2.35.8
olikraus/U8g2 @ ^2.35.9
buelowp/sunset @ ^1.1.7
https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/arkhipenko/TaskScheduler#testing

View File

@ -5,6 +5,7 @@
#include "PylontechCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
BatteryClass Battery;
@ -26,13 +27,14 @@ void BatteryClass::init(Scheduler& scheduler)
_loopTask.setCallback(std::bind(&BatteryClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
std::lock_guard<std::mutex> lock(_mutex);
this->updateSettings();
}
void BatteryClass::updateSettings()
{
std::lock_guard<std::mutex> lock(_mutex);
if (_upProvider) {
_upProvider->deinit();
_upProvider = nullptr;
@ -52,6 +54,10 @@ void BatteryClass::updateSettings()
_upProvider = std::make_unique<JkBms::Controller>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 2:
_upProvider = std::make_unique<MqttBattery>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
@ -62,7 +68,6 @@ void BatteryClass::updateSettings()
}
}
void BatteryClass::loop()
{
std::lock_guard<std::mutex> lock(_mutex);

View File

@ -7,30 +7,44 @@
#include "JkBmsDataPoints.h"
template<typename T>
void BatteryStats::addLiveViewValue(JsonVariant& root, std::string const& name,
T&& value, std::string const& unit, uint8_t precision) const
static void addLiveViewInSection(JsonVariant& root,
std::string const& section, std::string const& name,
T&& value, std::string const& unit, uint8_t precision)
{
auto jsonValue = root["values"][name];
auto jsonValue = root["values"][section][name];
jsonValue["v"] = value;
jsonValue["u"] = unit;
jsonValue["d"] = precision;
}
void BatteryStats::addLiveViewText(JsonVariant& root, std::string const& name,
std::string const& text) const
template<typename T>
static void addLiveViewValue(JsonVariant& root, std::string const& name,
T&& value, std::string const& unit, uint8_t precision)
{
root["values"][name] = text;
addLiveViewInSection(root, "status", name, value, unit, precision);
}
void BatteryStats::addLiveViewWarning(JsonVariant& root, std::string const& name,
bool warning) const
static void addLiveViewTextInSection(JsonVariant& root,
std::string const& section, std::string const& name, std::string const& text)
{
root["values"][section][name] = text;
}
static void addLiveViewTextValue(JsonVariant& root, std::string const& name,
std::string const& text)
{
addLiveViewTextInSection(root, "status", name, text);
}
static void addLiveViewWarning(JsonVariant& root, std::string const& name,
bool warning)
{
if (!warning) { return; }
root["issues"][name] = 1;
}
void BatteryStats::addLiveViewAlarm(JsonVariant& root, std::string const& name,
bool alarm) const
static void addLiveViewAlarm(JsonVariant& root, std::string const& name,
bool alarm)
{
if (!alarm) { return; }
root["issues"][name] = 2;
@ -57,9 +71,9 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
addLiveViewText(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
addLiveViewText(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no"));
addLiveViewText(root, "chargeImmediately", (_chargeImmediately?"yes":"no"));
addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no"));
// alarms and warnings go into the "Issues" card of the web application
addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge);
@ -108,38 +122,11 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
addLiveViewValue(root, "power", current * voltage , "W", 2);
}
if (verbose) {
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperatureOne.has_value()) {
addLiveViewValue(root, "batOneTemp", *oTemperatureOne, "°C", 0);
}
}
if (verbose) {
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
if (oTemperatureTwo.has_value()) {
addLiveViewValue(root, "batTwoTemp", *oTemperatureTwo, "°C", 0);
}
}
auto oTemperatureBms = _dataPoints.get<Label::BmsTempCelsius>();
if (oTemperatureBms.has_value()) {
addLiveViewValue(root, "bmsTemp", *oTemperatureBms, "°C", 0);
}
if (_cellVoltageTimestamp > 0) {
if (verbose) {
addLiveViewValue(root, "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
}
addLiveViewValue(root, "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);
if (verbose) {
addLiveViewValue(root, "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewValue(root, "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
}
}
// labels BatteryChargeEnabled, BatteryDischargeEnabled, and
// BalancingEnabled refer to the user setting. we want to show the
// actual MOSFETs' state which control whether charging and discharging
@ -148,11 +135,32 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
if (oStatus.has_value()) {
using Bits = JkBms::StatusBits;
auto chargeEnabled = *oStatus & static_cast<uint16_t>(Bits::ChargingActive);
addLiveViewText(root, "chargeEnabled", (chargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "chargeEnabled", (chargeEnabled?"yes":"no"));
auto dischargeEnabled = *oStatus & static_cast<uint16_t>(Bits::DischargingActive);
addLiveViewText(root, "dischargeEnabled", (dischargeEnabled?"yes":"no"));
addLiveViewTextValue(root, "dischargeEnabled", (dischargeEnabled?"yes":"no"));
}
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperatureOne.has_value()) {
addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0);
}
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
if (oTemperatureTwo.has_value()) {
addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0);
}
if (_cellVoltageTimestamp > 0) {
addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0);
}
if (oStatus.has_value()) {
using Bits = JkBms::StatusBits;
auto balancingActive = *oStatus & static_cast<uint16_t>(Bits::BalancingActive);
addLiveViewText(root, "balancingActive", (balancingActive?"yes":"no"));
addLiveViewTextInSection(root, "cells", "balancingActive", (balancingActive?"yes":"no"));
}
auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();

View File

@ -199,6 +199,7 @@ bool ConfigurationClass::write()
battery["provider"] = config.Battery.Provider;
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttTopic;
JsonObject huawei = doc.createNestedObject("huawei");
huawei["enabled"] = config.Huawei.Enabled;
@ -435,6 +436,7 @@ bool ConfigurationClass::read()
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
strlcpy(config.Battery.MqttTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttTopic));
JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;

View File

@ -12,6 +12,7 @@ std::map<DisplayType_t, std::function<U8G2*(uint8_t, uint8_t, uint8_t, uint8_t)>
{ DisplayType_t::PCD8544, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_PCD8544_84X48_F_4W_HW_SPI(U8G2_R0, cs, data, reset); } },
{ DisplayType_t::SSD1306, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } },
{ DisplayType_t::SH1106, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } },
{ DisplayType_t::SSD1309, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(U8G2_R0, reset, clock, data); } },
};
// Language defintion, respect order in languages[] and translation lists
@ -45,20 +46,20 @@ DisplayGraphicClass::~DisplayGraphicClass()
void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset)
{
_display_type = type;
if (_display_type > DisplayType_t::None) {
if (isValidDisplay()) {
auto constructor = display_types[_display_type];
_display = constructor(reset, clk, data, cs);
_display->begin();
setContrast(DISPLAY_CONTRAST);
setStatus(true);
_diagram.init(scheduler, _display);
}
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.setInterval(_period);
_loopTask.enable();
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.setInterval(_period);
_loopTask.enable();
}
}
void DisplayGraphicClass::calcLineHeights()
@ -86,6 +87,11 @@ void DisplayGraphicClass::setFont(const uint8_t line)
}
}
bool DisplayGraphicClass::isValidDisplay()
{
return _display_type > DisplayType_t::None && _display_type < DisplayType_Max;
}
void DisplayGraphicClass::printText(const char* text, const uint8_t line)
{
uint8_t dispX;
@ -102,7 +108,7 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line)
void DisplayGraphicClass::setOrientation(const uint8_t rotation)
{
if (_display_type == DisplayType_t::None) {
if (!isValidDisplay()) {
return;
}
@ -132,7 +138,7 @@ void DisplayGraphicClass::setLanguage(const uint8_t language)
void DisplayGraphicClass::setStartupDisplay()
{
if (_display_type == DisplayType_t::None) {
if (!isValidDisplay()) {
return;
}
@ -148,10 +154,6 @@ DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram()
void DisplayGraphicClass::loop()
{
if (_display_type == DisplayType_t::None) {
return;
}
_loopTask.setInterval(_period);
_display->clearBuffer();
@ -215,7 +217,7 @@ void DisplayGraphicClass::loop()
void DisplayGraphicClass::setContrast(const uint8_t contrast)
{
if (_display_type == DisplayType_t::None) {
if (!isValidDisplay()) {
return;
}
_display->setContrast(contrast * 2.55f);

View File

@ -62,20 +62,28 @@ void DisplayGraphicDiagramClass::updatePeriod()
void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX)
{
const uint8_t graphPosX = DIAG_POSX + ((screenSaverOffsetX > 3) ? 1 : 0); // screenSaverOffsetX expected to be in range 0..6
// screenSaverOffsetX expected to be in range 0..6
const uint8_t graphPosX = DIAG_POSX + ((screenSaverOffsetX > 3) ? 1 : 0);
const uint8_t graphPosY = DIAG_POSY + ((screenSaverOffsetX > 3) ? 1 : 0);
const uint8_t horizontal_line_y = graphPosY + CHART_HEIGHT - 1;
const uint8_t arrow_size = 2;
// draw diagram axis
_display->drawVLine(graphPosX, graphPosY, CHART_HEIGHT);
_display->drawHLine(graphPosX, graphPosY + CHART_HEIGHT - 1, CHART_WIDTH);
_display->drawHLine(graphPosX, horizontal_line_y, CHART_WIDTH);
_display->drawLine(graphPosX + 1, graphPosY + 1, graphPosX + 2, graphPosY + 2); // UP-arrow
_display->drawLine(graphPosX - 2, graphPosY + 2, graphPosX - 1, graphPosY + 1); // UP-arrow
_display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT - 3, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT - 2); // LEFT-arrow
_display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT + 1, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT); // LEFT-arrow
// UP-arrow
_display->drawLine(graphPosX, graphPosY, graphPosX + arrow_size, graphPosY + arrow_size);
_display->drawLine(graphPosX, graphPosY, graphPosX - arrow_size, graphPosY + arrow_size);
// LEFT-arrow
_display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y - arrow_size);
_display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y + arrow_size);
// draw AC value
_display->setFont(u8g2_font_tom_thumb_4x6_mr); // 4 pixels per char
// 4 pixels per char
_display->setFont(u8g2_font_tom_thumb_4x6_mr);
char fmtText[7];
const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end());
if (maxWatts > 999) {
@ -84,25 +92,24 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX)
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts));
}
const uint8_t textLength = strlen(fmtText);
const uint8_t space_and_arrow_pixels = 2;
_display->drawStr(graphPosX - space_and_arrow_pixels - (textLength * 4), graphPosY + 5, fmtText);
_display->drawStr(graphPosX - arrow_size - textLength * 4, graphPosY + 5, fmtText);
// draw chart
const float scaleFactor = maxWatts / CHART_HEIGHT;
uint8_t axisTick = 1;
for (int i = 0; i < _graphValuesCount; i++) {
if (scaleFactor > 0) {
if (i == 0) {
_display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); // + 0.5 to round mathematical
} else {
_display->drawLine(graphPosX + i, graphPosY + CHART_HEIGHT - ((_graphValues[i - 1] / scaleFactor) + 0.5), graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5));
}
}
for (uint8_t i = 1; i < _graphValuesCount; i++) {
// draw one tick per hour to the x-axis
if (i * getSecondsPerDot() > (3600u * axisTick)) {
_display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT);
axisTick++;
}
if (scaleFactor == 0) {
continue;
}
_display->drawLine(
graphPosX + i - 1, horizontal_line_y - std::max<int16_t>(0, _graphValues[i - 1] / scaleFactor - 0.5),
graphPosX + i, horizontal_line_y - std::max<int16_t>(0, _graphValues[i] / scaleFactor - 0.5));
}
}

View File

@ -2,10 +2,9 @@
/*
* Copyright (C) 2022-2023 Thomas Basler and others
*/
#include <HardwareSerial.h>
#include "MessageOutput.h"
#include <Arduino.h>
MessageOutputClass MessageOutput;
void MessageOutputClass::init(Scheduler& scheduler)
@ -18,46 +17,97 @@ void MessageOutputClass::init(Scheduler& scheduler)
void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
{
std::lock_guard<std::mutex> lock(_msgLock);
_ws = output;
}
void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m)
{
// on ESP32-S3, Serial.flush() blocks until a serial console is attached.
// operator bool() of HWCDC returns false if the device is not attached to
// a USB host. in general it makes sense to skip writing entirely if the
// default serial port is not ready.
if (!Serial) { return; }
size_t written = 0;
while (written < m.size()) {
written += Serial.write(m.data() + written, m.size() - written);
}
Serial.flush();
}
size_t MessageOutputClass::write(uint8_t c)
{
if (_buff_pos < BUFFER_SIZE) {
std::lock_guard<std::mutex> lock(_msgLock);
_buffer[_buff_pos] = c;
_buff_pos++;
} else {
_forceSend = true;
std::lock_guard<std::mutex> lock(_msgLock);
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
auto iter = res.first;
auto& message = iter->second;
message.push_back(c);
if (c == '\n') {
serialWrite(message);
_lines.emplace(std::move(message));
_task_messages.erase(iter);
}
return Serial.write(c);
return 1;
}
size_t MessageOutputClass::write(const uint8_t* buffer, size_t size)
size_t MessageOutputClass::write(const uint8_t *buffer, size_t size)
{
std::lock_guard<std::mutex> lock(_msgLock);
if (_buff_pos + size < BUFFER_SIZE) {
memcpy(&_buffer[_buff_pos], buffer, size);
_buff_pos += size;
}
_forceSend = true;
return Serial.write(buffer, size);
auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
auto iter = res.first;
auto& message = iter->second;
message.reserve(message.size() + size);
for (size_t idx = 0; idx < size; ++idx) {
uint8_t c = buffer[idx];
message.push_back(c);
if (c == '\n') {
serialWrite(message);
_lines.emplace(std::move(message));
message.clear();
message.reserve(size - idx - 1);
}
}
if (message.empty()) { _task_messages.erase(iter); }
return size;
}
void MessageOutputClass::loop()
{
// Send data via websocket if either time is over or buffer is full
if (_forceSend || (millis() - _lastSend > 1000)) {
std::lock_guard<std::mutex> lock(_msgLock);
if (_ws && _buff_pos > 0) {
_ws->textAll(_buffer, _buff_pos);
_buff_pos = 0;
std::lock_guard<std::mutex> lock(_msgLock);
// clean up (possibly filled) buffers of deleted tasks
auto map_iter = _task_messages.begin();
while (map_iter != _task_messages.end()) {
if (eTaskGetState(map_iter->first) == eDeleted) {
map_iter = _task_messages.erase(map_iter);
continue;
}
if (_forceSend) {
_buff_pos = 0;
++map_iter;
}
if (!_ws) {
while (!_lines.empty()) {
_lines.pop(); // do not hog memory
}
_forceSend = false;
return;
}
while (!_lines.empty() && _ws->availableForWriteAll()) {
_ws->textAll(std::make_shared<message_t>(std::move(_lines.front())));
_lines.pop();
}
}

65
src/MqttBattery.cpp Normal file
View File

@ -0,0 +1,65 @@
#include <functional>
#include "Configuration.h"
#include "MqttBattery.h"
#include "MqttSettings.h"
#include "MessageOutput.h"
bool MqttBattery::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;
auto const& config = Configuration.get();
_socTopic = config.Battery.MqttTopic;
if (_socTopic.isEmpty()) { return false; }
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
std::bind(&MqttBattery::onMqttMessage,
this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6)
);
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Subscribed to '%s'\r\n",
_socTopic.c_str());
}
return true;
}
void MqttBattery::deinit()
{
if (_socTopic.isEmpty()) { return; }
MqttSettings.unsubscribe(_socTopic);
}
void MqttBattery::onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
float soc = 0;
std::string value(reinterpret_cast<const char*>(payload), len);
try {
soc = std::stof(value);
}
catch(std::invalid_argument const& e) {
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
value.c_str(), topic);
return;
}
if (soc < 0 || soc > 100) {
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
soc, topic);
return;
}
_stats->setSoC(static_cast<uint8_t>(soc));
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
static_cast<uint8_t>(soc), topic);
}
}

View File

@ -10,12 +10,6 @@
#include "WebApi_Huawei.h"
#include <ctime>
#define TOPIC_SUB_LIMIT_ONLINE_VOLTAGE "limit_online_voltage"
#define TOPIC_SUB_LIMIT_ONLINE_CURRENT "limit_online_current"
#define TOPIC_SUB_LIMIT_OFFLINE_VOLTAGE "limit_offline_voltage"
#define TOPIC_SUB_LIMIT_OFFLINE_CURRENT "limit_offline_current"
#define TOPIC_SUB_MODE "mode"
MqttHandleHuaweiClass MqttHandleHuawei;
void MqttHandleHuaweiClass::init(Scheduler& scheduler)
@ -25,19 +19,22 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler)
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
String const& prefix = MqttSettings.getPrefix();
String topic = MqttSettings.getPrefix();
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_ONLINE_VOLTAGE).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_ONLINE_CURRENT).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_OFFLINE_VOLTAGE).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_OFFLINE_CURRENT).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_MODE).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
String fullTopic(prefix + "huawei/cmd/" + subTopic);
MqttSettings.subscribe(fullTopic.c_str(), 0,
std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6));
};
subscribe("limit_online_voltage", Topic::LimitOnlineVoltage);
subscribe("limit_online_current", Topic::LimitOnlineCurrent);
subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage);
subscribe("limit_offline_current", Topic::LimitOfflineCurrent);
subscribe("mode", Topic::Mode);
_lastPublish = millis();
@ -46,13 +43,21 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler)
void MqttHandleHuaweiClass::loop()
{
if (!MqttSettings.getConnected() ) {
const CONFIG_T& config = Configuration.get();
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
if (!config.Huawei.Enabled) {
_mqttCallbacks.clear();
return;
}
const CONFIG_T& config = Configuration.get();
for (auto& callback : _mqttCallbacks) { callback(); }
_mqttCallbacks.clear();
if (!config.Huawei.Enabled) {
mqttLock.unlock();
if (!MqttSettings.getConnected() ) {
return;
}
@ -78,76 +83,79 @@ void MqttHandleHuaweiClass::loop()
}
void MqttHandleHuaweiClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
void MqttHandleHuaweiClass::onMqttMessage(Topic t,
const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len,
size_t index, size_t total)
{
const CONFIG_T& config = Configuration.get();
// ignore messages if Huawei is disabled
if (!config.Huawei.Enabled) {
std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
try {
payload_val = std::stof(strValue);
}
catch (std::invalid_argument const& e) {
MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
topic, strValue.c_str());
return;
}
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
char* setting;
char* rest = &token_topic[strlen(config.Mqtt.Topic)];
switch (t) {
case Topic::LimitOnlineVoltage:
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE));
break;
strtok_r(rest, "/", &rest); // Remove "huawei"
strtok_r(rest, "/", &rest); // Remove "cmd"
case Topic::LimitOfflineVoltage:
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
break;
setting = strtok_r(rest, "/", &rest);
case Topic::LimitOnlineCurrent:
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
break;
if (setting == NULL) {
return;
}
case Topic::LimitOfflineCurrent:
MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT));
break;
char* strlimit = new char[len + 1];
memcpy(strlimit, payload, len);
strlimit[len] = '\0';
float payload_val = strtof(strlimit, NULL);
delete[] strlimit;
case Topic::Mode:
switch (static_cast<int>(payload_val)) {
case 3:
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control");
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
&HuaweiCan, HUAWEI_MODE_AUTO_INT));
break;
if (!strcmp(setting, TOPIC_SUB_LIMIT_ONLINE_VOLTAGE)) {
// Set voltage limit
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
HuaweiCan.setValue(payload_val, HUAWEI_ONLINE_VOLTAGE);
case 2:
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit");
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
&HuaweiCan, HUAWEI_MODE_AUTO_EXT));
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_OFFLINE_VOLTAGE)) {
// Set current limit
MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
HuaweiCan.setValue(payload_val, HUAWEI_OFFLINE_VOLTAGE);
case 1:
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON");
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
&HuaweiCan, HUAWEI_MODE_ON));
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_ONLINE_CURRENT)) {
// Set current limit
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
HuaweiCan.setValue(payload_val, HUAWEI_ONLINE_CURRENT);
case 0:
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF");
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode,
&HuaweiCan, HUAWEI_MODE_OFF));
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_OFFLINE_CURRENT)) {
// Set current limit
MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val);
HuaweiCan.setValue(payload_val, HUAWEI_OFFLINE_CURRENT);
} else if (!strcmp(setting, TOPIC_SUB_MODE)) {
// Control power on/off
if(payload_val == 3) {
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control");
HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT);
}
if(payload_val == 2) {
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit");
HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT);
}
if(payload_val == 1) {
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON");
HuaweiCan.setMode(HUAWEI_MODE_ON);
}
if(payload_val == 0) {
MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF");
HuaweiCan.setMode(HUAWEI_MODE_OFF);
}
default:
MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val);
break;
}
break;
}
}

View File

@ -34,11 +34,21 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
void MqttHandlePowerLimiterClass::loop()
{
if (!MqttSettings.getConnected() ) {
std::unique_lock<std::mutex> mqttLock(_mqttMutex);
const CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter.Enabled) {
_mqttCallbacks.clear();
return;
}
const CONFIG_T& config = Configuration.get();
for (auto& callback : _mqttCallbacks) { callback(); }
_mqttCallbacks.clear();
mqttLock.unlock();
if (!MqttSettings.getConnected() ) { return; }
if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
auto val = static_cast<unsigned>(PowerLimiter.getMode());
@ -53,13 +63,6 @@ void MqttHandlePowerLimiterClass::loop()
void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
const CONFIG_T& config = Configuration.get();
// ignore messages if PowerLimiter is disabled
if (!config.PowerLimiter.Enabled) {
return;
}
std::string strValue(reinterpret_cast<const char*>(payload), len);
int intValue = -1;
try {
@ -71,19 +74,24 @@ void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessagePro
return;
}
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
using Mode = PowerLimiterClass::Mode;
switch (static_cast<Mode>(intValue)) {
case Mode::UnconditionalFullSolarPassthrough:
MessageOutput.println("Power limiter unconditional full solar PT");
PowerLimiter.setMode(Mode::UnconditionalFullSolarPassthrough);
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
break;
case Mode::Disabled:
MessageOutput.println("Power limiter disabled (override)");
PowerLimiter.setMode(Mode::Disabled);
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Disabled));
break;
case Mode::Normal:
MessageOutput.println("Power limiter normal operation");
PowerLimiter.setMode(Mode::Normal);
_mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Normal));
break;
default:
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);

View File

@ -43,6 +43,7 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
root[F("provider")] = config.Battery.Provider;
root[F("jkbms_interface")] = config.Battery.JkBmsInterface;
root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval;
root[F("mqtt_topic")] = config.Battery.MqttTopic;
response->setLength();
request->send(response);
@ -106,6 +107,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
config.Battery.Provider = root[F("provider")].as<uint8_t>();
config.Battery.JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>();
config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].as<uint8_t>();
strlcpy(config.Battery.MqttTopic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Battery.MqttTopic));
Configuration.write();
retMsg[F("type")] = F("success");

View File

@ -68,6 +68,14 @@ void WebApiWsLiveClass::loop()
try {
std::lock_guard<std::mutex> lock(_mutex);
DynamicJsonDocument root(4200 * INV_MAX_COUNT); // TODO(helge) check if this calculation is correct
// TODO(helge) temporary dump of memory usage if allocation of DynamicJsonDocument fails (will be fixed in upstream repo)
if (root.capacity() == 0) {
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: Alloc memory for DynamicJsonDocument failed (FreeHeap = %d, MaxAllocHeap = %d, MinFreeHeap = %d).\r\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
_lastWsPublish = millis();
return;
}
JsonVariant var = root;
generateJsonResponse(var);

View File

@ -18,7 +18,7 @@
"mitt": "^3.0.1",
"sortablejs": "^1.15.1",
"spark-md5": "^3.0.2",
"vue": "^3.3.13",
"vue": "^3.4.3",
"vue-i18n": "^9.8.0",
"vue-router": "^4.2.5"
},
@ -27,21 +27,21 @@
"@rushstack/eslint-patch": "^1.6.1",
"@tsconfig/node18": "^18.2.2",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.10.5",
"@types/node": "^20.10.6",
"@types/sortablejs": "^1.15.7",
"@types/spark-md5": "^3.0.4",
"@vitejs/plugin-vue": "^4.5.2",
"@vitejs/plugin-vue": "^5.0.2",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5",
"sass": "^1.69.5",
"sass": "^1.69.6",
"terser": "^5.26.0",
"typescript": "^5.3.3",
"vite": "^5.0.10",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.3.1",
"vue-tsc": "^1.8.26"
"vue-tsc": "^1.8.27"
}
}

View File

@ -5,7 +5,7 @@
</div>
</div>
<div v-else>
<div v-else-if="'values' in batteryData"> <!-- suppress the card for MQTT battery provider -->
<div class="row gy-3">
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card">
@ -27,9 +27,9 @@
<div class="card-body">
<div class="row flex-row flex-wrap align-items-start g-3">
<div class="col order-0">
<div v-for="(values, section) in batteryData.values" v-bind:key="section" class="col order-0">
<div class="card" :class="{ 'border-info': true }">
<div class="card-header text-bg-info">{{ $t('battery.Status') }}</div>
<div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
@ -40,7 +40,7 @@
</tr>
</thead>
<tbody>
<tr v-for="(prop, key) in batteryData.values" v-bind:key="key">
<tr v-for="(prop, key) in values" v-bind:key="key">
<th scope="row">{{ $t('battery.' + key) }}</th>
<td style="text-align: right">
<template v-if="typeof prop === 'string'">

View File

@ -31,6 +31,7 @@
"Yes": "Ja",
"No": "Nein",
"VerboseLogging": "Ausführliche Protokollierung",
"Seconds": "Sekunden",
"Loading": "Lade...",
"Reload": "Aktualisieren",
"Cancel": "Abbrechen",
@ -519,8 +520,7 @@
"EnableVedirect": "Aktiviere VE.Direct",
"VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden",
"Save": "@:dtuadmin.Save"
"UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden"
},
"powermeteradmin":{
"PowerMeterSettings": "Stromzähler Einstellungen",
@ -541,7 +541,6 @@
"SDM": "SDM-Stromzähler Konfiguration",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Adresse",
"Save": "@:dtuadmin.Save",
"HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration",
"httpIndividualRequests": "Individuelle HTTP requests pro Phase",
"httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)! Beispiele gibt es unten.",
@ -596,8 +595,7 @@
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht normalerweise die Spannung etwas ein. Damit nicht vorzeitig der Wechelrichter ausgeschaltet wird sobald der \"Stop\"-Schwellenwert erreicht wird, wird der hier angegebene Korrekturfaktor mit einberechnet. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
"InverterRestart": "Wechselrichter Neustart",
"InverterRestartHour": "Stunde für Neustart",
"InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen.",
"Save": "@:dtuadmin.Save"
"InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen."
},
"batteryadmin": {
"BatterySettings": "Batterie Einstellungen",
@ -607,14 +605,16 @@
"Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "State of Charge (SoC) Wert aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"MqttConfiguration": "MQTT Einstellungen",
"MqttTopic": "SoC-Wert Topic",
"JkBmsConfiguration": "JK BMS Einstellungen",
"JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
"PollingInterval": "Abfrageintervall",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"inverteradmin": {
"InverterSettings": "Wechselrichter Einstellungen",
@ -800,14 +800,13 @@
"enableVoltageLimitHint": "Die automatische Leistungssteuerung wird deaktiviert wenn die Ausgangsspannung über diesem Wert liegt und wenn gleichzeitig die Ausgangsleistung unter die minimale Leistung fällt.\nDie automatische Leistungssteuerung wird re-aktiveiert wenn die Batteriespannung unter diesen Wert fällt.",
"lowerPowerLimit": "Minimale Leistung",
"upperPowerLimit": "Maximale Leistung",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"battery": {
"battery": "Batterie",
"DataAge": "letzte Aktualisierung: ",
"Seconds": "vor {val} Sekunden",
"Status": "Status",
"status": "Status",
"Property": "Eigenschaft",
"yes": "@:base.Yes",
"no": "@:base.No",
@ -825,9 +824,15 @@
"dischargeCurrentLimitation": "Entladestromlimit",
"chargeEnabled": "Laden ermöglicht",
"dischargeEnabled": "Entladen ermöglicht",
"balancingActive": "Ausgleichen aktiv",
"chargeImmediately": "Sofortiges Laden angefordert",
"cells": "Zellen",
"batOneTemp": "Batterietemperatur 1",
"batTwoTemp": "Batterietemperatur 2",
"cellMinVoltage": "Kleinste Zellspannung",
"cellAvgVoltage": "Durchschnittliche Zellspannung",
"cellMaxVoltage": "Höchste Zellspannung",
"cellDiffVoltage": "Zellspannungsdifferenz",
"balancingActive": "Ausgleichen aktiv",
"issues": "Meldungen",
"noIssues": "Keine Meldungen",
"issueName": "Bezeichnung",

View File

@ -31,6 +31,7 @@
"Yes": "Yes",
"No": "No",
"VerboseLogging": "Verbose Logging",
"Seconds": "Seconds",
"Loading": "Loading...",
"Reload": "Reload",
"Cancel": "Cancel",
@ -521,8 +522,7 @@
"EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Publish values to MQTT only when they change",
"Save": "@:dtuadmin.Save"
"UpdatesOnly": "Publish values to MQTT only when they change"
},
"powermeteradmin":{
"PowerMeterSettings": "Power Meter Settings",
@ -543,7 +543,6 @@
"SDM": "SDM-Power Meter Parameter",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Address",
"Save": "@:dtuadmin.Save",
"HTTP": "HTTP(S) + Json - General configuration",
"httpIndividualRequests": "Individual HTTP requests per phase",
"httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}",
@ -605,8 +604,7 @@
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
"InverterRestart": "Inverter Restart",
"InverterRestartHour": "Restart Hour",
"InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values.",
"Save": "@:dtuadmin.Save"
"InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values."
},
"batteryadmin": {
"BatterySettings": "Battery Settings",
@ -616,14 +614,16 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"PollingInterval": "Polling Interval",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"inverteradmin": {
"InverterSettings": "Inverter Settings",
@ -810,14 +810,13 @@
"enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.",
"lowerPowerLimit": "Minimum output power",
"upperPowerLimit": "Maximum output power",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"battery": {
"battery": "Battery",
"DataAge": "Data Age: ",
"Seconds": " {val} seconds",
"Status": "Status",
"status": "Status",
"Property": "Property",
"yes": "@:base.Yes",
"no": "@:base.No",
@ -835,9 +834,15 @@
"dischargeCurrentLimitation": "Discharge current limit",
"chargeEnabled": "Charging possible",
"dischargeEnabled": "Discharging possible",
"balancingActive": "Balancing active",
"chargeImmediately": "Immediate charging requested",
"cells": "Cells",
"batOneTemp": "Battery temperature 1",
"batTwoTemp": "Battery temperature 2",
"cellMinVoltage": "Minimum cell voltage",
"cellAvgVoltage": "Average cell voltage",
"cellMaxVoltage": "Maximum cell voltage",
"cellDiffVoltage": "Cell voltage difference",
"balancingActive": "Balancing active",
"issues": "Issues",
"noIssues": "No Issues",
"issueName": "Name",

View File

@ -31,6 +31,7 @@
"Yes": "Oui",
"No": "Non",
"VerboseLogging": "Journalisation Détaillée",
"Seconds": "Secondes",
"Loading": "Chargement...",
"Reload": "Reload",
"Cancel": "Annuler",
@ -467,7 +468,6 @@
"ApTimeout": "Délai d'attente du point d'accès",
"ApTimeoutHint": "Durée pendant laquelle le point d'accès reste ouvert. Une valeur de 0 signifie infini.",
"Minutes": "minutes",
"Save": "@:dtuadmin.Save",
"EnableMdns": "Activer mDNS",
"MdnsSettings": "mDNS Settings"
},
@ -512,8 +512,7 @@
"HassPrefixTopicHint": "Le préfixe de découverte du sujet",
"HassRetain": "Activer du maintien",
"HassExpire": "Activer l'expiration",
"HassIndividual": "Panneaux individuels",
"Save": "@:dtuadmin.Save"
"HassIndividual": "Panneaux individuels"
},
"vedirectadmin": {
"VedirectSettings": "VE.Direct Settings",
@ -521,8 +520,7 @@
"EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Publish values to MQTT only when they change",
"Save": "@:dtuadmin.Save"
"UpdatesOnly": "Publish values to MQTT only when they change"
},
"batteryadmin": {
"BatterySettings": "Battery Settings",
@ -532,14 +530,16 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"PollingInterval": "Polling Interval",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"inverteradmin": {
"InverterSettings": "Paramètres des onduleurs",
@ -648,8 +648,7 @@
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values can only be used if the battery communication interface is enabled. If the battery has not reported any SoC updates in the last minute, the voltage thresholds will be used as fallback.",
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
"Battery": "DC / Battery",
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
"Save": "@:dtuadmin.Save"
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor)."
},
"login": {
"Login": "Connexion",
@ -768,14 +767,13 @@
"enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.",
"lowerPowerLimit": "Minimum output power",
"upperPowerLimit": "Maximum output power",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
"Seconds": "@:base.Seconds"
},
"battery": {
"battery": "Battery",
"DataAge": "Data Age: ",
"Seconds": " {val} seconds",
"Status": "Status",
"status": "Status",
"Property": "Property",
"yes": "@:base.Yes",
"no": "@:base.No",
@ -793,9 +791,15 @@
"dischargeCurrentLimitation": "Discharge current limit",
"chargeEnabled": "Charging possible",
"dischargeEnabled": "Discharging possible",
"balancingActive": "Balancing active",
"chargeImmediately": "Immediate charging requested",
"cells": "Cells",
"batOneTemp": "Battery temperature 1",
"batTwoTemp": "Battery temperature 2",
"cellMinVoltage": "Minimum cell voltage",
"cellAvgVoltage": "Average cell voltage",
"cellMaxVoltage": "Maximum cell voltage",
"cellDiffVoltage": "Cell voltage difference",
"balancingActive": "Balancing active",
"issues": "Issues",
"noIssues": "No Issues",
"issueName": "Name",

View File

@ -4,4 +4,5 @@ export interface BatteryConfig {
provider: number;
jkbms_interface: number;
jkbms_polling_interval: number;
mqtt_topic: string;
}

View File

@ -1,8 +1,10 @@
import type { ValueObject } from '@/types/LiveDataStatus';
type BatteryData = (ValueObject | string)[];
export interface Battery {
manufacturer: string;
data_age: number;
values: (ValueObject | string)[];
values: BatteryData[];
issues: number[];
}

View File

@ -56,7 +56,7 @@
<div class="input-group">
<input type="number" class="form-control" id="lowerPowerLimit"
placeholder="150" v-model="acChargerConfigList.lower_power_limit"
aria-describedby="lowerPowerLimitDescription" min="100" max="3000" required/>
aria-describedby="lowerPowerLimitDescription" min="50" max="3000" required/>
<span class="input-group-text" id="lowerPowerLimitDescription">W</span>
</div>
</div>
@ -73,7 +73,7 @@
</CardElement>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('acchargeradmin.Save') }}</button>
<FormFooter @reload="getChargerConfig"/>
</form>
</BasePage>
</template>
@ -82,6 +82,7 @@
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue';
import type { AcChargerConfig } from "@/types/AcChargerConfig";
@ -93,6 +94,7 @@ export default defineComponent({
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement,
BIconInfoCircle,
},

View File

@ -49,7 +49,21 @@
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('batteryadmin.Save') }}</button>
<CardElement v-show="batteryConfigList.enabled && batteryConfigList.provider == 2"
:text="$t('batteryadmin.MqttConfiguration')" textVariant="text-bg-primary" addSpace>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('batteryadmin.MqttTopic') }}
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" v-model="batteryConfigList.mqtt_topic" />
</div>
</div>
</div>
</CardElement>
<FormFooter @reload="getBatteryConfig"/>
</form>
</BasePage>
</template>
@ -58,6 +72,7 @@
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import type { BatteryConfig } from "@/types/BatteryConfig";
import { authHeader, handleResponse } from '@/utils/authentication';
@ -68,6 +83,7 @@ export default defineComponent({
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement,
},
data() {
@ -80,6 +96,7 @@ export default defineComponent({
providerTypeList: [
{ key: 0, value: 'PylontechCan' },
{ key: 1, value: 'JkBmsSerial' },
{ key: 2, value: 'Mqtt' },
{ key: 3, value: 'Victron' },
],
jkBmsInterfaceTypeList: [

View File

@ -265,7 +265,7 @@
</div>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('powerlimiteradmin.Save') }}</button>
<FormFooter @reload="getPowerLimiterConfig"/>
</form>
</BasePage>
</template>
@ -276,6 +276,7 @@ import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue';
import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig";
@ -285,6 +286,7 @@ export default defineComponent({
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement,
BIconInfoCircle,
},

View File

@ -185,7 +185,7 @@
</div>
</div>
<button type="submit" class="btn btn-primary mb-3">{{ $t('powermeteradmin.Save') }}</button>
<FormFooter @reload="getPowerMeterConfig"/>
<div v-if="powerMeterConfigList.source === 3" class="alert alert-secondary" role="alert">
<h2>URL examples:</h2>
@ -213,6 +213,7 @@ import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig";
@ -222,6 +223,7 @@ export default defineComponent({
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement
},
data() {

View File

@ -23,7 +23,7 @@
type="checkbox" wide/>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('vedirectadmin.Save') }}</button>
<FormFooter @reload="getVedirectConfig"/>
</form>
</BasePage>
</template>
@ -32,6 +32,7 @@
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import type { VedirectConfig } from "@/types/VedirectConfig";
import { authHeader, handleResponse } from '@/utils/authentication';
@ -42,6 +43,7 @@ export default defineComponent({
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement,
},
data() {

View File

@ -17,7 +17,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8"
integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==
"@babel/parser@^7.23.5":
"@babel/parser@^7.23.6":
version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b"
integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==
@ -435,10 +435,10 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/node@^20.10.5":
version "20.10.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2"
integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==
"@types/node@^20.10.6":
version "20.10.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5"
integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==
dependencies:
undici-types "~5.26.4"
@ -547,10 +547,10 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vitejs/plugin-vue@^4.5.2":
version "4.5.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz#1212d81bc83680e14448fefe55abd9fe1ed49ed1"
integrity sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==
"@vitejs/plugin-vue@^5.0.2":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.2.tgz#8428ec3f446b9c2f7a7ec950f34e3d6f3c665444"
integrity sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==
"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1":
version "1.11.1"
@ -584,16 +584,6 @@
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-core@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.13.tgz#b3d5f8f84caee5de3f31d95cb568d899fd19c599"
integrity sha512-bwi9HShGu7uaZLOErZgsH2+ojsEdsjerbf2cMXPwmvcgZfVPZ2BVZzCVnwZBxTAYd6Mzbmf6izcUNDkWnBBQ6A==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/shared" "3.3.13"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.3.2":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.2.tgz#39567bd15c7f97add97bfc4d44e814df36eb797b"
@ -604,6 +594,17 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.3.tgz#8e8f88273f061cf0a49bf958255f5f0621f12d8b"
integrity sha512-u8jzgFg0EDtSrb/hG53Wwh1bAOQFtc1ZCegBpA/glyvTlgHl+tq13o1zvRfLbegYUw/E4mSTGOiCnAJ9SJ+lsg==
dependencies:
"@babel/parser" "^7.23.6"
"@vue/shared" "3.4.3"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-dom@3.2.47":
version "3.2.47"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305"
@ -612,13 +613,13 @@
"@vue/compiler-core" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-dom@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.13.tgz#d029e222e545e7ab00be35aafd3abed167f962bf"
integrity sha512-EYRDpbLadGtNL0Gph+HoKiYqXLqZ0xSSpR5Dvnu/Ep7ggaCbjRDIus1MMxTS2Qm0koXED4xSlvTZaTnI8cYAsw==
"@vue/compiler-dom@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.3.tgz#bea8acde9585d5ce92a3f11c062c863fb33e44d7"
integrity sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg==
dependencies:
"@vue/compiler-core" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/compiler-core" "3.4.3"
"@vue/shared" "3.4.3"
"@vue/compiler-dom@^3.3.0":
version "3.3.2"
@ -628,17 +629,16 @@
"@vue/compiler-core" "3.3.2"
"@vue/shared" "3.3.2"
"@vue/compiler-sfc@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.13.tgz#7b397acefd5c0c3808701d2855be88c4be60155c"
integrity sha512-DQVmHEy/EKIgggvnGRLx21hSqnr1smUS9Aq8tfxiiot8UR0/pXKHN9k78/qQ7etyQTFj5em5nruODON7dBeumw==
"@vue/compiler-sfc@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.3.tgz#a9d35b2deef38576dedd9938851c032fb2ca8617"
integrity sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/compiler-core" "3.3.13"
"@vue/compiler-dom" "3.3.13"
"@vue/compiler-ssr" "3.3.13"
"@vue/reactivity-transform" "3.3.13"
"@vue/shared" "3.3.13"
"@babel/parser" "^7.23.6"
"@vue/compiler-core" "3.4.3"
"@vue/compiler-dom" "3.4.3"
"@vue/compiler-ssr" "3.4.3"
"@vue/shared" "3.4.3"
estree-walker "^2.0.2"
magic-string "^0.30.5"
postcss "^8.4.32"
@ -668,13 +668,13 @@
"@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-ssr@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.13.tgz#ad8748abff8d738ac9c6a3c47be42020f0fbaa63"
integrity sha512-d/P3bCeUGmkJNS1QUZSAvoCIW4fkOKK3l2deE7zrp0ypJEy+En2AcypIkqvcFQOcw3F0zt2VfMvNsA9JmExTaw==
"@vue/compiler-ssr@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.3.tgz#c3f641a15a04893b5bc3278f3dac65bed44dce1d"
integrity sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA==
dependencies:
"@vue/compiler-dom" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/compiler-dom" "3.4.3"
"@vue/shared" "3.4.3"
"@vue/devtools-api@^6.5.0":
version "6.5.0"
@ -690,10 +690,10 @@
"@typescript-eslint/parser" "^6.7.0"
vue-eslint-parser "^9.3.1"
"@vue/language-core@1.8.26":
version "1.8.26"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.26.tgz#7edb6b51a6ed57b618928500c3cbda9757a9f5f0"
integrity sha512-9cmza/Y2YTiOnKZ0Mi9zsNn7Irw+aKirP+5LLWVSNaL3fjKJjW1cD3HGBckasY2RuVh4YycvdA9/Q6EBpVd/7Q==
"@vue/language-core@1.8.27":
version "1.8.27"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.27.tgz#2ca6892cb524e024a44e554e4c55d7a23e72263f"
integrity sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==
dependencies:
"@volar/language-core" "~1.11.1"
"@volar/source-map" "~1.11.1"
@ -716,64 +716,53 @@
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity-transform@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.13.tgz#dc8e9be961865dc666e367e1aaaea0716afa5c90"
integrity sha512-oWnydGH0bBauhXvh5KXUy61xr9gKaMbtsMHk40IK9M4gMuKPJ342tKFarY0eQ6jef8906m35q37wwA8DMZOm5Q==
"@vue/reactivity@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.3.tgz#95287b5950b328df4a942a7cf14a0e13487f1eac"
integrity sha512-q5f9HLDU+5aBKizXHAx0w4whkIANs1Muiq9R5YXm0HtorSlflqv9u/ohaMxuuhHWCji4xqpQ1eL04WvmAmGnFg==
dependencies:
"@babel/parser" "^7.23.5"
"@vue/compiler-core" "3.3.13"
"@vue/shared" "3.3.13"
estree-walker "^2.0.2"
magic-string "^0.30.5"
"@vue/shared" "3.4.3"
"@vue/reactivity@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.13.tgz#9b1dff3b523a69997b66cba2f86f83839e8285fb"
integrity sha512-fjzCxceMahHhi4AxUBzQqqVhuA21RJ0COaWTbIBl1PruGW1CeY97louZzLi4smpYx+CHfFPPU/CS8NybbGvPKQ==
"@vue/runtime-core@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.3.tgz#fe7649a93d9b20b9b351cd699f69f0e34a26e3ab"
integrity sha512-C1r6QhB1qY7D591RCSFhMULyzL9CuyrGc+3PpB0h7dU4Qqw6GNyo4BNFjHZVvsWncrUlKX3DIKg0Y7rNNr06NQ==
dependencies:
"@vue/shared" "3.3.13"
"@vue/reactivity" "3.4.3"
"@vue/shared" "3.4.3"
"@vue/runtime-core@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.13.tgz#e8414218e8c7db94acfcec6fd12044704adda9cf"
integrity sha512-1TzA5TvGuh2zUwMJgdfvrBABWZ7y8kBwBhm7BXk8rvdx2SsgcGfz2ruv2GzuGZNvL1aKnK8CQMV/jFOrxNQUMA==
"@vue/runtime-dom@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.3.tgz#54a6115cfba364f20cdf5a44c2ff87337a57def8"
integrity sha512-wrsprg7An5Ec+EhPngWdPuzkp0BEUxAKaQtN9dPU/iZctPyD9aaXmVtehPJerdQxQale6gEnhpnfywNw3zOv2A==
dependencies:
"@vue/reactivity" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/runtime-dom@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.13.tgz#36b42b479d5a394972f305ca8e95c5f648bf55ef"
integrity sha512-JJkpE8R/hJKXqVTgUoODwS5wqKtOsmJPEqmp90PDVGygtJ4C0PtOkcEYXwhiVEmef6xeXcIlrT3Yo5aQ4qkHhQ==
dependencies:
"@vue/runtime-core" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/runtime-core" "3.4.3"
"@vue/shared" "3.4.3"
csstype "^3.1.3"
"@vue/server-renderer@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.13.tgz#fccdd0787798173be8929f40f23161c17b60ed36"
integrity sha512-vSnN+nuf6iSqTL3Qgx/9A+BT+0Zf/VJOgF5uMZrKjYPs38GMYyAU1coDyBNHauehXDaP+zl73VhwWv0vBRBHcg==
"@vue/server-renderer@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.3.tgz#c508f58b9f83f0959085d5aa6854eac9141b4bc6"
integrity sha512-BUxt8oVGMKKsqSkM1uU3d3Houyfy4WAc2SpSQRebNd+XJGATVkW/rO129jkyL+kpB/2VRKzE63zwf5RtJ3XuZw==
dependencies:
"@vue/compiler-ssr" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/compiler-ssr" "3.4.3"
"@vue/shared" "3.4.3"
"@vue/shared@3.2.47":
version "3.2.47"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c"
integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==
"@vue/shared@3.3.13":
version "3.3.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.13.tgz#4cb73cda958d77ffd389c8640cf7d93a10ac676f"
integrity sha512-/zYUwiHD8j7gKx2argXEMCUXVST6q/21DFU0sTfNX0URJroCe3b1UF6vLJ3lQDfLNIiiRl2ONp7Nh5UVWS6QnA==
"@vue/shared@3.3.2", "@vue/shared@^3.3.0":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3"
integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==
"@vue/shared@3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.3.tgz#01d54b32b9796c85c853c670d9395a813f23a8c2"
integrity sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ==
"@vue/tsconfig@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz#3124ec16cc0c7e04165b88dc091e6b97782fffa9"
@ -1051,6 +1040,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@ -2244,10 +2238,10 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3"
is-regex "^1.1.4"
sass@^1.69.5:
version "1.69.5"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde"
integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==
sass@^1.69.6:
version "1.69.6"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.6.tgz#88ae1f93facc46d2da9b0bdd652d65068bcfa397"
integrity sha512-qbRr3k9JGHWXCvZU77SD2OTwUlC+gNT+61JOLcmLm+XqH4h/5D+p4IIsxvpkB89S9AwJOyb5+rWNpIucaFxSFQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@ -2605,25 +2599,25 @@ vue-template-compiler@^2.7.14:
de-indent "^1.0.2"
he "^1.2.0"
vue-tsc@^1.8.26:
version "1.8.26"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.26.tgz#f66abd1dab4e4593590b2b7d4ede0a696882feec"
integrity sha512-jMEJ4aqU/l1hdgmeExH5h1TFoN+hbho0A2ZAhHy53/947DGm7Qj/bpB85VpECOCwV00h7JYNVnvoD2ceOorB4Q==
vue-tsc@^1.8.27:
version "1.8.27"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.27.tgz#feb2bb1eef9be28017bb9e95e2bbd1ebdd48481c"
integrity sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==
dependencies:
"@volar/typescript" "~1.11.1"
"@vue/language-core" "1.8.26"
"@vue/language-core" "1.8.27"
semver "^7.5.4"
vue@^3.3.13:
version "3.3.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.13.tgz#f03098fa1b4e7cc88c133bef92260b55e3767002"
integrity sha512-LDnUpQvDgsfc0u/YgtAgTMXJlJQqjkxW1PVcOnJA5cshPleULDjHi7U45pl2VJYazSSvLH8UKcid/kzH8I0a0Q==
vue@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.3.tgz#e1ba36a64134dcedc12cfb2c28e7cd15ba121f04"
integrity sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA==
dependencies:
"@vue/compiler-dom" "3.3.13"
"@vue/compiler-sfc" "3.3.13"
"@vue/runtime-dom" "3.3.13"
"@vue/server-renderer" "3.3.13"
"@vue/shared" "3.3.13"
"@vue/compiler-dom" "3.4.3"
"@vue/compiler-sfc" "3.4.3"
"@vue/runtime-dom" "3.4.3"
"@vue/server-renderer" "3.4.3"
"@vue/shared" "3.4.3"
webpack-sources@^3.2.3:
version "3.2.3"

Binary file not shown.