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) * 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) * 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 ### 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. 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 # 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. ## Structure of the json file for openDTU-onBattery (outdated example)
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
```json ```json
[ [
@ -92,48 +88,4 @@ 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 "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", "name": "CMT2300A with SSD1306",
"nrf24": { "nrf24": {
@ -127,6 +146,33 @@
"clk": 22 "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", "name": "NRF24 + CMT2300A",
"nrf24": { "nrf24": {

View File

@ -77,5 +77,33 @@
"data": 33, "data": 33,
"clk": 32 "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, "data": 5,
"clk": 17 "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 # MQTT Topics
The base topic, as configured in the web GUI is prepended to all follwing topics. This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/mqtt_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 |

View File

@ -1,6 +1,6 @@
# Web API # 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 ## List of URLs
@ -8,36 +8,6 @@ This list may be incomplete
| GET/POST | Auth required | URL | | 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/vedirectlivedata/status |
| Get | no | /api/vedirect/status | | Get | no | /api/vedirect/status |
| Get | no | /api/huawei/status | | Get | no | /api/huawei/status |
@ -47,455 +17,6 @@ This list may be incomplete
| Get | no | /api/battery/status | | Get | no | /api/battery/status |
| Get | no | /api/powerlimiter/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): ### Victron REST-API (/api/vedirectlivedata/status):
````JSON ````JSON
{ {
@ -520,79 +41,4 @@ $ curl --no-progress-meter http://192.168.10.10/api/eventlog/status?inv=11418186
"H22":{"v":1.43,"u":"kWh"}, "H22":{"v":1.43,"u":"kWh"},
"H23":{"v":737,"u":"W"} "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; } bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
protected: 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"; String _manufacturer = "unknown";
uint8_t _SoC = 0; uint8_t _SoC = 0;
uint32_t _lastUpdateSoC = 0; uint32_t _lastUpdateSoC = 0;
@ -138,3 +128,16 @@ class VictronSmartShuntStats : public BatteryStats {
bool _alarmLowTemperature; bool _alarmLowTemperature;
bool _alarmHighTemperature; 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 Provider;
uint8_t JkBmsInterface; uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval; uint8_t JkBmsPollingInterval;
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery; } Battery;
struct { struct {

View File

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

View File

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

View File

@ -2,12 +2,13 @@
#pragma once #pragma once
#include <AsyncWebSocket.h> #include <AsyncWebSocket.h>
#include <HardwareSerial.h>
#include <Stream.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <Print.h>
#include <freertos/task.h>
#include <mutex> #include <mutex>
#include <vector>
#define BUFFER_SIZE 500 #include <unordered_map>
#include <queue>
class MessageOutputClass : public Print { class MessageOutputClass : public Print {
public: public:
@ -21,13 +22,19 @@ private:
Task _loopTask; 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; AsyncWebSocket* _ws = nullptr;
char _buffer[BUFFER_SIZE];
uint16_t _buff_pos = 0;
uint32_t _lastSend = 0;
bool _forceSend = false;
std::mutex _msgLock; std::mutex _msgLock;
void serialWrite(message_t const& m);
}; };
extern MessageOutputClass MessageOutput; 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 <Huawei_can.h>
#include <espMqttClient.h> #include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandleHuaweiClass { class MqttHandleHuaweiClass {
public: public:
@ -12,13 +15,30 @@ public:
private: private:
void loop(); 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; Task _loopTask;
uint32_t _lastPublishStats; uint32_t _lastPublishStats;
uint32_t _lastPublish; 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; extern MqttHandleHuaweiClass MqttHandleHuawei;

View File

@ -4,6 +4,9 @@
#include "Configuration.h" #include "Configuration.h"
#include <espMqttClient.h> #include <espMqttClient.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <deque>
#include <functional>
class MqttHandlePowerLimiterClass { class MqttHandlePowerLimiterClass {
public: public:
@ -18,6 +21,11 @@ private:
uint32_t _lastPublishStats; uint32_t _lastPublishStats;
uint32_t _lastPublish; 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; extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;

View File

@ -9,13 +9,16 @@
#include <frozen/string.h> #include <frozen/string.h>
const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> GridProfileParser::_profileTypes = { { const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> GridProfileParser::_profileTypes = { {
{ 0x02, 0x00, "no data (yet)" }, { 0x02, 0x00, "US - NA_IEEE1547_240V" },
{ 0x03, 0x00, "Germany - DE_VDE4105_2018" }, { 0x03, 0x00, "DE - DE_VDE4105_2018" },
{ 0x0a, 0x00, "European - EN 50549-1:2019" }, { 0x03, 0x01, "XX - unknown" },
{ 0x0c, 0x00, "AT Tor - EU_EN50438" }, { 0x0a, 0x00, "XX - EN 50549-1:2019" },
{ 0x0d, 0x04, "France" }, { 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" },
{ 0x12, 0x00, "Poland - EU_EN50438" }, { 0x0d, 0x04, "FR -" },
{ 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" }, { 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 = { 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; 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) }, { 0x01, make_value("Nominale Voltage (NV)", "V", 10) },
{ 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) },
{ 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) },
{ 0x04, make_value("High Voltage 1 (HV1)", "V", 10) }, { 0x04, make_value("High Voltage 1 (HV1)", "V", 10) },
{ 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) }, { 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) },
{ 0x06, make_value("Low Voltage 2 (LV2)", "V", 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) }, { 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) }, { 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) },
{ 0x0b, make_value("High Voltage 3 (HV3)", "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) }, { 0x0d, make_value("Nominal Frequency", "Hz", 100) },
{ 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) }, { 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) },
{ 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) }, { 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) }, { 0x2e, make_value("Voltage Set Point V3", "V", 10) },
{ 0x2f, make_value("Voltage Set Point V4", "V", 10) }, { 0x2f, make_value("Voltage Set Point V4", "V", 10) },
{ 0x30, make_value("Reactive Set Point Q4", "%Pn", 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) }, { 0x32, make_value("SPF Function Activated", "bool", 1) },
{ 0x33, make_value("Power Factor (PF)", "", 100) }, { 0x33, make_value("Power Factor (PF)", "", 100) },
{ 0x34, make_value("RPC Function Activated", "bool", 1) }, { 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) }, { 0x36, make_value("WPF Function Activated", "bool", 1) },
{ 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) },
{ 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, { 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) }, { 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, 0x04 },
{ 0x00, 0x00, 0x05 }, { 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 // Version 0x03
{ 0x00, 0x03, 0x01 }, { 0x00, 0x03, 0x01 },
{ 0x00, 0x03, 0x02 }, { 0x00, 0x03, 0x02 },
@ -178,10 +208,10 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x00, 0x35, 0x07 }, { 0x00, 0x35, 0x07 },
{ 0x00, 0x35, 0x08 }, { 0x00, 0x35, 0x08 },
{ 0x00, 0x35, 0x09 }, { 0x00, 0x35, 0x09 },
{ 0x00, 0x35, 0xff }, { 0x00, 0x35, 0x39 },
{ 0x00, 0x35, 0xff }, { 0x00, 0x35, 0x3a },
{ 0x00, 0x35, 0xff }, { 0x00, 0x35, 0x3b },
{ 0x00, 0x35, 0xff }, { 0x00, 0x35, 0x3c },
// Frequency (H/LFRT) // Frequency (H/LFRT)
// Version 0x00 // Version 0x00
@ -198,9 +228,9 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x10, 0x03, 0x10 }, { 0x10, 0x03, 0x10 },
{ 0x10, 0x03, 0x11 }, { 0x10, 0x03, 0x11 },
{ 0x10, 0x03, 0x12 }, { 0x10, 0x03, 0x12 },
{ 0x10, 0x03, 0x13 }, { 0x10, 0x03, 0x3e },
{ 0x10, 0x03, 0x14 }, { 0x10, 0x03, 0x14 },
{ 0x10, 0x03, 0x15 }, { 0x10, 0x03, 0x3f },
// Island Detection (ID) // Island Detection (ID)
// Version 0x00 // Version 0x00
@ -220,8 +250,8 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x30, 0x07, 0x19 }, { 0x30, 0x07, 0x19 },
{ 0x30, 0x07, 0x1a }, { 0x30, 0x07, 0x1a },
{ 0x30, 0x07, 0x1b }, { 0x30, 0x07, 0x1b },
{ 0x30, 0x07, 0xff }, { 0x30, 0x07, 0x40 },
{ 0x30, 0x07, 0xff }, { 0x30, 0x07, 0x41 },
// Ramp Rates (RR) // Ramp Rates (RR)
// Version 0x00 // Version 0x00
@ -255,7 +285,7 @@ const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> GridProfileParse
{ 0x50, 0x11, 0x1f }, { 0x50, 0x11, 0x1f },
{ 0x50, 0x11, 0x20 }, { 0x50, 0x11, 0x20 },
{ 0x50, 0x11, 0x21 }, { 0x50, 0x11, 0x21 },
{ 0x50, 0x11, 0x22 }, { 0x50, 0x11, 0x3d },
// Volt Watt (VW) // Volt Watt (VW)
// Version 0x00 // Version 0x00

View File

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

View File

@ -19,12 +19,16 @@ extra_configs =
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
framework = arduino framework = arduino
platform = espressif32@6.3.2 platform = espressif32@6.5.0
build_flags = build_flags =
-DPIOENV=\"$PIOENV\" -DPIOENV=\"$PIOENV\"
-D_TASK_STD_FUNCTION=1 -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=c++17
-std=gnu++17 -std=gnu++17
build_unflags = build_unflags =
@ -35,7 +39,7 @@ lib_deps =
bblanchon/ArduinoJson @ ^6.21.4 bblanchon/ArduinoJson @ ^6.21.4
https://github.com/bertmelis/espMqttClient.git#v1.5.0 https://github.com/bertmelis/espMqttClient.git#v1.5.0
nrf24/RF24 @ ^1.4.8 nrf24/RF24 @ ^1.4.8
olikraus/U8g2 @ ^2.35.8 olikraus/U8g2 @ ^2.35.9
buelowp/sunset @ ^1.1.7 buelowp/sunset @ ^1.1.7
https://github.com/arkhipenko/TaskScheduler#testing https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/arkhipenko/TaskScheduler#testing https://github.com/arkhipenko/TaskScheduler#testing

View File

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

View File

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

View File

@ -199,6 +199,7 @@ bool ConfigurationClass::write()
battery["provider"] = config.Battery.Provider; battery["provider"] = config.Battery.Provider;
battery["jkbms_interface"] = config.Battery.JkBmsInterface; battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttTopic;
JsonObject huawei = doc.createNestedObject("huawei"); JsonObject huawei = doc.createNestedObject("huawei");
huawei["enabled"] = config.Huawei.Enabled; huawei["enabled"] = config.Huawei.Enabled;
@ -435,6 +436,7 @@ bool ConfigurationClass::read()
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER; config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; 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"]; JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; 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::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::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::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 // 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) 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; _display_type = type;
if (_display_type > DisplayType_t::None) { if (isValidDisplay()) {
auto constructor = display_types[_display_type]; auto constructor = display_types[_display_type];
_display = constructor(reset, clk, data, cs); _display = constructor(reset, clk, data, cs);
_display->begin(); _display->begin();
setContrast(DISPLAY_CONTRAST); setContrast(DISPLAY_CONTRAST);
setStatus(true); setStatus(true);
_diagram.init(scheduler, _display); _diagram.init(scheduler, _display);
}
scheduler.addTask(_loopTask); scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this)); _loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this));
_loopTask.setIterations(TASK_FOREVER); _loopTask.setIterations(TASK_FOREVER);
_loopTask.setInterval(_period); _loopTask.setInterval(_period);
_loopTask.enable(); _loopTask.enable();
}
} }
void DisplayGraphicClass::calcLineHeights() 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) void DisplayGraphicClass::printText(const char* text, const uint8_t line)
{ {
uint8_t dispX; uint8_t dispX;
@ -102,7 +108,7 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line)
void DisplayGraphicClass::setOrientation(const uint8_t rotation) void DisplayGraphicClass::setOrientation(const uint8_t rotation)
{ {
if (_display_type == DisplayType_t::None) { if (!isValidDisplay()) {
return; return;
} }
@ -132,7 +138,7 @@ void DisplayGraphicClass::setLanguage(const uint8_t language)
void DisplayGraphicClass::setStartupDisplay() void DisplayGraphicClass::setStartupDisplay()
{ {
if (_display_type == DisplayType_t::None) { if (!isValidDisplay()) {
return; return;
} }
@ -148,10 +154,6 @@ DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram()
void DisplayGraphicClass::loop() void DisplayGraphicClass::loop()
{ {
if (_display_type == DisplayType_t::None) {
return;
}
_loopTask.setInterval(_period); _loopTask.setInterval(_period);
_display->clearBuffer(); _display->clearBuffer();
@ -215,7 +217,7 @@ void DisplayGraphicClass::loop()
void DisplayGraphicClass::setContrast(const uint8_t contrast) void DisplayGraphicClass::setContrast(const uint8_t contrast)
{ {
if (_display_type == DisplayType_t::None) { if (!isValidDisplay()) {
return; return;
} }
_display->setContrast(contrast * 2.55f); _display->setContrast(contrast * 2.55f);

View File

@ -62,20 +62,28 @@ void DisplayGraphicDiagramClass::updatePeriod()
void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX) 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 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 // draw diagram axis
_display->drawVLine(graphPosX, graphPosY, CHART_HEIGHT); _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 // UP-arrow
_display->drawLine(graphPosX - 2, graphPosY + 2, graphPosX - 1, graphPosY + 1); // UP-arrow _display->drawLine(graphPosX, graphPosY, graphPosX + arrow_size, graphPosY + arrow_size);
_display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT - 3, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT - 2); // LEFT-arrow _display->drawLine(graphPosX, graphPosY, graphPosX - arrow_size, graphPosY + arrow_size);
_display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT + 1, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT); // LEFT-arrow
// 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 // 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]; char fmtText[7];
const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end()); const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end());
if (maxWatts > 999) { if (maxWatts > 999) {
@ -84,25 +92,24 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX)
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts)); snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts));
} }
const uint8_t textLength = strlen(fmtText); const uint8_t textLength = strlen(fmtText);
const uint8_t space_and_arrow_pixels = 2; _display->drawStr(graphPosX - arrow_size - textLength * 4, graphPosY + 5, fmtText);
_display->drawStr(graphPosX - space_and_arrow_pixels - (textLength * 4), graphPosY + 5, fmtText);
// draw chart // draw chart
const float scaleFactor = maxWatts / CHART_HEIGHT; const float scaleFactor = maxWatts / CHART_HEIGHT;
uint8_t axisTick = 1; uint8_t axisTick = 1;
for (int i = 0; i < _graphValuesCount; i++) { for (uint8_t i = 1; 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));
}
}
// draw one tick per hour to the x-axis // draw one tick per hour to the x-axis
if (i * getSecondsPerDot() > (3600u * axisTick)) { if (i * getSecondsPerDot() > (3600u * axisTick)) {
_display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT); _display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT);
axisTick++; 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 * Copyright (C) 2022-2023 Thomas Basler and others
*/ */
#include <HardwareSerial.h>
#include "MessageOutput.h" #include "MessageOutput.h"
#include <Arduino.h>
MessageOutputClass MessageOutput; MessageOutputClass MessageOutput;
void MessageOutputClass::init(Scheduler& scheduler) void MessageOutputClass::init(Scheduler& scheduler)
@ -18,46 +17,97 @@ void MessageOutputClass::init(Scheduler& scheduler)
void MessageOutputClass::register_ws_output(AsyncWebSocket* output) void MessageOutputClass::register_ws_output(AsyncWebSocket* output)
{ {
std::lock_guard<std::mutex> lock(_msgLock);
_ws = output; _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) size_t MessageOutputClass::write(uint8_t c)
{ {
if (_buff_pos < BUFFER_SIZE) { std::lock_guard<std::mutex> lock(_msgLock);
std::lock_guard<std::mutex> lock(_msgLock);
_buffer[_buff_pos] = c; auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t());
_buff_pos++; auto iter = res.first;
} else { auto& message = iter->second;
_forceSend = true;
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); 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() void MessageOutputClass::loop()
{ {
// Send data via websocket if either time is over or buffer is full std::lock_guard<std::mutex> lock(_msgLock);
if (_forceSend || (millis() - _lastSend > 1000)) {
std::lock_guard<std::mutex> lock(_msgLock); // clean up (possibly filled) buffers of deleted tasks
if (_ws && _buff_pos > 0) { auto map_iter = _task_messages.begin();
_ws->textAll(_buffer, _buff_pos); while (map_iter != _task_messages.end()) {
_buff_pos = 0; 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 "WebApi_Huawei.h"
#include <ctime> #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; MqttHandleHuaweiClass MqttHandleHuawei;
void MqttHandleHuaweiClass::init(Scheduler& scheduler) void MqttHandleHuaweiClass::init(Scheduler& scheduler)
@ -25,19 +19,22 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler)
_loopTask.setIterations(TASK_FOREVER); _loopTask.setIterations(TASK_FOREVER);
_loopTask.enable(); _loopTask.enable();
using std::placeholders::_1; String const& prefix = MqttSettings.getPrefix();
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
String topic = MqttSettings.getPrefix(); auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
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)); String fullTopic(prefix + "huawei/cmd/" + subTopic);
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(fullTopic.c_str(), 0,
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)); std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t,
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)); std::placeholders::_1, std::placeholders::_2,
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_MODE).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); 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(); _lastPublish = millis();
@ -46,13 +43,21 @@ void MqttHandleHuaweiClass::init(Scheduler& scheduler)
void MqttHandleHuaweiClass::loop() 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; return;
} }
const CONFIG_T& config = Configuration.get(); for (auto& callback : _mqttCallbacks) { callback(); }
_mqttCallbacks.clear();
if (!config.Huawei.Enabled) { mqttLock.unlock();
if (!MqttSettings.getConnected() ) {
return; 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(); std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
// ignore messages if Huawei is disabled try {
if (!config.Huawei.Enabled) { 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; return;
} }
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics std::lock_guard<std::mutex> mqttLock(_mqttMutex);
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
char* setting; switch (t) {
char* rest = &token_topic[strlen(config.Mqtt.Topic)]; 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" case Topic::LimitOfflineVoltage:
strtok_r(rest, "/", &rest); // Remove "cmd" MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
setting = strtok_r(rest, "/", &rest); &HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE));
break;
if (setting == NULL) { case Topic::LimitOnlineCurrent:
return; MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
_mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue,
&HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT));
break;
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;
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;
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;
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;
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;
default:
MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val);
break;
}
break;
} }
char* strlimit = new char[len + 1];
memcpy(strlimit, payload, len);
strlimit[len] = '\0';
float payload_val = strtof(strlimit, NULL);
delete[] strlimit;
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);
} 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);
} 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);
} 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);
}
}
} }

View File

@ -34,11 +34,21 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler)
void MqttHandlePowerLimiterClass::loop() 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; 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) ) { if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) {
auto val = static_cast<unsigned>(PowerLimiter.getMode()); auto val = static_cast<unsigned>(PowerLimiter.getMode());
@ -53,13 +63,6 @@ void MqttHandlePowerLimiterClass::loop()
void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties, void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties,
const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) 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); std::string strValue(reinterpret_cast<const char*>(payload), len);
int intValue = -1; int intValue = -1;
try { try {
@ -71,19 +74,24 @@ void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessagePro
return; return;
} }
std::lock_guard<std::mutex> mqttLock(_mqttMutex);
using Mode = PowerLimiterClass::Mode; using Mode = PowerLimiterClass::Mode;
switch (static_cast<Mode>(intValue)) { switch (static_cast<Mode>(intValue)) {
case Mode::UnconditionalFullSolarPassthrough: case Mode::UnconditionalFullSolarPassthrough:
MessageOutput.println("Power limiter unconditional full solar PT"); MessageOutput.println("Power limiter unconditional full solar PT");
PowerLimiter.setMode(Mode::UnconditionalFullSolarPassthrough); _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::UnconditionalFullSolarPassthrough));
break; break;
case Mode::Disabled: case Mode::Disabled:
MessageOutput.println("Power limiter disabled (override)"); MessageOutput.println("Power limiter disabled (override)");
PowerLimiter.setMode(Mode::Disabled); _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Disabled));
break; break;
case Mode::Normal: case Mode::Normal:
MessageOutput.println("Power limiter normal operation"); MessageOutput.println("Power limiter normal operation");
PowerLimiter.setMode(Mode::Normal); _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode,
&PowerLimiter, Mode::Normal));
break; break;
default: default:
MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); 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("provider")] = config.Battery.Provider;
root[F("jkbms_interface")] = config.Battery.JkBmsInterface; root[F("jkbms_interface")] = config.Battery.JkBmsInterface;
root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval; root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval;
root[F("mqtt_topic")] = config.Battery.MqttTopic;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -106,6 +107,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
config.Battery.Provider = root[F("provider")].as<uint8_t>(); config.Battery.Provider = root[F("provider")].as<uint8_t>();
config.Battery.JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>(); config.Battery.JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>();
config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].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(); Configuration.write();
retMsg[F("type")] = F("success"); retMsg[F("type")] = F("success");

View File

@ -68,6 +68,14 @@ void WebApiWsLiveClass::loop()
try { try {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
DynamicJsonDocument root(4200 * INV_MAX_COUNT); // TODO(helge) check if this calculation is correct 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; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);

View File

@ -18,7 +18,7 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"sortablejs": "^1.15.1", "sortablejs": "^1.15.1",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.3.13", "vue": "^3.4.3",
"vue-i18n": "^9.8.0", "vue-i18n": "^9.8.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
@ -27,21 +27,21 @@
"@rushstack/eslint-patch": "^1.6.1", "@rushstack/eslint-patch": "^1.6.1",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/node": "^20.10.5", "@types/node": "^20.10.6",
"@types/sortablejs": "^1.15.7", "@types/sortablejs": "^1.15.7",
"@types/spark-md5": "^3.0.4", "@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/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.69.5", "sass": "^1.69.6",
"terser": "^5.26.0", "terser": "^5.26.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.10", "vite": "^5.0.10",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.3.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> </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="row gy-3">
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent"> <div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card"> <div class="card">
@ -27,9 +27,9 @@
<div class="card-body"> <div class="card-body">
<div class="row flex-row flex-wrap align-items-start g-3"> <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" :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"> <div class="card-body">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
@ -40,7 +40,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <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> <th scope="row">{{ $t('battery.' + key) }}</th>
<td style="text-align: right"> <td style="text-align: right">
<template v-if="typeof prop === 'string'"> <template v-if="typeof prop === 'string'">

View File

@ -31,6 +31,7 @@
"Yes": "Ja", "Yes": "Ja",
"No": "Nein", "No": "Nein",
"VerboseLogging": "Ausführliche Protokollierung", "VerboseLogging": "Ausführliche Protokollierung",
"Seconds": "Sekunden",
"Loading": "Lade...", "Loading": "Lade...",
"Reload": "Aktualisieren", "Reload": "Aktualisieren",
"Cancel": "Abbrechen", "Cancel": "Abbrechen",
@ -519,8 +520,7 @@
"EnableVedirect": "Aktiviere VE.Direct", "EnableVedirect": "Aktiviere VE.Direct",
"VedirectParameter": "VE.Direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden", "UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden"
"Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{ "powermeteradmin":{
"PowerMeterSettings": "Stromzähler Einstellungen", "PowerMeterSettings": "Stromzähler Einstellungen",
@ -541,7 +541,6 @@
"SDM": "SDM-Stromzähler Konfiguration", "SDM": "SDM-Stromzähler Konfiguration",
"sdmbaudrate": "Baudrate", "sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Adresse", "sdmaddress": "Modbus Adresse",
"Save": "@:dtuadmin.Save",
"HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", "HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration",
"httpIndividualRequests": "Individuelle HTTP requests pro Phase", "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.", "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).", "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", "InverterRestart": "Wechselrichter Neustart",
"InverterRestartHour": "Stunde für Neustart", "InverterRestartHour": "Stunde für Neustart",
"InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen.", "InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen."
"Save": "@:dtuadmin.Save"
}, },
"batteryadmin": { "batteryadmin": {
"BatterySettings": "Batterie Einstellungen", "BatterySettings": "Batterie Einstellungen",
@ -607,14 +605,16 @@
"Provider": "Datenanbieter", "Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus", "ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung", "ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "State of Charge (SoC) Wert aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle", "ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"MqttConfiguration": "MQTT Einstellungen",
"MqttTopic": "SoC-Wert Topic",
"JkBmsConfiguration": "JK BMS Einstellungen", "JkBmsConfiguration": "JK BMS Einstellungen",
"JkBmsInterface": "Schnittstellentyp", "JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU", "JkBmsInterfaceUart": "TTL-UART an der MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU",
"PollingInterval": "Abfrageintervall", "PollingInterval": "Abfrageintervall",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"inverteradmin": { "inverteradmin": {
"InverterSettings": "Wechselrichter Einstellungen", "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.", "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", "lowerPowerLimit": "Minimale Leistung",
"upperPowerLimit": "Maximale Leistung", "upperPowerLimit": "Maximale Leistung",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"battery": { "battery": {
"battery": "Batterie", "battery": "Batterie",
"DataAge": "letzte Aktualisierung: ", "DataAge": "letzte Aktualisierung: ",
"Seconds": "vor {val} Sekunden", "Seconds": "vor {val} Sekunden",
"Status": "Status", "status": "Status",
"Property": "Eigenschaft", "Property": "Eigenschaft",
"yes": "@:base.Yes", "yes": "@:base.Yes",
"no": "@:base.No", "no": "@:base.No",
@ -825,9 +824,15 @@
"dischargeCurrentLimitation": "Entladestromlimit", "dischargeCurrentLimitation": "Entladestromlimit",
"chargeEnabled": "Laden ermöglicht", "chargeEnabled": "Laden ermöglicht",
"dischargeEnabled": "Entladen ermöglicht", "dischargeEnabled": "Entladen ermöglicht",
"balancingActive": "Ausgleichen aktiv",
"chargeImmediately": "Sofortiges Laden angefordert", "chargeImmediately": "Sofortiges Laden angefordert",
"cells": "Zellen",
"batOneTemp": "Batterietemperatur 1",
"batTwoTemp": "Batterietemperatur 2",
"cellMinVoltage": "Kleinste Zellspannung",
"cellAvgVoltage": "Durchschnittliche Zellspannung", "cellAvgVoltage": "Durchschnittliche Zellspannung",
"cellMaxVoltage": "Höchste Zellspannung",
"cellDiffVoltage": "Zellspannungsdifferenz",
"balancingActive": "Ausgleichen aktiv",
"issues": "Meldungen", "issues": "Meldungen",
"noIssues": "Keine Meldungen", "noIssues": "Keine Meldungen",
"issueName": "Bezeichnung", "issueName": "Bezeichnung",

View File

@ -31,6 +31,7 @@
"Yes": "Yes", "Yes": "Yes",
"No": "No", "No": "No",
"VerboseLogging": "Verbose Logging", "VerboseLogging": "Verbose Logging",
"Seconds": "Seconds",
"Loading": "Loading...", "Loading": "Loading...",
"Reload": "Reload", "Reload": "Reload",
"Cancel": "Cancel", "Cancel": "Cancel",
@ -521,8 +522,7 @@
"EnableVedirect": "Enable VE.Direct", "EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "VE.Direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Publish values to MQTT only when they change", "UpdatesOnly": "Publish values to MQTT only when they change"
"Save": "@:dtuadmin.Save"
}, },
"powermeteradmin":{ "powermeteradmin":{
"PowerMeterSettings": "Power Meter Settings", "PowerMeterSettings": "Power Meter Settings",
@ -543,7 +543,6 @@
"SDM": "SDM-Power Meter Parameter", "SDM": "SDM-Power Meter Parameter",
"sdmbaudrate": "Baudrate", "sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Address", "sdmaddress": "Modbus Address",
"Save": "@:dtuadmin.Save",
"HTTP": "HTTP(S) + Json - General configuration", "HTTP": "HTTP(S) + Json - General configuration",
"httpIndividualRequests": "Individual HTTP requests per phase", "httpIndividualRequests": "Individual HTTP requests per phase",
"httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}", "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).", "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", "InverterRestart": "Inverter Restart",
"InverterRestartHour": "Restart Hour", "InverterRestartHour": "Restart Hour",
"InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values.", "InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values."
"Save": "@:dtuadmin.Save"
}, },
"batteryadmin": { "batteryadmin": {
"BatterySettings": "Battery Settings", "BatterySettings": "Battery Settings",
@ -616,14 +614,16 @@
"Provider": "Data Provider", "Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus", "ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface", "ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings", "JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type", "JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"PollingInterval": "Polling Interval", "PollingInterval": "Polling Interval",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"inverteradmin": { "inverteradmin": {
"InverterSettings": "Inverter Settings", "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.", "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", "lowerPowerLimit": "Minimum output power",
"upperPowerLimit": "Maximum output power", "upperPowerLimit": "Maximum output power",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"battery": { "battery": {
"battery": "Battery", "battery": "Battery",
"DataAge": "Data Age: ", "DataAge": "Data Age: ",
"Seconds": " {val} seconds", "Seconds": " {val} seconds",
"Status": "Status", "status": "Status",
"Property": "Property", "Property": "Property",
"yes": "@:base.Yes", "yes": "@:base.Yes",
"no": "@:base.No", "no": "@:base.No",
@ -835,9 +834,15 @@
"dischargeCurrentLimitation": "Discharge current limit", "dischargeCurrentLimitation": "Discharge current limit",
"chargeEnabled": "Charging possible", "chargeEnabled": "Charging possible",
"dischargeEnabled": "Discharging possible", "dischargeEnabled": "Discharging possible",
"balancingActive": "Balancing active",
"chargeImmediately": "Immediate charging requested", "chargeImmediately": "Immediate charging requested",
"cells": "Cells",
"batOneTemp": "Battery temperature 1",
"batTwoTemp": "Battery temperature 2",
"cellMinVoltage": "Minimum cell voltage",
"cellAvgVoltage": "Average cell voltage", "cellAvgVoltage": "Average cell voltage",
"cellMaxVoltage": "Maximum cell voltage",
"cellDiffVoltage": "Cell voltage difference",
"balancingActive": "Balancing active",
"issues": "Issues", "issues": "Issues",
"noIssues": "No Issues", "noIssues": "No Issues",
"issueName": "Name", "issueName": "Name",

View File

@ -31,6 +31,7 @@
"Yes": "Oui", "Yes": "Oui",
"No": "Non", "No": "Non",
"VerboseLogging": "Journalisation Détaillée", "VerboseLogging": "Journalisation Détaillée",
"Seconds": "Secondes",
"Loading": "Chargement...", "Loading": "Chargement...",
"Reload": "Reload", "Reload": "Reload",
"Cancel": "Annuler", "Cancel": "Annuler",
@ -467,7 +468,6 @@
"ApTimeout": "Délai d'attente du point d'accès", "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.", "ApTimeoutHint": "Durée pendant laquelle le point d'accès reste ouvert. Une valeur de 0 signifie infini.",
"Minutes": "minutes", "Minutes": "minutes",
"Save": "@:dtuadmin.Save",
"EnableMdns": "Activer mDNS", "EnableMdns": "Activer mDNS",
"MdnsSettings": "mDNS Settings" "MdnsSettings": "mDNS Settings"
}, },
@ -512,8 +512,7 @@
"HassPrefixTopicHint": "Le préfixe de découverte du sujet", "HassPrefixTopicHint": "Le préfixe de découverte du sujet",
"HassRetain": "Activer du maintien", "HassRetain": "Activer du maintien",
"HassExpire": "Activer l'expiration", "HassExpire": "Activer l'expiration",
"HassIndividual": "Panneaux individuels", "HassIndividual": "Panneaux individuels"
"Save": "@:dtuadmin.Save"
}, },
"vedirectadmin": { "vedirectadmin": {
"VedirectSettings": "VE.Direct Settings", "VedirectSettings": "VE.Direct Settings",
@ -521,8 +520,7 @@
"EnableVedirect": "Enable VE.Direct", "EnableVedirect": "Enable VE.Direct",
"VedirectParameter": "VE.Direct Parameter", "VedirectParameter": "VE.Direct Parameter",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Publish values to MQTT only when they change", "UpdatesOnly": "Publish values to MQTT only when they change"
"Save": "@:dtuadmin.Save"
}, },
"batteryadmin": { "batteryadmin": {
"BatterySettings": "Battery Settings", "BatterySettings": "Battery Settings",
@ -532,14 +530,16 @@
"Provider": "Data Provider", "Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus", "ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface", "ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings", "JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type", "JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceUart": "TTL-UART on MCU",
"JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU",
"PollingInterval": "Polling Interval", "PollingInterval": "Polling Interval",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"inverteradmin": { "inverteradmin": {
"InverterSettings": "Paramètres des onduleurs", "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.", "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", "InverterIsBehindPowerMeter": "Inverter is behind Power meter",
"Battery": "DC / Battery", "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).", "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"
}, },
"login": { "login": {
"Login": "Connexion", "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.", "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", "lowerPowerLimit": "Minimum output power",
"upperPowerLimit": "Maximum output power", "upperPowerLimit": "Maximum output power",
"Seconds": "@:dtuadmin.Seconds", "Seconds": "@:base.Seconds"
"Save": "@:dtuadmin.Save"
}, },
"battery": { "battery": {
"battery": "Battery", "battery": "Battery",
"DataAge": "Data Age: ", "DataAge": "Data Age: ",
"Seconds": " {val} seconds", "Seconds": " {val} seconds",
"Status": "Status", "status": "Status",
"Property": "Property", "Property": "Property",
"yes": "@:base.Yes", "yes": "@:base.Yes",
"no": "@:base.No", "no": "@:base.No",
@ -793,9 +791,15 @@
"dischargeCurrentLimitation": "Discharge current limit", "dischargeCurrentLimitation": "Discharge current limit",
"chargeEnabled": "Charging possible", "chargeEnabled": "Charging possible",
"dischargeEnabled": "Discharging possible", "dischargeEnabled": "Discharging possible",
"balancingActive": "Balancing active",
"chargeImmediately": "Immediate charging requested", "chargeImmediately": "Immediate charging requested",
"cells": "Cells",
"batOneTemp": "Battery temperature 1",
"batTwoTemp": "Battery temperature 2",
"cellMinVoltage": "Minimum cell voltage",
"cellAvgVoltage": "Average cell voltage", "cellAvgVoltage": "Average cell voltage",
"cellMaxVoltage": "Maximum cell voltage",
"cellDiffVoltage": "Cell voltage difference",
"balancingActive": "Balancing active",
"issues": "Issues", "issues": "Issues",
"noIssues": "No Issues", "noIssues": "No Issues",
"issueName": "Name", "issueName": "Name",

View File

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

View File

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

View File

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

View File

@ -49,7 +49,21 @@
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/> type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/>
</CardElement> </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> </form>
</BasePage> </BasePage>
</template> </template>
@ -58,6 +72,7 @@
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import type { BatteryConfig } from "@/types/BatteryConfig"; import type { BatteryConfig } from "@/types/BatteryConfig";
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
@ -68,6 +83,7 @@ export default defineComponent({
BasePage, BasePage,
BootstrapAlert, BootstrapAlert,
CardElement, CardElement,
FormFooter,
InputElement, InputElement,
}, },
data() { data() {
@ -80,6 +96,7 @@ export default defineComponent({
providerTypeList: [ providerTypeList: [
{ key: 0, value: 'PylontechCan' }, { key: 0, value: 'PylontechCan' },
{ key: 1, value: 'JkBmsSerial' }, { key: 1, value: 'JkBmsSerial' },
{ key: 2, value: 'Mqtt' },
{ key: 3, value: 'Victron' }, { key: 3, value: 'Victron' },
], ],
jkBmsInterfaceTypeList: [ jkBmsInterfaceTypeList: [

View File

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

View File

@ -185,7 +185,7 @@
</div> </div>
</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"> <div v-if="powerMeterConfigList.source === 3" class="alert alert-secondary" role="alert">
<h2>URL examples:</h2> <h2>URL examples:</h2>
@ -213,6 +213,7 @@ import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import { handleResponse, authHeader } from '@/utils/authentication'; import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig";
@ -222,6 +223,7 @@ export default defineComponent({
BasePage, BasePage,
BootstrapAlert, BootstrapAlert,
CardElement, CardElement,
FormFooter,
InputElement InputElement
}, },
data() { data() {

View File

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

View File

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

Binary file not shown.